@librechat/agents 3.0.69 → 3.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +95 -73
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/ToolSearch.mjs +95 -74
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/types/tools/ToolSearch.d.ts +12 -4
- package/package.json +1 -1
- package/src/tools/ToolSearch.ts +117 -82
- package/src/tools/__tests__/ToolSearch.test.ts +116 -25
package/src/tools/ToolSearch.ts
CHANGED
|
@@ -465,30 +465,27 @@ function parseSearchResults(stdout: string): t.ToolSearchResponse {
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
467
|
/**
|
|
468
|
-
* Formats search results
|
|
468
|
+
* Formats search results as structured JSON for efficient parsing.
|
|
469
469
|
* @param searchResponse - The parsed search response
|
|
470
|
-
* @returns
|
|
470
|
+
* @returns JSON string with search results
|
|
471
471
|
*/
|
|
472
472
|
function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
|
|
473
473
|
const { tool_references, total_tools_searched, pattern_used } =
|
|
474
474
|
searchResponse;
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
response += `Total tools searched: ${total_tools_searched}\n`;
|
|
489
|
-
response += `Pattern used: ${pattern_used}`;
|
|
476
|
+
const output = {
|
|
477
|
+
found: tool_references.length,
|
|
478
|
+
tools: tool_references.map((ref) => ({
|
|
479
|
+
name: ref.tool_name,
|
|
480
|
+
score: Number(ref.match_score.toFixed(2)),
|
|
481
|
+
matched_in: ref.matched_field,
|
|
482
|
+
snippet: ref.snippet,
|
|
483
|
+
})),
|
|
484
|
+
total_searched: total_tools_searched,
|
|
485
|
+
query: pattern_used,
|
|
486
|
+
};
|
|
490
487
|
|
|
491
|
-
return
|
|
488
|
+
return JSON.stringify(output, null, 2);
|
|
492
489
|
}
|
|
493
490
|
|
|
494
491
|
/**
|
|
@@ -505,12 +502,61 @@ function getBaseToolName(toolName: string): string {
|
|
|
505
502
|
}
|
|
506
503
|
|
|
507
504
|
/**
|
|
508
|
-
*
|
|
509
|
-
*
|
|
505
|
+
* Generates a compact listing of deferred tools grouped by server.
|
|
506
|
+
* Format: "server: tool1, tool2, tool3"
|
|
507
|
+
* Non-MCP tools are grouped under "other".
|
|
508
|
+
* @param toolRegistry - The tool registry
|
|
509
|
+
* @param onlyDeferred - Whether to only include deferred tools
|
|
510
|
+
* @returns Formatted string with tools grouped by server
|
|
511
|
+
*/
|
|
512
|
+
function getDeferredToolsListing(
|
|
513
|
+
toolRegistry: t.LCToolRegistry | undefined,
|
|
514
|
+
onlyDeferred: boolean
|
|
515
|
+
): string {
|
|
516
|
+
if (!toolRegistry) {
|
|
517
|
+
return '';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const toolsByServer: Record<string, string[]> = {};
|
|
521
|
+
|
|
522
|
+
for (const lcTool of toolRegistry.values()) {
|
|
523
|
+
if (onlyDeferred && lcTool.defer_loading !== true) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const toolName = lcTool.name;
|
|
528
|
+
const serverName = extractMcpServerName(toolName) ?? 'other';
|
|
529
|
+
const baseName = getBaseToolName(toolName);
|
|
530
|
+
|
|
531
|
+
if (!(serverName in toolsByServer)) {
|
|
532
|
+
toolsByServer[serverName] = [];
|
|
533
|
+
}
|
|
534
|
+
toolsByServer[serverName].push(baseName);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const serverNames = Object.keys(toolsByServer).sort((a, b) => {
|
|
538
|
+
if (a === 'other') return 1;
|
|
539
|
+
if (b === 'other') return -1;
|
|
540
|
+
return a.localeCompare(b);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
if (serverNames.length === 0) {
|
|
544
|
+
return '';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const lines = serverNames.map(
|
|
548
|
+
(server) => `${server}: ${toolsByServer[server].join(', ')}`
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
return lines.join('\n');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Formats a server listing response as structured JSON.
|
|
510
556
|
* NOTE: This is a PREVIEW only - tools are NOT discovered/loaded.
|
|
511
557
|
* @param tools - Array of tool metadata from the server(s)
|
|
512
558
|
* @param serverNames - The MCP server name(s)
|
|
513
|
-
* @returns
|
|
559
|
+
* @returns JSON string showing all tools grouped by server
|
|
514
560
|
*/
|
|
515
561
|
function formatServerListing(
|
|
516
562
|
tools: t.ToolMetadata[],
|
|
@@ -519,48 +565,46 @@ function formatServerListing(
|
|
|
519
565
|
const servers = Array.isArray(serverNames) ? serverNames : [serverNames];
|
|
520
566
|
|
|
521
567
|
if (tools.length === 0) {
|
|
522
|
-
return
|
|
568
|
+
return JSON.stringify(
|
|
569
|
+
{
|
|
570
|
+
listing_mode: true,
|
|
571
|
+
servers,
|
|
572
|
+
total_tools: 0,
|
|
573
|
+
tools_by_server: {},
|
|
574
|
+
hint: 'No tools found from the specified MCP server(s).',
|
|
575
|
+
},
|
|
576
|
+
null,
|
|
577
|
+
2
|
|
578
|
+
);
|
|
523
579
|
}
|
|
524
580
|
|
|
525
|
-
const toolsByServer
|
|
581
|
+
const toolsByServer: Record<
|
|
582
|
+
string,
|
|
583
|
+
Array<{ name: string; description: string }>
|
|
584
|
+
> = {};
|
|
526
585
|
for (const tool of tools) {
|
|
527
586
|
const server = extractMcpServerName(tool.name) ?? 'unknown';
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
toolsByServer.set(server, existing);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
let response =
|
|
534
|
-
servers.length === 1
|
|
535
|
-
? `## Tools from MCP server: ${servers[0]}\n\n`
|
|
536
|
-
: `## Tools from MCP servers: ${servers.join(', ')}\n\n`;
|
|
537
|
-
|
|
538
|
-
response += `Found ${tools.length} tool(s) (preview only - not yet loaded):\n\n`;
|
|
539
|
-
|
|
540
|
-
for (const [server, serverTools] of toolsByServer) {
|
|
541
|
-
if (servers.length > 1) {
|
|
542
|
-
response += `### ${server}\n\n`;
|
|
543
|
-
}
|
|
544
|
-
for (const tool of serverTools) {
|
|
545
|
-
const baseName = getBaseToolName(tool.name);
|
|
546
|
-
response += `- **${baseName}**`;
|
|
547
|
-
if (tool.description) {
|
|
548
|
-
const shortDesc =
|
|
549
|
-
tool.description.length > 80
|
|
550
|
-
? tool.description.substring(0, 77) + '...'
|
|
551
|
-
: tool.description;
|
|
552
|
-
response += `: ${shortDesc}`;
|
|
553
|
-
}
|
|
554
|
-
response += '\n';
|
|
555
|
-
}
|
|
556
|
-
if (servers.length > 1) {
|
|
557
|
-
response += '\n';
|
|
587
|
+
if (!(server in toolsByServer)) {
|
|
588
|
+
toolsByServer[server] = [];
|
|
558
589
|
}
|
|
590
|
+
toolsByServer[server].push({
|
|
591
|
+
name: getBaseToolName(tool.name),
|
|
592
|
+
description:
|
|
593
|
+
tool.description.length > 100
|
|
594
|
+
? tool.description.substring(0, 97) + '...'
|
|
595
|
+
: tool.description,
|
|
596
|
+
});
|
|
559
597
|
}
|
|
560
598
|
|
|
561
|
-
|
|
599
|
+
const output = {
|
|
600
|
+
listing_mode: true,
|
|
601
|
+
servers,
|
|
602
|
+
total_tools: tools.length,
|
|
603
|
+
tools_by_server: toolsByServer,
|
|
604
|
+
hint: `To use a tool, search for it by name (e.g., query: "${getBaseToolName(tools[0]?.name ?? 'tool_name')}") to load it.`,
|
|
605
|
+
};
|
|
562
606
|
|
|
563
|
-
return
|
|
607
|
+
return JSON.stringify(output, null, 2);
|
|
564
608
|
}
|
|
565
609
|
|
|
566
610
|
/**
|
|
@@ -615,46 +659,36 @@ function createToolSearch(
|
|
|
615
659
|
const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
|
|
616
660
|
const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
|
|
617
661
|
|
|
618
|
-
const
|
|
662
|
+
const deferredToolsListing = getDeferredToolsListing(
|
|
619
663
|
initParams.toolRegistry,
|
|
620
664
|
defaultOnlyDeferred
|
|
621
665
|
);
|
|
622
666
|
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
?
|
|
626
|
-
: '';
|
|
667
|
+
const toolsListSection =
|
|
668
|
+
deferredToolsListing.length > 0
|
|
669
|
+
? `
|
|
627
670
|
|
|
628
|
-
|
|
671
|
+
Deferred tools (search to load):
|
|
672
|
+
${deferredToolsListing}`
|
|
673
|
+
: '';
|
|
629
674
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
-
|
|
675
|
+
const mcpNote =
|
|
676
|
+
deferredToolsListing.includes(Constants.MCP_DELIMITER) ||
|
|
677
|
+
deferredToolsListing.split('\n').some((line) => !line.startsWith('other:'))
|
|
678
|
+
? `
|
|
679
|
+
- MCP tools use format: toolName${Constants.MCP_DELIMITER}serverName
|
|
680
|
+
- Use mcp_server param to filter by server`
|
|
681
|
+
: '';
|
|
635
682
|
|
|
636
683
|
const description =
|
|
637
684
|
mode === 'local'
|
|
638
685
|
? `
|
|
639
|
-
Searches
|
|
640
|
-
|
|
641
|
-
Usage:
|
|
642
|
-
- Provide a search term to find in tool names and descriptions.
|
|
643
|
-
- Uses case-insensitive substring matching (fast and safe).
|
|
644
|
-
- Use this when you need to discover tools for a specific task.
|
|
645
|
-
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
646
|
-
- Higher scores (0.95+) indicate name matches, medium scores (0.70+) indicate description matches.
|
|
647
|
-
${mcpInstructions}
|
|
686
|
+
Searches deferred tools by name/description. Case-insensitive substring matching.
|
|
687
|
+
${mcpNote}${toolsListSection}
|
|
648
688
|
`.trim()
|
|
649
689
|
: `
|
|
650
|
-
Searches
|
|
651
|
-
|
|
652
|
-
Usage:
|
|
653
|
-
- Provide a regex pattern to search tool names and descriptions.
|
|
654
|
-
- Use this when you need to discover tools for a specific task.
|
|
655
|
-
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
656
|
-
- Higher scores (0.9+) indicate name matches, medium scores (0.7+) indicate description matches.
|
|
657
|
-
${mcpInstructions}
|
|
690
|
+
Searches deferred tools by regex pattern.
|
|
691
|
+
${mcpNote}${toolsListSection}
|
|
658
692
|
`.trim();
|
|
659
693
|
|
|
660
694
|
return tool<typeof schema>(
|
|
@@ -885,6 +919,7 @@ export {
|
|
|
885
919
|
isFromAnyMcpServer,
|
|
886
920
|
normalizeServerFilter,
|
|
887
921
|
getAvailableMcpServers,
|
|
922
|
+
getDeferredToolsListing,
|
|
888
923
|
getBaseToolName,
|
|
889
924
|
formatServerListing,
|
|
890
925
|
sanitizeRegex,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isFromAnyMcpServer,
|
|
17
17
|
normalizeServerFilter,
|
|
18
18
|
getAvailableMcpServers,
|
|
19
|
+
getDeferredToolsListing,
|
|
19
20
|
getBaseToolName,
|
|
20
21
|
formatServerListing,
|
|
21
22
|
} from '../ToolSearch';
|
|
@@ -650,6 +651,90 @@ describe('ToolSearch', () => {
|
|
|
650
651
|
});
|
|
651
652
|
});
|
|
652
653
|
|
|
654
|
+
describe('getDeferredToolsListing', () => {
|
|
655
|
+
const createRegistry = (): LCToolRegistry => {
|
|
656
|
+
const registry: LCToolRegistry = new Map();
|
|
657
|
+
registry.set('get_weather_mcp_weather-api', {
|
|
658
|
+
name: 'get_weather_mcp_weather-api',
|
|
659
|
+
description: 'Get weather',
|
|
660
|
+
defer_loading: true,
|
|
661
|
+
});
|
|
662
|
+
registry.set('get_forecast_mcp_weather-api', {
|
|
663
|
+
name: 'get_forecast_mcp_weather-api',
|
|
664
|
+
description: 'Get forecast',
|
|
665
|
+
defer_loading: true,
|
|
666
|
+
});
|
|
667
|
+
registry.set('send_email_mcp_gmail', {
|
|
668
|
+
name: 'send_email_mcp_gmail',
|
|
669
|
+
description: 'Send email',
|
|
670
|
+
defer_loading: true,
|
|
671
|
+
});
|
|
672
|
+
registry.set('execute_code', {
|
|
673
|
+
name: 'execute_code',
|
|
674
|
+
description: 'Execute code',
|
|
675
|
+
defer_loading: true,
|
|
676
|
+
});
|
|
677
|
+
registry.set('read_file', {
|
|
678
|
+
name: 'read_file',
|
|
679
|
+
description: 'Read file',
|
|
680
|
+
defer_loading: false,
|
|
681
|
+
});
|
|
682
|
+
return registry;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
it('groups tools by server with format D', () => {
|
|
686
|
+
const registry = createRegistry();
|
|
687
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
688
|
+
|
|
689
|
+
expect(listing).toContain('gmail: send_email');
|
|
690
|
+
expect(listing).toContain('weather-api: get_weather, get_forecast');
|
|
691
|
+
expect(listing).toContain('other: execute_code');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('sorts servers alphabetically with other last', () => {
|
|
695
|
+
const registry = createRegistry();
|
|
696
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
697
|
+
const lines = listing.split('\n');
|
|
698
|
+
|
|
699
|
+
expect(lines[0]).toMatch(/^gmail:/);
|
|
700
|
+
expect(lines[1]).toMatch(/^weather-api:/);
|
|
701
|
+
expect(lines[2]).toMatch(/^other:/);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('uses base tool names without MCP suffix', () => {
|
|
705
|
+
const registry = createRegistry();
|
|
706
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
707
|
+
|
|
708
|
+
expect(listing).toContain('get_weather');
|
|
709
|
+
expect(listing).not.toContain('get_weather_mcp_weather-api');
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('respects onlyDeferred flag', () => {
|
|
713
|
+
const registry = createRegistry();
|
|
714
|
+
|
|
715
|
+
const deferredOnly = getDeferredToolsListing(registry, true);
|
|
716
|
+
expect(deferredOnly).not.toContain('read_file');
|
|
717
|
+
|
|
718
|
+
const allTools = getDeferredToolsListing(registry, false);
|
|
719
|
+
expect(allTools).toContain('read_file');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('returns empty string for undefined registry', () => {
|
|
723
|
+
expect(getDeferredToolsListing(undefined, true)).toBe('');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('returns empty string for registry with no matching tools', () => {
|
|
727
|
+
const registry: LCToolRegistry = new Map();
|
|
728
|
+
registry.set('read_file', {
|
|
729
|
+
name: 'read_file',
|
|
730
|
+
description: 'Read file',
|
|
731
|
+
defer_loading: false,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(getDeferredToolsListing(registry, true)).toBe('');
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
653
738
|
describe('performLocalSearch with MCP tools', () => {
|
|
654
739
|
const mcpTools: ToolMetadata[] = [
|
|
655
740
|
{
|
|
@@ -732,34 +817,41 @@ describe('ToolSearch', () => {
|
|
|
732
817
|
},
|
|
733
818
|
];
|
|
734
819
|
|
|
735
|
-
it('
|
|
820
|
+
it('returns valid JSON with tool listing', () => {
|
|
736
821
|
const result = formatServerListing(serverTools, 'weather-api');
|
|
822
|
+
const parsed = JSON.parse(result);
|
|
737
823
|
|
|
738
|
-
expect(
|
|
739
|
-
expect(
|
|
740
|
-
expect(
|
|
741
|
-
expect(
|
|
742
|
-
expect(result).toContain('preview only');
|
|
824
|
+
expect(parsed.listing_mode).toBe(true);
|
|
825
|
+
expect(parsed.servers).toEqual(['weather-api']);
|
|
826
|
+
expect(parsed.total_tools).toBe(2);
|
|
827
|
+
expect(parsed.tools_by_server['weather-api']).toHaveLength(2);
|
|
743
828
|
});
|
|
744
829
|
|
|
745
830
|
it('includes hint to search for specific tool to load it', () => {
|
|
746
831
|
const result = formatServerListing(serverTools, 'weather-api');
|
|
832
|
+
const parsed = JSON.parse(result);
|
|
747
833
|
|
|
748
|
-
expect(
|
|
834
|
+
expect(parsed.hint).toContain('To use a tool, search for it by name');
|
|
749
835
|
});
|
|
750
836
|
|
|
751
837
|
it('uses base tool name (without MCP suffix) in display', () => {
|
|
752
838
|
const result = formatServerListing(serverTools, 'weather-api');
|
|
839
|
+
const parsed = JSON.parse(result);
|
|
753
840
|
|
|
754
|
-
|
|
755
|
-
|
|
841
|
+
const toolNames = parsed.tools_by_server['weather-api'].map(
|
|
842
|
+
(t: { name: string }) => t.name
|
|
843
|
+
);
|
|
844
|
+
expect(toolNames).toContain('get_weather');
|
|
845
|
+
expect(toolNames).not.toContain('get_weather_mcp_weather-api');
|
|
756
846
|
});
|
|
757
847
|
|
|
758
848
|
it('handles empty tools array', () => {
|
|
759
849
|
const result = formatServerListing([], 'empty-server');
|
|
850
|
+
const parsed = JSON.parse(result);
|
|
760
851
|
|
|
761
|
-
expect(
|
|
762
|
-
expect(
|
|
852
|
+
expect(parsed.total_tools).toBe(0);
|
|
853
|
+
expect(parsed.servers).toContain('empty-server');
|
|
854
|
+
expect(parsed.hint).toContain('No tools found');
|
|
763
855
|
});
|
|
764
856
|
|
|
765
857
|
it('truncates long descriptions', () => {
|
|
@@ -767,17 +859,17 @@ describe('ToolSearch', () => {
|
|
|
767
859
|
{
|
|
768
860
|
name: 'long_tool_mcp_server',
|
|
769
861
|
description:
|
|
770
|
-
'This is a very long description that exceeds
|
|
862
|
+
'This is a very long description that exceeds 100 characters and should be truncated to keep the listing compact and readable for the LLM.',
|
|
771
863
|
parameters: undefined,
|
|
772
864
|
},
|
|
773
865
|
];
|
|
774
866
|
|
|
775
867
|
const result = formatServerListing(toolsWithLongDesc, 'server');
|
|
868
|
+
const parsed = JSON.parse(result);
|
|
776
869
|
|
|
777
|
-
|
|
778
|
-
expect(
|
|
779
|
-
|
|
780
|
-
);
|
|
870
|
+
const toolDesc = parsed.tools_by_server['server'][0].description;
|
|
871
|
+
expect(toolDesc).toContain('...');
|
|
872
|
+
expect(toolDesc.length).toBeLessThanOrEqual(100);
|
|
781
873
|
});
|
|
782
874
|
|
|
783
875
|
it('handles multiple servers with grouped output', () => {
|
|
@@ -803,21 +895,20 @@ describe('ToolSearch', () => {
|
|
|
803
895
|
'weather-api',
|
|
804
896
|
'gmail',
|
|
805
897
|
]);
|
|
898
|
+
const parsed = JSON.parse(result);
|
|
806
899
|
|
|
807
|
-
expect(
|
|
808
|
-
expect(
|
|
809
|
-
expect(
|
|
810
|
-
expect(
|
|
811
|
-
expect(result).toContain('get_weather');
|
|
812
|
-
expect(result).toContain('send_email');
|
|
813
|
-
expect(result).toContain('read_inbox');
|
|
900
|
+
expect(parsed.servers).toEqual(['weather-api', 'gmail']);
|
|
901
|
+
expect(parsed.total_tools).toBe(3);
|
|
902
|
+
expect(parsed.tools_by_server['weather-api']).toHaveLength(1);
|
|
903
|
+
expect(parsed.tools_by_server['gmail']).toHaveLength(2);
|
|
814
904
|
});
|
|
815
905
|
|
|
816
906
|
it('accepts single server as array', () => {
|
|
817
907
|
const result = formatServerListing(serverTools, ['weather-api']);
|
|
908
|
+
const parsed = JSON.parse(result);
|
|
818
909
|
|
|
819
|
-
expect(
|
|
820
|
-
expect(
|
|
910
|
+
expect(parsed.servers).toEqual(['weather-api']);
|
|
911
|
+
expect(parsed.tools_by_server['weather-api']).toBeDefined();
|
|
821
912
|
});
|
|
822
913
|
});
|
|
823
914
|
});
|