@librechat/agents 3.1.30 → 3.1.32

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.
@@ -38,6 +38,17 @@ export const ToolSearchToolName = Constants.TOOL_SEARCH;
38
38
  export const ToolSearchToolDescription =
39
39
  'Searches deferred tools using BM25 ranking. Multi-word queries supported. Use mcp_server param to filter by server.';
40
40
 
41
+ const QUERY_DESCRIPTION_LOCAL =
42
+ 'Search term to find in tool names and descriptions. Case-insensitive substring matching. Optional if mcp_server is provided.';
43
+ const QUERY_DESCRIPTION_REGEX =
44
+ 'Regex pattern to search tool names and descriptions. Optional if mcp_server is provided.';
45
+ const FIELDS_DESCRIPTION =
46
+ 'Which fields to search. Default: name and description';
47
+ const MAX_RESULTS_DESCRIPTION = 'Maximum number of matching tools to return';
48
+ const DEFAULT_MAX_RESULTS = 5;
49
+ const MCP_SERVER_DESCRIPTION =
50
+ 'Filter to tools from specific MCP server(s). Can be a single server name or array of names. If provided without a query, lists all tools from those servers.';
51
+
41
52
  export const ToolSearchToolSchema = {
42
53
  type: 'object',
43
54
  properties: {
@@ -45,26 +56,24 @@ export const ToolSearchToolSchema = {
45
56
  type: 'string',
46
57
  maxLength: MAX_PATTERN_LENGTH,
47
58
  default: '',
48
- description:
49
- 'Search term to find in tool names and descriptions. Case-insensitive substring matching. Optional if mcp_server is provided.',
59
+ description: QUERY_DESCRIPTION_LOCAL,
50
60
  },
51
61
  fields: {
52
62
  type: 'array',
53
63
  items: { type: 'string', enum: ['name', 'description', 'parameters'] },
54
64
  default: ['name', 'description'],
55
- description: 'Which fields to search. Default: name and description',
65
+ description: FIELDS_DESCRIPTION,
56
66
  },
57
67
  max_results: {
58
68
  type: 'integer',
59
69
  minimum: 1,
60
70
  maximum: 50,
61
- default: 10,
62
- description: 'Maximum number of matching tools to return',
71
+ default: DEFAULT_MAX_RESULTS,
72
+ description: MAX_RESULTS_DESCRIPTION,
63
73
  },
64
74
  mcp_server: {
65
75
  oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
66
- description:
67
- 'Filter to tools from specific MCP server(s). Can be a single server name or array of names. If provided without a query, lists all tools from those servers.',
76
+ description: MCP_SERVER_DESCRIPTION,
68
77
  },
69
78
  },
70
79
  required: [],
@@ -104,9 +113,7 @@ interface ToolSearchParams {
104
113
  */
105
114
  function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
106
115
  const queryDescription =
107
- mode === 'local'
108
- ? 'Search term to find in tool names and descriptions. Case-insensitive substring matching. Optional if mcp_server is provided.'
109
- : 'Regex pattern to search tool names and descriptions. Optional if mcp_server is provided.';
116
+ mode === 'local' ? QUERY_DESCRIPTION_LOCAL : QUERY_DESCRIPTION_REGEX;
110
117
 
111
118
  return {
112
119
  type: 'object',
@@ -121,22 +128,21 @@ function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
121
128
  type: 'array',
122
129
  items: { type: 'string', enum: ['name', 'description', 'parameters'] },
123
130
  default: ['name', 'description'],
124
- description: 'Which fields to search. Default: name and description',
131
+ description: FIELDS_DESCRIPTION,
125
132
  },
126
133
  max_results: {
127
134
  type: 'integer',
128
135
  minimum: 1,
129
136
  maximum: 50,
130
- default: 10,
131
- description: 'Maximum number of matching tools to return',
137
+ default: DEFAULT_MAX_RESULTS,
138
+ description: MAX_RESULTS_DESCRIPTION,
132
139
  },
133
140
  mcp_server: {
134
141
  oneOf: [
135
142
  { type: 'string' },
136
143
  { type: 'array', items: { type: 'string' } },
137
144
  ],
138
- description:
139
- 'Filter to tools from specific MCP server(s). Can be a single server name or array of names. If provided without a query, lists all tools from those servers.',
145
+ description: MCP_SERVER_DESCRIPTION,
140
146
  },
141
147
  },
142
148
  required: [],
@@ -446,8 +452,9 @@ function findMatchedField(
446
452
  /**
447
453
  * Performs BM25-based search for better relevance ranking.
448
454
  * Uses Okapi BM25 algorithm for term frequency and document length normalization.
455
+ * If query is empty, returns all tools (up to maxResults) sorted alphabetically.
449
456
  * @param tools - Array of tool metadata to search
450
- * @param query - The search query
457
+ * @param query - The search query (empty returns all tools)
451
458
  * @param fields - Which fields to search
452
459
  * @param maxResults - Maximum results to return
453
460
  * @returns Search response with matching tools ranked by BM25 score
@@ -458,25 +465,36 @@ function performLocalSearch(
458
465
  fields: string[],
459
466
  maxResults: number
460
467
  ): t.ToolSearchResponse {
461
- if (tools.length === 0 || !query.trim()) {
468
+ if (tools.length === 0) {
462
469
  return {
463
470
  tool_references: [],
464
- total_tools_searched: tools.length,
471
+ total_tools_searched: 0,
465
472
  pattern_used: query,
466
473
  };
467
474
  }
468
475
 
469
- const documents = tools.map((tool) => createToolDocument(tool, fields));
470
476
  const queryTokens = tokenize(query);
471
477
 
472
478
  if (queryTokens.length === 0) {
479
+ const allTools = tools
480
+ .slice()
481
+ .sort((a, b) => a.name.localeCompare(b.name))
482
+ .slice(0, maxResults)
483
+ .map((tool) => ({
484
+ tool_name: tool.name,
485
+ match_score: 1.0,
486
+ matched_field: 'name',
487
+ snippet: tool.description.substring(0, 100) || tool.name,
488
+ }));
489
+
473
490
  return {
474
- tool_references: [],
491
+ tool_references: allTools,
475
492
  total_tools_searched: tools.length,
476
493
  pattern_used: query,
477
494
  };
478
495
  }
479
496
 
497
+ const documents = tools.map((tool) => createToolDocument(tool, fields));
480
498
  const scores = BM25(documents, queryTokens, { k1: 1.5, b: 0.75 }) as number[];
481
499
 
482
500
  const maxScore = Math.max(...scores.filter((s) => s > 0), 1);
@@ -492,7 +510,6 @@ function performLocalSearch(
492
510
  );
493
511
  let normalizedScore = Math.min(scores[i] / maxScore, 1.0);
494
512
 
495
- // Boost score for exact base name match
496
513
  const baseName = getBaseToolName(tools[i].name).toLowerCase();
497
514
  if (baseName === queryLower) {
498
515
  normalizedScore = 1.0;
@@ -631,16 +648,21 @@ function parseSearchResults(stdout: string): t.ToolSearchResponse {
631
648
  /**
632
649
  * Formats search results as structured JSON for efficient parsing.
633
650
  * @param searchResponse - The parsed search response
651
+ * @param nameFormat - Whether to show 'full' names (tool_mcp_server) or 'base' names (tool only)
634
652
  * @returns JSON string with search results
635
653
  */
636
- function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
654
+ function formatSearchResults(
655
+ searchResponse: t.ToolSearchResponse,
656
+ nameFormat: t.McpNameFormat = 'full'
657
+ ): string {
637
658
  const { tool_references, total_tools_searched, pattern_used } =
638
659
  searchResponse;
660
+ const useFullName = nameFormat === 'full';
639
661
 
640
662
  const output = {
641
663
  found: tool_references.length,
642
664
  tools: tool_references.map((ref) => ({
643
- name: ref.tool_name,
665
+ name: useFullName ? ref.tool_name : getBaseToolName(ref.tool_name),
644
666
  score: Number(ref.match_score.toFixed(2)),
645
667
  matched_in: ref.matched_field,
646
668
  snippet: ref.snippet,
@@ -720,13 +742,16 @@ function getDeferredToolsListing(
720
742
  * NOTE: This is a PREVIEW only - tools are NOT discovered/loaded.
721
743
  * @param tools - Array of tool metadata from the server(s)
722
744
  * @param serverNames - The MCP server name(s)
745
+ * @param nameFormat - Whether to show 'full' names (tool_mcp_server) or 'base' names (tool only)
723
746
  * @returns JSON string showing all tools grouped by server
724
747
  */
725
748
  function formatServerListing(
726
749
  tools: t.ToolMetadata[],
727
- serverNames: string | string[]
750
+ serverNames: string | string[],
751
+ nameFormat: t.McpNameFormat = 'full'
728
752
  ): string {
729
753
  const servers = Array.isArray(serverNames) ? serverNames : [serverNames];
754
+ const useFullName = nameFormat === 'full';
730
755
 
731
756
  if (tools.length === 0) {
732
757
  return JSON.stringify(
@@ -752,7 +777,7 @@ function formatServerListing(
752
777
  toolsByServer[server] = [];
753
778
  }
754
779
  toolsByServer[server].push({
755
- name: getBaseToolName(tool.name),
780
+ name: useFullName ? tool.name : getBaseToolName(tool.name),
756
781
  description:
757
782
  tool.description.length > 100
758
783
  ? tool.description.substring(0, 97) + '...'
@@ -760,12 +785,16 @@ function formatServerListing(
760
785
  });
761
786
  }
762
787
 
788
+ const exampleToolName = useFullName
789
+ ? (tools[0]?.name ?? 'tool_name')
790
+ : getBaseToolName(tools[0]?.name ?? 'tool_name');
791
+
763
792
  const output = {
764
793
  listing_mode: true,
765
794
  servers,
766
795
  total_tools: tools.length,
767
796
  tools_by_server: toolsByServer,
768
- hint: `To use a tool, search for it by name (e.g., query: "${getBaseToolName(tools[0]?.name ?? 'tool_name')}") to load it.`,
797
+ hint: `To use a tool, search for it by name (e.g., query: "${exampleToolName}") to load it.`,
769
798
  };
770
799
 
771
800
  return JSON.stringify(output, null, 2);
@@ -804,6 +833,7 @@ function createToolSearch(
804
833
  ): DynamicStructuredTool {
805
834
  const mode: t.ToolSearchMode = initParams.mode ?? 'code_interpreter';
806
835
  const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
836
+ const mcpNameFormat: t.McpNameFormat = initParams.mcpNameFormat ?? 'full';
807
837
  const schema = createToolSearchSchema(mode);
808
838
 
809
839
  const apiKey: string =
@@ -861,7 +891,7 @@ ${mcpNote}${toolsListSection}
861
891
  const {
862
892
  query = '',
863
893
  fields = ['name', 'description'],
864
- max_results = 10,
894
+ max_results = DEFAULT_MAX_RESULTS,
865
895
  mcp_server,
866
896
  } = params;
867
897
 
@@ -937,7 +967,8 @@ ${mcpNote}${toolsListSection}
937
967
  if (isServerListing) {
938
968
  const formattedOutput = formatServerListing(
939
969
  deferredTools,
940
- serverFilters
970
+ serverFilters,
971
+ mcpNameFormat
941
972
  );
942
973
 
943
974
  return [
@@ -960,7 +991,10 @@ ${mcpNote}${toolsListSection}
960
991
  fields,
961
992
  max_results
962
993
  );
963
- const formattedOutput = formatSearchResults(searchResponse);
994
+ const formattedOutput = formatSearchResults(
995
+ searchResponse,
996
+ mcpNameFormat
997
+ );
964
998
 
965
999
  return [
966
1000
  formattedOutput,
@@ -1036,7 +1070,7 @@ ${mcpNote}${toolsListSection}
1036
1070
  }
1037
1071
 
1038
1072
  const searchResponse = parseSearchResults(result.stdout);
1039
- const formattedOutput = `${warningMessage}${formatSearchResults(searchResponse)}`;
1073
+ const formattedOutput = `${warningMessage}${formatSearchResults(searchResponse, mcpNameFormat)}`;
1040
1074
 
1041
1075
  return [
1042
1076
  formattedOutput,
@@ -426,8 +426,10 @@ describe('ToolSearch', () => {
426
426
  it('handles empty query gracefully', () => {
427
427
  const result = performLocalSearch(mockTools, '', ['name'], 10);
428
428
 
429
- // BM25 correctly returns no results for empty queries (no terms to match)
430
- expect(result.tool_references.length).toBe(0);
429
+ // Empty queries return all tools (up to maxResults) sorted alphabetically
430
+ expect(result.tool_references.length).toBe(
431
+ Math.min(mockTools.length, 10)
432
+ );
431
433
  expect(result.total_tools_searched).toBe(mockTools.length);
432
434
  });
433
435
 
@@ -923,10 +925,21 @@ describe('ToolSearch', () => {
923
925
  expect(parsed.hint).toContain('To use a tool, search for it by name');
924
926
  });
925
927
 
926
- it('uses base tool name (without MCP suffix) in display', () => {
928
+ it('uses full tool name by default', () => {
927
929
  const result = formatServerListing(serverTools, 'weather-api');
928
930
  const parsed = JSON.parse(result);
929
931
 
932
+ const toolNames = parsed.tools_by_server['weather-api'].map(
933
+ (t: { name: string }) => t.name
934
+ );
935
+ expect(toolNames).toContain('get_weather_mcp_weather-api');
936
+ expect(toolNames).not.toContain('get_weather');
937
+ });
938
+
939
+ it('uses base tool name when mcpNameFormat is "base"', () => {
940
+ const result = formatServerListing(serverTools, 'weather-api', 'base');
941
+ const parsed = JSON.parse(result);
942
+
930
943
  const toolNames = parsed.tools_by_server['weather-api'].map(
931
944
  (t: { name: string }) => t.name
932
945
  );
@@ -193,6 +193,9 @@ export type ProgrammaticCache = { toolMap: ToolMap; toolDefs: LCTool[] };
193
193
  /** Search mode: code_interpreter uses external sandbox, local uses safe substring matching */
194
194
  export type ToolSearchMode = 'code_interpreter' | 'local';
195
195
 
196
+ /** Format for MCP tool names in search results */
197
+ export type McpNameFormat = 'full' | 'base';
198
+
196
199
  /** Parameters for creating a Tool Search tool */
197
200
  export type ToolSearchParams = {
198
201
  apiKey?: string;
@@ -203,6 +206,8 @@ export type ToolSearchParams = {
203
206
  mode?: ToolSearchMode;
204
207
  /** Filter tools to only those from specific MCP server(s). Can be a single name or array of names. */
205
208
  mcpServer?: string | string[];
209
+ /** Format for MCP tool names: 'full' (tool_mcp_server) or 'base' (tool only). Default: 'full' */
210
+ mcpNameFormat?: McpNameFormat;
206
211
  [key: string]: unknown;
207
212
  };
208
213