@librechat/agents 3.0.66 → 3.0.68
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/common/enum.cjs +3 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/main.cjs +14 -7
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +2 -2
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/{ToolSearchRegex.cjs → ToolSearch.cjs} +318 -63
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +3 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/tools.mjs +2 -2
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/{ToolSearchRegex.mjs → ToolSearch.mjs} +311 -63
- package/dist/esm/tools/ToolSearch.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +4 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/tools/ToolSearch.d.ts +133 -0
- package/dist/types/types/tools.d.ts +8 -2
- package/package.json +2 -2
- package/src/common/enum.ts +3 -1
- package/src/index.ts +1 -1
- package/src/messages/__tests__/tools.test.ts +21 -21
- package/src/messages/tools.ts +2 -2
- package/src/scripts/programmatic_exec_agent.ts +4 -4
- package/src/scripts/{tool_search_regex.ts → tool_search.ts} +5 -5
- package/src/tools/ToolNode.ts +1 -1
- package/src/tools/{ToolSearchRegex.ts → ToolSearch.ts} +390 -69
- package/src/tools/__tests__/{ToolSearchRegex.integration.test.ts → ToolSearch.integration.test.ts} +6 -6
- package/src/tools/__tests__/ToolSearch.test.ts +734 -0
- package/src/types/tools.ts +9 -2
- package/dist/cjs/tools/ToolSearchRegex.cjs.map +0 -1
- package/dist/esm/tools/ToolSearchRegex.mjs.map +0 -1
- package/dist/types/tools/ToolSearchRegex.d.ts +0 -80
- package/src/tools/__tests__/ToolSearchRegex.test.ts +0 -232
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/tools/
|
|
1
|
+
// src/tools/ToolSearch.ts
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { config } from 'dotenv';
|
|
4
4
|
import fetch, { RequestInit } from 'node-fetch';
|
|
@@ -20,28 +20,111 @@ const MAX_REGEX_COMPLEXITY = 5;
|
|
|
20
20
|
/** Default search timeout in milliseconds */
|
|
21
21
|
const SEARCH_TIMEOUT = 5000;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
/** Zod schema type for tool search parameters */
|
|
24
|
+
type ToolSearchSchema = z.ZodObject<{
|
|
25
|
+
query: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
26
|
+
fields: z.ZodDefault<
|
|
27
|
+
z.ZodOptional<z.ZodArray<z.ZodEnum<['name', 'description', 'parameters']>>>
|
|
28
|
+
>;
|
|
29
|
+
max_results: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
30
|
+
mcp_server: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates the Zod schema with dynamic query description based on mode.
|
|
35
|
+
* @param mode - The search mode determining query interpretation
|
|
36
|
+
* @returns Zod schema for tool search parameters
|
|
37
|
+
*/
|
|
38
|
+
function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
|
|
39
|
+
const queryDescription =
|
|
40
|
+
mode === 'local'
|
|
41
|
+
? 'Search term to find in tool names and descriptions. Case-insensitive substring matching. Optional if mcp_server is provided.'
|
|
42
|
+
: 'Regex pattern to search tool names and descriptions. Optional if mcp_server is provided.';
|
|
43
|
+
|
|
44
|
+
return z.object({
|
|
45
|
+
query: z
|
|
46
|
+
.string()
|
|
47
|
+
.max(MAX_PATTERN_LENGTH)
|
|
48
|
+
.optional()
|
|
49
|
+
.default('')
|
|
50
|
+
.describe(queryDescription),
|
|
51
|
+
fields: z
|
|
52
|
+
.array(z.enum(['name', 'description', 'parameters']))
|
|
53
|
+
.optional()
|
|
54
|
+
.default(['name', 'description'])
|
|
55
|
+
.describe('Which fields to search. Default: name and description'),
|
|
56
|
+
max_results: z
|
|
57
|
+
.number()
|
|
58
|
+
.int()
|
|
59
|
+
.min(1)
|
|
60
|
+
.max(50)
|
|
61
|
+
.optional()
|
|
62
|
+
.default(10)
|
|
63
|
+
.describe('Maximum number of matching tools to return'),
|
|
64
|
+
mcp_server: z
|
|
65
|
+
.union([z.string(), z.array(z.string())])
|
|
66
|
+
.optional()
|
|
67
|
+
.describe(
|
|
68
|
+
'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.'
|
|
69
|
+
),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extracts the MCP server name from a tool name.
|
|
75
|
+
* MCP tools follow the pattern: toolName_mcp_serverName
|
|
76
|
+
* @param toolName - The full tool name
|
|
77
|
+
* @returns The server name if it's an MCP tool, undefined otherwise
|
|
78
|
+
*/
|
|
79
|
+
function extractMcpServerName(toolName: string): string | undefined {
|
|
80
|
+
const delimiterIndex = toolName.indexOf(Constants.MCP_DELIMITER);
|
|
81
|
+
if (delimiterIndex === -1) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return toolName.substring(delimiterIndex + Constants.MCP_DELIMITER.length);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks if a tool belongs to a specific MCP server.
|
|
89
|
+
* @param toolName - The full tool name
|
|
90
|
+
* @param serverName - The server name to match
|
|
91
|
+
* @returns True if the tool belongs to the specified server
|
|
92
|
+
*/
|
|
93
|
+
function isFromMcpServer(toolName: string, serverName: string): boolean {
|
|
94
|
+
const toolServer = extractMcpServerName(toolName);
|
|
95
|
+
return toolServer === serverName;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a tool belongs to any of the specified MCP servers.
|
|
100
|
+
* @param toolName - The full tool name
|
|
101
|
+
* @param serverNames - Array of server names to match
|
|
102
|
+
* @returns True if the tool belongs to any of the specified servers
|
|
103
|
+
*/
|
|
104
|
+
function isFromAnyMcpServer(toolName: string, serverNames: string[]): boolean {
|
|
105
|
+
const toolServer = extractMcpServerName(toolName);
|
|
106
|
+
if (toolServer === undefined) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return serverNames.includes(toolServer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalizes server filter input to always be an array.
|
|
114
|
+
* @param serverFilter - String, array of strings, or undefined
|
|
115
|
+
* @returns Array of server names (empty if none specified)
|
|
116
|
+
*/
|
|
117
|
+
function normalizeServerFilter(
|
|
118
|
+
serverFilter: string | string[] | undefined
|
|
119
|
+
): string[] {
|
|
120
|
+
if (serverFilter === undefined) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
if (typeof serverFilter === 'string') {
|
|
124
|
+
return serverFilter === '' ? [] : [serverFilter];
|
|
125
|
+
}
|
|
126
|
+
return serverFilter.filter((s) => s !== '');
|
|
127
|
+
}
|
|
45
128
|
|
|
46
129
|
/**
|
|
47
130
|
* Escapes special regex characters in a string to use as a literal pattern.
|
|
@@ -170,6 +253,80 @@ function simplifyParametersForSearch(
|
|
|
170
253
|
return { type: parameters.type };
|
|
171
254
|
}
|
|
172
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Performs safe local substring search without regex.
|
|
258
|
+
* Uses case-insensitive String.includes() for complete safety against ReDoS.
|
|
259
|
+
* @param tools - Array of tool metadata to search
|
|
260
|
+
* @param query - The search term (treated as literal substring)
|
|
261
|
+
* @param fields - Which fields to search
|
|
262
|
+
* @param maxResults - Maximum results to return
|
|
263
|
+
* @returns Search response with matching tools
|
|
264
|
+
*/
|
|
265
|
+
function performLocalSearch(
|
|
266
|
+
tools: t.ToolMetadata[],
|
|
267
|
+
query: string,
|
|
268
|
+
fields: string[],
|
|
269
|
+
maxResults: number
|
|
270
|
+
): t.ToolSearchResponse {
|
|
271
|
+
const lowerQuery = query.toLowerCase();
|
|
272
|
+
const results: t.ToolSearchResult[] = [];
|
|
273
|
+
|
|
274
|
+
for (const tool of tools) {
|
|
275
|
+
let bestScore = 0;
|
|
276
|
+
let matchedField = '';
|
|
277
|
+
let snippet = '';
|
|
278
|
+
|
|
279
|
+
if (fields.includes('name')) {
|
|
280
|
+
const lowerName = tool.name.toLowerCase();
|
|
281
|
+
if (lowerName.includes(lowerQuery)) {
|
|
282
|
+
const isExactMatch = lowerName === lowerQuery;
|
|
283
|
+
const startsWithQuery = lowerName.startsWith(lowerQuery);
|
|
284
|
+
bestScore = isExactMatch ? 1.0 : startsWithQuery ? 0.95 : 0.85;
|
|
285
|
+
matchedField = 'name';
|
|
286
|
+
snippet = tool.name;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (fields.includes('description') && tool.description) {
|
|
291
|
+
const lowerDesc = tool.description.toLowerCase();
|
|
292
|
+
if (lowerDesc.includes(lowerQuery) && bestScore === 0) {
|
|
293
|
+
bestScore = 0.7;
|
|
294
|
+
matchedField = 'description';
|
|
295
|
+
snippet = tool.description.substring(0, 100);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (fields.includes('parameters') && tool.parameters?.properties) {
|
|
300
|
+
const paramNames = Object.keys(tool.parameters.properties)
|
|
301
|
+
.join(' ')
|
|
302
|
+
.toLowerCase();
|
|
303
|
+
if (paramNames.includes(lowerQuery) && bestScore === 0) {
|
|
304
|
+
bestScore = 0.55;
|
|
305
|
+
matchedField = 'parameters';
|
|
306
|
+
snippet = Object.keys(tool.parameters.properties).join(' ');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (bestScore > 0) {
|
|
311
|
+
results.push({
|
|
312
|
+
tool_name: tool.name,
|
|
313
|
+
match_score: bestScore,
|
|
314
|
+
matched_field: matchedField,
|
|
315
|
+
snippet,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
results.sort((a, b) => b.match_score - a.match_score);
|
|
321
|
+
const topResults = results.slice(0, maxResults);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
tool_references: topResults,
|
|
325
|
+
total_tools_searched: tools.length,
|
|
326
|
+
pattern_used: query,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
173
330
|
/**
|
|
174
331
|
* Generates the JavaScript search script to be executed in the sandbox.
|
|
175
332
|
* Uses plain JavaScript for maximum compatibility with the Code API.
|
|
@@ -307,11 +464,87 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
|
|
|
307
464
|
}
|
|
308
465
|
|
|
309
466
|
/**
|
|
310
|
-
*
|
|
467
|
+
* Extracts the base tool name (without MCP server suffix) from a full tool name.
|
|
468
|
+
* @param toolName - The full tool name
|
|
469
|
+
* @returns The base tool name without server suffix
|
|
470
|
+
*/
|
|
471
|
+
function getBaseToolName(toolName: string): string {
|
|
472
|
+
const delimiterIndex = toolName.indexOf(Constants.MCP_DELIMITER);
|
|
473
|
+
if (delimiterIndex === -1) {
|
|
474
|
+
return toolName;
|
|
475
|
+
}
|
|
476
|
+
return toolName.substring(0, delimiterIndex);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Formats a server listing response when listing all tools from MCP server(s).
|
|
481
|
+
* Provides a cohesive view of all tools grouped by server.
|
|
482
|
+
* NOTE: This is a PREVIEW only - tools are NOT discovered/loaded.
|
|
483
|
+
* @param tools - Array of tool metadata from the server(s)
|
|
484
|
+
* @param serverNames - The MCP server name(s)
|
|
485
|
+
* @returns Formatted string showing all tools from the server(s)
|
|
486
|
+
*/
|
|
487
|
+
function formatServerListing(
|
|
488
|
+
tools: t.ToolMetadata[],
|
|
489
|
+
serverNames: string | string[]
|
|
490
|
+
): string {
|
|
491
|
+
const servers = Array.isArray(serverNames) ? serverNames : [serverNames];
|
|
492
|
+
|
|
493
|
+
if (tools.length === 0) {
|
|
494
|
+
return `No tools found from MCP server(s): ${servers.join(', ')}.`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const toolsByServer = new Map<string, t.ToolMetadata[]>();
|
|
498
|
+
for (const tool of tools) {
|
|
499
|
+
const server = extractMcpServerName(tool.name) ?? 'unknown';
|
|
500
|
+
const existing = toolsByServer.get(server) ?? [];
|
|
501
|
+
existing.push(tool);
|
|
502
|
+
toolsByServer.set(server, existing);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let response =
|
|
506
|
+
servers.length === 1
|
|
507
|
+
? `## Tools from MCP server: ${servers[0]}\n\n`
|
|
508
|
+
: `## Tools from MCP servers: ${servers.join(', ')}\n\n`;
|
|
509
|
+
|
|
510
|
+
response += `Found ${tools.length} tool(s) (preview only - not yet loaded):\n\n`;
|
|
511
|
+
|
|
512
|
+
for (const [server, serverTools] of toolsByServer) {
|
|
513
|
+
if (servers.length > 1) {
|
|
514
|
+
response += `### ${server}\n\n`;
|
|
515
|
+
}
|
|
516
|
+
for (const tool of serverTools) {
|
|
517
|
+
const baseName = getBaseToolName(tool.name);
|
|
518
|
+
response += `- **${baseName}**`;
|
|
519
|
+
if (tool.description) {
|
|
520
|
+
const shortDesc =
|
|
521
|
+
tool.description.length > 80
|
|
522
|
+
? tool.description.substring(0, 77) + '...'
|
|
523
|
+
: tool.description;
|
|
524
|
+
response += `: ${shortDesc}`;
|
|
525
|
+
}
|
|
526
|
+
response += '\n';
|
|
527
|
+
}
|
|
528
|
+
if (servers.length > 1) {
|
|
529
|
+
response += '\n';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
response += `\n_To use a tool, search for it by name (e.g., query: "${getBaseToolName(tools[0]?.name ?? 'tool_name')}") to load it._`;
|
|
534
|
+
|
|
535
|
+
return response;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Creates a Tool Search tool for discovering tools from a large registry.
|
|
311
540
|
*
|
|
312
541
|
* This tool enables AI agents to dynamically discover tools from a large library
|
|
313
542
|
* without loading all tool definitions into the LLM context window. The agent
|
|
314
|
-
* can search for relevant tools on-demand
|
|
543
|
+
* can search for relevant tools on-demand.
|
|
544
|
+
*
|
|
545
|
+
* **Modes:**
|
|
546
|
+
* - `code_interpreter` (default): Uses external sandbox for regex search. Safer for complex patterns.
|
|
547
|
+
* - `local`: Uses safe substring matching locally. No network call, faster, completely safe from ReDoS.
|
|
315
548
|
*
|
|
316
549
|
* The tool registry can be provided either:
|
|
317
550
|
* 1. At initialization time via params.toolRegistry
|
|
@@ -321,36 +554,61 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
|
|
|
321
554
|
* @returns A LangChain DynamicStructuredTool for tool searching
|
|
322
555
|
*
|
|
323
556
|
* @example
|
|
324
|
-
* // Option 1:
|
|
325
|
-
* const tool =
|
|
326
|
-
* await tool.invoke({ query: 'expense' });
|
|
557
|
+
* // Option 1: Code interpreter mode (regex via sandbox)
|
|
558
|
+
* const tool = createToolSearch({ apiKey, toolRegistry });
|
|
559
|
+
* await tool.invoke({ query: 'expense.*report' });
|
|
327
560
|
*
|
|
328
561
|
* @example
|
|
329
|
-
* // Option 2:
|
|
330
|
-
* const tool =
|
|
331
|
-
* await tool.invoke(
|
|
332
|
-
* { query: 'expense' },
|
|
333
|
-
* { configurable: { toolRegistry, onlyDeferred: true } }
|
|
334
|
-
* );
|
|
562
|
+
* // Option 2: Local mode (safe substring search, no API key needed)
|
|
563
|
+
* const tool = createToolSearch({ mode: 'local', toolRegistry });
|
|
564
|
+
* await tool.invoke({ query: 'expense' });
|
|
335
565
|
*/
|
|
336
|
-
function
|
|
337
|
-
initParams: t.
|
|
338
|
-
): DynamicStructuredTool<typeof
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
|
|
343
|
-
'';
|
|
566
|
+
function createToolSearch(
|
|
567
|
+
initParams: t.ToolSearchParams = {}
|
|
568
|
+
): DynamicStructuredTool<ReturnType<typeof createToolSearchSchema>> {
|
|
569
|
+
const mode: t.ToolSearchMode = initParams.mode ?? 'code_interpreter';
|
|
570
|
+
const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
|
|
571
|
+
const schema = createToolSearchSchema(mode);
|
|
344
572
|
|
|
345
|
-
|
|
346
|
-
|
|
573
|
+
const apiKey: string =
|
|
574
|
+
mode === 'code_interpreter'
|
|
575
|
+
? ((initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
|
|
576
|
+
initParams.apiKey ??
|
|
577
|
+
getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
|
|
578
|
+
'')
|
|
579
|
+
: '';
|
|
580
|
+
|
|
581
|
+
if (mode === 'code_interpreter' && !apiKey) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
'No API key provided for tool search in code_interpreter mode. Use mode: "local" to search without an API key.'
|
|
584
|
+
);
|
|
347
585
|
}
|
|
348
586
|
|
|
349
587
|
const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
|
|
350
588
|
const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
|
|
351
|
-
const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
|
|
352
589
|
|
|
353
|
-
const
|
|
590
|
+
const mcpInstructions = `
|
|
591
|
+
|
|
592
|
+
MCP Server Tools:
|
|
593
|
+
- Tools from MCP servers follow the naming convention: toolName${Constants.MCP_DELIMITER}serverName
|
|
594
|
+
- Example: "get_weather${Constants.MCP_DELIMITER}weather-api" is the "get_weather" tool from the "weather-api" server
|
|
595
|
+
- Use mcp_server parameter to filter by server (e.g., mcp_server: "weather-api")
|
|
596
|
+
- If mcp_server is provided without a query, lists ALL tools from that server`;
|
|
597
|
+
|
|
598
|
+
const description =
|
|
599
|
+
mode === 'local'
|
|
600
|
+
? `
|
|
601
|
+
Searches through available tools to find ones matching your search term.
|
|
602
|
+
|
|
603
|
+
Usage:
|
|
604
|
+
- Provide a search term to find in tool names and descriptions.
|
|
605
|
+
- Uses case-insensitive substring matching (fast and safe).
|
|
606
|
+
- Use this when you need to discover tools for a specific task.
|
|
607
|
+
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
608
|
+
- Higher scores (0.95+) indicate name matches, medium scores (0.70+) indicate description matches.
|
|
609
|
+
${mcpInstructions}
|
|
610
|
+
`.trim()
|
|
611
|
+
: `
|
|
354
612
|
Searches through available tools to find ones matching your query pattern.
|
|
355
613
|
|
|
356
614
|
Usage:
|
|
@@ -358,45 +616,42 @@ Usage:
|
|
|
358
616
|
- Use this when you need to discover tools for a specific task.
|
|
359
617
|
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
360
618
|
- Higher scores (0.9+) indicate name matches, medium scores (0.7+) indicate description matches.
|
|
619
|
+
${mcpInstructions}
|
|
361
620
|
`.trim();
|
|
362
621
|
|
|
363
|
-
return tool<typeof
|
|
622
|
+
return tool<typeof schema>(
|
|
364
623
|
async (params, config) => {
|
|
365
624
|
const {
|
|
366
625
|
query,
|
|
367
626
|
fields = ['name', 'description'],
|
|
368
627
|
max_results = 10,
|
|
628
|
+
mcp_server,
|
|
369
629
|
} = params;
|
|
370
630
|
|
|
371
|
-
// Extra params injected by ToolNode (follows web_search pattern)
|
|
372
631
|
const {
|
|
373
632
|
toolRegistry: paramToolRegistry,
|
|
374
633
|
onlyDeferred: paramOnlyDeferred,
|
|
634
|
+
mcpServer: paramMcpServer,
|
|
375
635
|
} = config.toolCall ?? {};
|
|
376
636
|
|
|
377
|
-
const { safe: sanitizedPattern, wasEscaped } = sanitizeRegex(query);
|
|
378
|
-
|
|
379
|
-
let warningMessage = '';
|
|
380
|
-
if (wasEscaped) {
|
|
381
|
-
warningMessage =
|
|
382
|
-
'Note: The provided pattern was converted to a literal search for safety.\n\n';
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Priority: ToolNode injection (via config.toolCall) > initialization params
|
|
386
637
|
const toolRegistry = paramToolRegistry ?? initParams.toolRegistry;
|
|
387
638
|
const onlyDeferred =
|
|
388
639
|
paramOnlyDeferred !== undefined
|
|
389
640
|
? paramOnlyDeferred
|
|
390
641
|
: defaultOnlyDeferred;
|
|
642
|
+
const rawServerFilter =
|
|
643
|
+
mcp_server ?? paramMcpServer ?? initParams.mcpServer;
|
|
644
|
+
const serverFilters = normalizeServerFilter(rawServerFilter);
|
|
645
|
+
const hasServerFilter = serverFilters.length > 0;
|
|
391
646
|
|
|
392
647
|
if (toolRegistry == null) {
|
|
393
648
|
return [
|
|
394
|
-
|
|
649
|
+
'Error: No tool registry provided. Configure toolRegistry at agent level or initialization.',
|
|
395
650
|
{
|
|
396
651
|
tool_references: [],
|
|
397
652
|
metadata: {
|
|
398
653
|
total_searched: 0,
|
|
399
|
-
pattern:
|
|
654
|
+
pattern: query,
|
|
400
655
|
error: 'No tool registry provided',
|
|
401
656
|
},
|
|
402
657
|
},
|
|
@@ -404,11 +659,16 @@ Usage:
|
|
|
404
659
|
}
|
|
405
660
|
|
|
406
661
|
const toolsArray: t.LCTool[] = Array.from(toolRegistry.values());
|
|
407
|
-
|
|
408
662
|
const deferredTools: t.ToolMetadata[] = toolsArray
|
|
409
663
|
.filter((lcTool) => {
|
|
410
|
-
if (onlyDeferred === true) {
|
|
411
|
-
return
|
|
664
|
+
if (onlyDeferred === true && lcTool.defer_loading !== true) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
if (
|
|
668
|
+
hasServerFilter &&
|
|
669
|
+
!isFromAnyMcpServer(lcTool.name, serverFilters)
|
|
670
|
+
) {
|
|
671
|
+
return false;
|
|
412
672
|
}
|
|
413
673
|
return true;
|
|
414
674
|
})
|
|
@@ -419,18 +679,72 @@ Usage:
|
|
|
419
679
|
}));
|
|
420
680
|
|
|
421
681
|
if (deferredTools.length === 0) {
|
|
682
|
+
const serverMsg = hasServerFilter
|
|
683
|
+
? ` from MCP server(s): ${serverFilters.join(', ')}`
|
|
684
|
+
: '';
|
|
422
685
|
return [
|
|
423
|
-
|
|
686
|
+
`No tools available to search${serverMsg}. The tool registry is empty or no matching deferred tools are registered.`,
|
|
424
687
|
{
|
|
425
688
|
tool_references: [],
|
|
426
689
|
metadata: {
|
|
427
690
|
total_searched: 0,
|
|
428
|
-
pattern:
|
|
691
|
+
pattern: query,
|
|
692
|
+
mcp_server: serverFilters,
|
|
429
693
|
},
|
|
430
694
|
},
|
|
431
695
|
];
|
|
432
696
|
}
|
|
433
697
|
|
|
698
|
+
const isServerListing = hasServerFilter && query === '';
|
|
699
|
+
|
|
700
|
+
if (isServerListing) {
|
|
701
|
+
const formattedOutput = formatServerListing(
|
|
702
|
+
deferredTools,
|
|
703
|
+
serverFilters
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
return [
|
|
707
|
+
formattedOutput,
|
|
708
|
+
{
|
|
709
|
+
tool_references: [],
|
|
710
|
+
metadata: {
|
|
711
|
+
total_available: deferredTools.length,
|
|
712
|
+
mcp_server: serverFilters,
|
|
713
|
+
listing_mode: true,
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (mode === 'local') {
|
|
720
|
+
const searchResponse = performLocalSearch(
|
|
721
|
+
deferredTools,
|
|
722
|
+
query,
|
|
723
|
+
fields,
|
|
724
|
+
max_results
|
|
725
|
+
);
|
|
726
|
+
const formattedOutput = formatSearchResults(searchResponse);
|
|
727
|
+
|
|
728
|
+
return [
|
|
729
|
+
formattedOutput,
|
|
730
|
+
{
|
|
731
|
+
tool_references: searchResponse.tool_references,
|
|
732
|
+
metadata: {
|
|
733
|
+
total_searched: searchResponse.total_tools_searched,
|
|
734
|
+
pattern: searchResponse.pattern_used,
|
|
735
|
+
mcp_server: serverFilters.length > 0 ? serverFilters : undefined,
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const { safe: sanitizedPattern, wasEscaped } = sanitizeRegex(query);
|
|
742
|
+
let warningMessage = '';
|
|
743
|
+
if (wasEscaped) {
|
|
744
|
+
warningMessage =
|
|
745
|
+
'Note: The provided pattern was converted to a literal search for safety.\n\n';
|
|
746
|
+
}
|
|
747
|
+
|
|
434
748
|
const searchScript = generateSearchScript(
|
|
435
749
|
deferredTools,
|
|
436
750
|
fields,
|
|
@@ -468,7 +782,7 @@ Usage:
|
|
|
468
782
|
|
|
469
783
|
if (result.stderr && result.stderr.trim()) {
|
|
470
784
|
// eslint-disable-next-line no-console
|
|
471
|
-
console.warn('[
|
|
785
|
+
console.warn('[ToolSearch] stderr:', result.stderr);
|
|
472
786
|
}
|
|
473
787
|
|
|
474
788
|
if (!result.stdout || !result.stdout.trim()) {
|
|
@@ -499,7 +813,7 @@ Usage:
|
|
|
499
813
|
];
|
|
500
814
|
} catch (error) {
|
|
501
815
|
// eslint-disable-next-line no-console
|
|
502
|
-
console.error('[
|
|
816
|
+
console.error('[ToolSearch] Error:', error);
|
|
503
817
|
|
|
504
818
|
const errorMessage =
|
|
505
819
|
error instanceof Error ? error.message : String(error);
|
|
@@ -517,16 +831,23 @@ Usage:
|
|
|
517
831
|
}
|
|
518
832
|
},
|
|
519
833
|
{
|
|
520
|
-
name: Constants.
|
|
834
|
+
name: Constants.TOOL_SEARCH,
|
|
521
835
|
description,
|
|
522
|
-
schema
|
|
836
|
+
schema,
|
|
523
837
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
524
838
|
}
|
|
525
839
|
);
|
|
526
840
|
}
|
|
527
841
|
|
|
528
842
|
export {
|
|
529
|
-
|
|
843
|
+
createToolSearch,
|
|
844
|
+
performLocalSearch,
|
|
845
|
+
extractMcpServerName,
|
|
846
|
+
isFromMcpServer,
|
|
847
|
+
isFromAnyMcpServer,
|
|
848
|
+
normalizeServerFilter,
|
|
849
|
+
getBaseToolName,
|
|
850
|
+
formatServerListing,
|
|
530
851
|
sanitizeRegex,
|
|
531
852
|
escapeRegexSpecialChars,
|
|
532
853
|
isDangerousPattern,
|
package/src/tools/__tests__/{ToolSearchRegex.integration.test.ts → ToolSearch.integration.test.ts}
RENAMED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
// src/tools/__tests__/
|
|
1
|
+
// src/tools/__tests__/ToolSearch.integration.test.ts
|
|
2
2
|
/**
|
|
3
3
|
* Integration tests for Tool Search Regex.
|
|
4
4
|
* These tests hit the LIVE Code API and verify end-to-end search functionality.
|
|
5
5
|
*
|
|
6
|
-
* Run with: npm test --
|
|
6
|
+
* Run with: npm test -- ToolSearch.integration.test.ts
|
|
7
7
|
*/
|
|
8
8
|
import { config as dotenvConfig } from 'dotenv';
|
|
9
9
|
dotenvConfig();
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect, beforeAll } from '@jest/globals';
|
|
12
|
-
import {
|
|
12
|
+
import { createToolSearch } from '../ToolSearch';
|
|
13
13
|
import { createToolSearchToolRegistry } from '@/test/mockTools';
|
|
14
14
|
|
|
15
|
-
describe('
|
|
16
|
-
let searchTool: ReturnType<typeof
|
|
15
|
+
describe('ToolSearch - Live API Integration', () => {
|
|
16
|
+
let searchTool: ReturnType<typeof createToolSearch>;
|
|
17
17
|
const toolRegistry = createToolSearchToolRegistry();
|
|
18
18
|
|
|
19
19
|
beforeAll(() => {
|
|
@@ -24,7 +24,7 @@ describe('ToolSearchRegex - Live API Integration', () => {
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
searchTool =
|
|
27
|
+
searchTool = createToolSearch({ apiKey, toolRegistry });
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('searches for expense-related tools', async () => {
|