@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.
@@ -465,30 +465,27 @@ function parseSearchResults(stdout: string): t.ToolSearchResponse {
465
465
  }
466
466
 
467
467
  /**
468
- * Formats search results into a human-readable string.
468
+ * Formats search results as structured JSON for efficient parsing.
469
469
  * @param searchResponse - The parsed search response
470
- * @returns Formatted string for LLM consumption
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
- if (tool_references.length === 0) {
477
- return `No tools matched the pattern "${pattern_used}".\nTotal tools searched: ${total_tools_searched}`;
478
- }
479
-
480
- let response = `Found ${tool_references.length} matching tools:\n\n`;
481
-
482
- for (const ref of tool_references) {
483
- response += `- ${ref.tool_name} (score: ${ref.match_score.toFixed(2)})\n`;
484
- response += ` Matched in: ${ref.matched_field}\n`;
485
- response += ` Snippet: ${ref.snippet}\n\n`;
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 response;
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
- * Formats a server listing response when listing all tools from MCP server(s).
509
- * Provides a cohesive view of all tools grouped by server.
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 Formatted string showing all tools from the server(s)
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 `No tools found from MCP server(s): ${servers.join(', ')}.`;
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 = new Map<string, t.ToolMetadata[]>();
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
- const existing = toolsByServer.get(server) ?? [];
529
- existing.push(tool);
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
- response += `\n_To use a tool, search for it by name (e.g., query: "${getBaseToolName(tools[0]?.name ?? 'tool_name')}") to load it._`;
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 response;
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 availableServers = getAvailableMcpServers(
662
+ const deferredToolsListing = getDeferredToolsListing(
619
663
  initParams.toolRegistry,
620
664
  defaultOnlyDeferred
621
665
  );
622
666
 
623
- const serverListText =
624
- availableServers.length > 0
625
- ? `\n- Available MCP servers: ${availableServers.join(', ')}`
626
- : '';
667
+ const toolsListSection =
668
+ deferredToolsListing.length > 0
669
+ ? `
627
670
 
628
- const mcpInstructions = `
671
+ Deferred tools (search to load):
672
+ ${deferredToolsListing}`
673
+ : '';
629
674
 
630
- MCP Server Tools:
631
- - Tools from MCP servers follow the naming convention: toolName${Constants.MCP_DELIMITER}serverName
632
- - Example: "get_weather${Constants.MCP_DELIMITER}weather-api" is the "get_weather" tool from the "weather-api" server
633
- - Use mcp_server parameter to filter by server (e.g., mcp_server: "weather-api")
634
- - If mcp_server is provided without a query, lists ALL tools from that server${serverListText}`;
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 through available tools to find ones matching your search term.
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 through available tools to find ones matching your query pattern.
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('formats server listing with tool names and descriptions', () => {
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(result).toContain('Tools from MCP server: weather-api');
739
- expect(result).toContain('2 tool(s)');
740
- expect(result).toContain('get_weather');
741
- expect(result).toContain('get_forecast');
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(result).toContain('To use a tool, search for it by name');
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
- expect(result).toContain('**get_weather**');
755
- expect(result).not.toContain('**get_weather_mcp_weather-api**');
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(result).toContain('No tools found');
762
- expect(result).toContain('empty-server');
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 80 characters and should be truncated to keep the listing compact and readable.',
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
- expect(result).toContain('...');
778
- expect(result.length).toBeLessThan(
779
- toolsWithLongDesc[0].description.length + 200
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(result).toContain('Tools from MCP servers: weather-api, gmail');
808
- expect(result).toContain('3 tool(s)');
809
- expect(result).toContain('### weather-api');
810
- expect(result).toContain('### gmail');
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(result).toContain('Tools from MCP server: weather-api');
820
- expect(result).not.toContain('###');
910
+ expect(parsed.servers).toEqual(['weather-api']);
911
+ expect(parsed.tools_by_server['weather-api']).toBeDefined();
821
912
  });
822
913
  });
823
914
  });