@librechat/agents 3.0.70 → 3.0.72
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 +158 -67
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/ToolSearch.mjs +158 -68
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/types/tools/ToolSearch.d.ts +14 -5
- package/package.json +3 -1
- package/src/tools/ToolSearch.ts +194 -71
- package/src/tools/__tests__/ToolSearch.test.ts +103 -13
package/src/tools/ToolSearch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/tools/ToolSearch.ts
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import BM25 from 'okapibm25';
|
|
3
4
|
import { config } from 'dotenv';
|
|
4
5
|
import fetch, { RequestInit } from 'node-fetch';
|
|
5
6
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
@@ -282,13 +283,101 @@ function simplifyParametersForSearch(
|
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
286
|
+
* Tokenizes a string into lowercase words for BM25.
|
|
287
|
+
* @param text - The text to tokenize
|
|
288
|
+
* @returns Array of lowercase tokens
|
|
289
|
+
*/
|
|
290
|
+
function tokenize(text: string): string[] {
|
|
291
|
+
return text
|
|
292
|
+
.toLowerCase()
|
|
293
|
+
.replace(/[^a-z0-9_]/g, ' ')
|
|
294
|
+
.split(/\s+/)
|
|
295
|
+
.filter((token) => token.length > 0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Creates a searchable document string from tool metadata.
|
|
300
|
+
* @param tool - The tool metadata
|
|
301
|
+
* @param fields - Which fields to include
|
|
302
|
+
* @returns Combined document string for BM25
|
|
303
|
+
*/
|
|
304
|
+
function createToolDocument(tool: t.ToolMetadata, fields: string[]): string {
|
|
305
|
+
const parts: string[] = [];
|
|
306
|
+
|
|
307
|
+
if (fields.includes('name')) {
|
|
308
|
+
const baseName = tool.name.replace(/_/g, ' ');
|
|
309
|
+
parts.push(baseName, baseName);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (fields.includes('description') && tool.description) {
|
|
313
|
+
parts.push(tool.description);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (fields.includes('parameters') && tool.parameters?.properties) {
|
|
317
|
+
const paramNames = Object.keys(tool.parameters.properties).join(' ');
|
|
318
|
+
parts.push(paramNames);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return parts.join(' ');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Determines which field had the best match for a query.
|
|
326
|
+
* @param tool - The tool to check
|
|
327
|
+
* @param queryTokens - Tokenized query
|
|
328
|
+
* @param fields - Fields to check
|
|
329
|
+
* @returns The matched field and a snippet
|
|
330
|
+
*/
|
|
331
|
+
function findMatchedField(
|
|
332
|
+
tool: t.ToolMetadata,
|
|
333
|
+
queryTokens: string[],
|
|
334
|
+
fields: string[]
|
|
335
|
+
): { field: string; snippet: string } {
|
|
336
|
+
if (fields.includes('name')) {
|
|
337
|
+
const nameLower = tool.name.toLowerCase();
|
|
338
|
+
for (const token of queryTokens) {
|
|
339
|
+
if (nameLower.includes(token)) {
|
|
340
|
+
return { field: 'name', snippet: tool.name };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (fields.includes('description') && tool.description) {
|
|
346
|
+
const descLower = tool.description.toLowerCase();
|
|
347
|
+
for (const token of queryTokens) {
|
|
348
|
+
if (descLower.includes(token)) {
|
|
349
|
+
return {
|
|
350
|
+
field: 'description',
|
|
351
|
+
snippet: tool.description.substring(0, 100),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (fields.includes('parameters') && tool.parameters?.properties) {
|
|
358
|
+
const paramNames = Object.keys(tool.parameters.properties);
|
|
359
|
+
const paramLower = paramNames.join(' ').toLowerCase();
|
|
360
|
+
for (const token of queryTokens) {
|
|
361
|
+
if (paramLower.includes(token)) {
|
|
362
|
+
return { field: 'parameters', snippet: paramNames.join(', ') };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const fallbackSnippet = tool.description
|
|
368
|
+
? tool.description.substring(0, 100)
|
|
369
|
+
: tool.name;
|
|
370
|
+
return { field: 'unknown', snippet: fallbackSnippet };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Performs BM25-based search for better relevance ranking.
|
|
375
|
+
* Uses Okapi BM25 algorithm for term frequency and document length normalization.
|
|
287
376
|
* @param tools - Array of tool metadata to search
|
|
288
|
-
* @param query - The search
|
|
377
|
+
* @param query - The search query
|
|
289
378
|
* @param fields - Which fields to search
|
|
290
379
|
* @param maxResults - Maximum results to return
|
|
291
|
-
* @returns Search response with matching tools
|
|
380
|
+
* @returns Search response with matching tools ranked by BM25 score
|
|
292
381
|
*/
|
|
293
382
|
function performLocalSearch(
|
|
294
383
|
tools: t.ToolMetadata[],
|
|
@@ -296,50 +385,43 @@ function performLocalSearch(
|
|
|
296
385
|
fields: string[],
|
|
297
386
|
maxResults: number
|
|
298
387
|
): t.ToolSearchResponse {
|
|
299
|
-
|
|
300
|
-
|
|
388
|
+
if (tools.length === 0 || !query.trim()) {
|
|
389
|
+
return {
|
|
390
|
+
tool_references: [],
|
|
391
|
+
total_tools_searched: tools.length,
|
|
392
|
+
pattern_used: query,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
301
395
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
let matchedField = '';
|
|
305
|
-
let snippet = '';
|
|
306
|
-
|
|
307
|
-
if (fields.includes('name')) {
|
|
308
|
-
const lowerName = tool.name.toLowerCase();
|
|
309
|
-
if (lowerName.includes(lowerQuery)) {
|
|
310
|
-
const isExactMatch = lowerName === lowerQuery;
|
|
311
|
-
const startsWithQuery = lowerName.startsWith(lowerQuery);
|
|
312
|
-
bestScore = isExactMatch ? 1.0 : startsWithQuery ? 0.95 : 0.85;
|
|
313
|
-
matchedField = 'name';
|
|
314
|
-
snippet = tool.name;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
396
|
+
const documents = tools.map((tool) => createToolDocument(tool, fields));
|
|
397
|
+
const queryTokens = tokenize(query);
|
|
317
398
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
399
|
+
if (queryTokens.length === 0) {
|
|
400
|
+
return {
|
|
401
|
+
tool_references: [],
|
|
402
|
+
total_tools_searched: tools.length,
|
|
403
|
+
pattern_used: query,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
326
406
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
407
|
+
const scores = BM25(documents, queryTokens, { k1: 1.5, b: 0.75 }) as number[];
|
|
408
|
+
|
|
409
|
+
const maxScore = Math.max(...scores.filter((s) => s > 0), 1);
|
|
410
|
+
|
|
411
|
+
const results: t.ToolSearchResult[] = [];
|
|
412
|
+
for (let i = 0; i < tools.length; i++) {
|
|
413
|
+
if (scores[i] > 0) {
|
|
414
|
+
const { field, snippet } = findMatchedField(
|
|
415
|
+
tools[i],
|
|
416
|
+
queryTokens,
|
|
417
|
+
fields
|
|
418
|
+
);
|
|
419
|
+
const normalizedScore = Math.min(scores[i] / maxScore, 1.0);
|
|
337
420
|
|
|
338
|
-
if (bestScore > 0) {
|
|
339
421
|
results.push({
|
|
340
|
-
tool_name:
|
|
341
|
-
match_score:
|
|
342
|
-
matched_field:
|
|
422
|
+
tool_name: tools[i].name,
|
|
423
|
+
match_score: normalizedScore,
|
|
424
|
+
matched_field: field,
|
|
343
425
|
snippet,
|
|
344
426
|
});
|
|
345
427
|
}
|
|
@@ -501,6 +583,56 @@ function getBaseToolName(toolName: string): string {
|
|
|
501
583
|
return toolName.substring(0, delimiterIndex);
|
|
502
584
|
}
|
|
503
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Generates a compact listing of deferred tools grouped by server.
|
|
588
|
+
* Format: "server: tool1, tool2, tool3"
|
|
589
|
+
* Non-MCP tools are grouped under "other".
|
|
590
|
+
* @param toolRegistry - The tool registry
|
|
591
|
+
* @param onlyDeferred - Whether to only include deferred tools
|
|
592
|
+
* @returns Formatted string with tools grouped by server
|
|
593
|
+
*/
|
|
594
|
+
function getDeferredToolsListing(
|
|
595
|
+
toolRegistry: t.LCToolRegistry | undefined,
|
|
596
|
+
onlyDeferred: boolean
|
|
597
|
+
): string {
|
|
598
|
+
if (!toolRegistry) {
|
|
599
|
+
return '';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const toolsByServer: Record<string, string[]> = {};
|
|
603
|
+
|
|
604
|
+
for (const lcTool of toolRegistry.values()) {
|
|
605
|
+
if (onlyDeferred && lcTool.defer_loading !== true) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const toolName = lcTool.name;
|
|
610
|
+
const serverName = extractMcpServerName(toolName) ?? 'other';
|
|
611
|
+
const baseName = getBaseToolName(toolName);
|
|
612
|
+
|
|
613
|
+
if (!(serverName in toolsByServer)) {
|
|
614
|
+
toolsByServer[serverName] = [];
|
|
615
|
+
}
|
|
616
|
+
toolsByServer[serverName].push(baseName);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const serverNames = Object.keys(toolsByServer).sort((a, b) => {
|
|
620
|
+
if (a === 'other') return 1;
|
|
621
|
+
if (b === 'other') return -1;
|
|
622
|
+
return a.localeCompare(b);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
if (serverNames.length === 0) {
|
|
626
|
+
return '';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const lines = serverNames.map(
|
|
630
|
+
(server) => `${server}: ${toolsByServer[server].join(', ')}`
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
return lines.join('\n');
|
|
634
|
+
}
|
|
635
|
+
|
|
504
636
|
/**
|
|
505
637
|
* Formats a server listing response as structured JSON.
|
|
506
638
|
* NOTE: This is a PREVIEW only - tools are NOT discovered/loaded.
|
|
@@ -609,46 +741,36 @@ function createToolSearch(
|
|
|
609
741
|
const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
|
|
610
742
|
const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
|
|
611
743
|
|
|
612
|
-
const
|
|
744
|
+
const deferredToolsListing = getDeferredToolsListing(
|
|
613
745
|
initParams.toolRegistry,
|
|
614
746
|
defaultOnlyDeferred
|
|
615
747
|
);
|
|
616
748
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
?
|
|
620
|
-
: '';
|
|
749
|
+
const toolsListSection =
|
|
750
|
+
deferredToolsListing.length > 0
|
|
751
|
+
? `
|
|
621
752
|
|
|
622
|
-
|
|
753
|
+
Deferred tools (search to load):
|
|
754
|
+
${deferredToolsListing}`
|
|
755
|
+
: '';
|
|
623
756
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
-
|
|
757
|
+
const mcpNote =
|
|
758
|
+
deferredToolsListing.includes(Constants.MCP_DELIMITER) ||
|
|
759
|
+
deferredToolsListing.split('\n').some((line) => !line.startsWith('other:'))
|
|
760
|
+
? `
|
|
761
|
+
- MCP tools use format: toolName${Constants.MCP_DELIMITER}serverName
|
|
762
|
+
- Use mcp_server param to filter by server`
|
|
763
|
+
: '';
|
|
629
764
|
|
|
630
765
|
const description =
|
|
631
766
|
mode === 'local'
|
|
632
767
|
? `
|
|
633
|
-
Searches
|
|
634
|
-
|
|
635
|
-
Usage:
|
|
636
|
-
- Provide a search term to find in tool names and descriptions.
|
|
637
|
-
- Uses case-insensitive substring matching (fast and safe).
|
|
638
|
-
- Use this when you need to discover tools for a specific task.
|
|
639
|
-
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
640
|
-
- Higher scores (0.95+) indicate name matches, medium scores (0.70+) indicate description matches.
|
|
641
|
-
${mcpInstructions}
|
|
768
|
+
Searches deferred tools using BM25 ranking. Multi-word queries supported.
|
|
769
|
+
${mcpNote}${toolsListSection}
|
|
642
770
|
`.trim()
|
|
643
771
|
: `
|
|
644
|
-
Searches
|
|
645
|
-
|
|
646
|
-
Usage:
|
|
647
|
-
- Provide a regex pattern to search tool names and descriptions.
|
|
648
|
-
- Use this when you need to discover tools for a specific task.
|
|
649
|
-
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
650
|
-
- Higher scores (0.9+) indicate name matches, medium scores (0.7+) indicate description matches.
|
|
651
|
-
${mcpInstructions}
|
|
772
|
+
Searches deferred tools by regex pattern.
|
|
773
|
+
${mcpNote}${toolsListSection}
|
|
652
774
|
`.trim();
|
|
653
775
|
|
|
654
776
|
return tool<typeof schema>(
|
|
@@ -879,6 +1001,7 @@ export {
|
|
|
879
1001
|
isFromAnyMcpServer,
|
|
880
1002
|
normalizeServerFilter,
|
|
881
1003
|
getAvailableMcpServers,
|
|
1004
|
+
getDeferredToolsListing,
|
|
882
1005
|
getBaseToolName,
|
|
883
1006
|
formatServerListing,
|
|
884
1007
|
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';
|
|
@@ -280,19 +281,21 @@ describe('ToolSearch', () => {
|
|
|
280
281
|
];
|
|
281
282
|
|
|
282
283
|
it('finds tools by exact name match', () => {
|
|
283
|
-
|
|
284
|
+
// BM25 tokenizes "get weather" from "get_weather"
|
|
285
|
+
const result = performLocalSearch(mockTools, 'get weather', ['name'], 10);
|
|
284
286
|
|
|
285
|
-
expect(result.tool_references.length).
|
|
287
|
+
expect(result.tool_references.length).toBeGreaterThan(0);
|
|
286
288
|
expect(result.tool_references[0].tool_name).toBe('get_weather');
|
|
287
|
-
expect(result.tool_references[0].match_score).
|
|
289
|
+
expect(result.tool_references[0].match_score).toBeGreaterThan(0.5);
|
|
288
290
|
expect(result.tool_references[0].matched_field).toBe('name');
|
|
289
291
|
});
|
|
290
292
|
|
|
291
|
-
it('finds tools by partial name match
|
|
292
|
-
|
|
293
|
+
it('finds tools by partial name match', () => {
|
|
294
|
+
// BM25 finds tools containing "get" token
|
|
295
|
+
const result = performLocalSearch(mockTools, 'get', ['name'], 10);
|
|
293
296
|
|
|
294
297
|
expect(result.tool_references.length).toBe(3);
|
|
295
|
-
expect(result.tool_references[0].match_score).
|
|
298
|
+
expect(result.tool_references[0].match_score).toBeGreaterThan(0);
|
|
296
299
|
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
297
300
|
'get_weather'
|
|
298
301
|
);
|
|
@@ -314,7 +317,7 @@ describe('ToolSearch', () => {
|
|
|
314
317
|
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
315
318
|
'calculate_expense_totals'
|
|
316
319
|
);
|
|
317
|
-
expect(result.tool_references[0].match_score).
|
|
320
|
+
expect(result.tool_references[0].match_score).toBeGreaterThan(0);
|
|
318
321
|
});
|
|
319
322
|
|
|
320
323
|
it('performs case-insensitive search', () => {
|
|
@@ -345,16 +348,16 @@ describe('ToolSearch', () => {
|
|
|
345
348
|
expect(result.tool_references.length).toBe(1);
|
|
346
349
|
expect(result.tool_references[0].tool_name).toBe('send_email');
|
|
347
350
|
expect(result.tool_references[0].matched_field).toBe('description');
|
|
348
|
-
expect(result.tool_references[0].match_score).
|
|
351
|
+
expect(result.tool_references[0].match_score).toBeGreaterThan(0);
|
|
349
352
|
});
|
|
350
353
|
|
|
351
354
|
it('searches in parameter names', () => {
|
|
352
355
|
const result = performLocalSearch(mockTools, 'query', ['parameters'], 10);
|
|
353
356
|
|
|
354
|
-
expect(result.tool_references.length).
|
|
357
|
+
expect(result.tool_references.length).toBeGreaterThan(0);
|
|
355
358
|
expect(result.tool_references[0].tool_name).toBe('run_database_query');
|
|
356
359
|
expect(result.tool_references[0].matched_field).toBe('parameters');
|
|
357
|
-
expect(result.tool_references[0].match_score).
|
|
360
|
+
expect(result.tool_references[0].match_score).toBeGreaterThan(0);
|
|
358
361
|
});
|
|
359
362
|
|
|
360
363
|
it('prioritizes name matches over description matches', () => {
|
|
@@ -423,7 +426,9 @@ describe('ToolSearch', () => {
|
|
|
423
426
|
it('handles empty query gracefully', () => {
|
|
424
427
|
const result = performLocalSearch(mockTools, '', ['name'], 10);
|
|
425
428
|
|
|
426
|
-
|
|
429
|
+
// BM25 correctly returns no results for empty queries (no terms to match)
|
|
430
|
+
expect(result.tool_references.length).toBe(0);
|
|
431
|
+
expect(result.total_tools_searched).toBe(mockTools.length);
|
|
427
432
|
});
|
|
428
433
|
|
|
429
434
|
it('includes correct metadata in response', () => {
|
|
@@ -650,6 +655,90 @@ describe('ToolSearch', () => {
|
|
|
650
655
|
});
|
|
651
656
|
});
|
|
652
657
|
|
|
658
|
+
describe('getDeferredToolsListing', () => {
|
|
659
|
+
const createRegistry = (): LCToolRegistry => {
|
|
660
|
+
const registry: LCToolRegistry = new Map();
|
|
661
|
+
registry.set('get_weather_mcp_weather-api', {
|
|
662
|
+
name: 'get_weather_mcp_weather-api',
|
|
663
|
+
description: 'Get weather',
|
|
664
|
+
defer_loading: true,
|
|
665
|
+
});
|
|
666
|
+
registry.set('get_forecast_mcp_weather-api', {
|
|
667
|
+
name: 'get_forecast_mcp_weather-api',
|
|
668
|
+
description: 'Get forecast',
|
|
669
|
+
defer_loading: true,
|
|
670
|
+
});
|
|
671
|
+
registry.set('send_email_mcp_gmail', {
|
|
672
|
+
name: 'send_email_mcp_gmail',
|
|
673
|
+
description: 'Send email',
|
|
674
|
+
defer_loading: true,
|
|
675
|
+
});
|
|
676
|
+
registry.set('execute_code', {
|
|
677
|
+
name: 'execute_code',
|
|
678
|
+
description: 'Execute code',
|
|
679
|
+
defer_loading: true,
|
|
680
|
+
});
|
|
681
|
+
registry.set('read_file', {
|
|
682
|
+
name: 'read_file',
|
|
683
|
+
description: 'Read file',
|
|
684
|
+
defer_loading: false,
|
|
685
|
+
});
|
|
686
|
+
return registry;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
it('groups tools by server with format D', () => {
|
|
690
|
+
const registry = createRegistry();
|
|
691
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
692
|
+
|
|
693
|
+
expect(listing).toContain('gmail: send_email');
|
|
694
|
+
expect(listing).toContain('weather-api: get_weather, get_forecast');
|
|
695
|
+
expect(listing).toContain('other: execute_code');
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('sorts servers alphabetically with other last', () => {
|
|
699
|
+
const registry = createRegistry();
|
|
700
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
701
|
+
const lines = listing.split('\n');
|
|
702
|
+
|
|
703
|
+
expect(lines[0]).toMatch(/^gmail:/);
|
|
704
|
+
expect(lines[1]).toMatch(/^weather-api:/);
|
|
705
|
+
expect(lines[2]).toMatch(/^other:/);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('uses base tool names without MCP suffix', () => {
|
|
709
|
+
const registry = createRegistry();
|
|
710
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
711
|
+
|
|
712
|
+
expect(listing).toContain('get_weather');
|
|
713
|
+
expect(listing).not.toContain('get_weather_mcp_weather-api');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('respects onlyDeferred flag', () => {
|
|
717
|
+
const registry = createRegistry();
|
|
718
|
+
|
|
719
|
+
const deferredOnly = getDeferredToolsListing(registry, true);
|
|
720
|
+
expect(deferredOnly).not.toContain('read_file');
|
|
721
|
+
|
|
722
|
+
const allTools = getDeferredToolsListing(registry, false);
|
|
723
|
+
expect(allTools).toContain('read_file');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('returns empty string for undefined registry', () => {
|
|
727
|
+
expect(getDeferredToolsListing(undefined, true)).toBe('');
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('returns empty string for registry with no matching tools', () => {
|
|
731
|
+
const registry: LCToolRegistry = new Map();
|
|
732
|
+
registry.set('read_file', {
|
|
733
|
+
name: 'read_file',
|
|
734
|
+
description: 'Read file',
|
|
735
|
+
defer_loading: false,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
expect(getDeferredToolsListing(registry, true)).toBe('');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
653
742
|
describe('performLocalSearch with MCP tools', () => {
|
|
654
743
|
const mcpTools: ToolMetadata[] = [
|
|
655
744
|
{
|
|
@@ -708,8 +797,9 @@ describe('ToolSearch', () => {
|
|
|
708
797
|
);
|
|
709
798
|
});
|
|
710
799
|
|
|
711
|
-
it('can search for tools by MCP
|
|
712
|
-
|
|
800
|
+
it('can search for tools by MCP keyword', () => {
|
|
801
|
+
// BM25 tokenizes queries, so search for "mcp" to find MCP tools
|
|
802
|
+
const result = performLocalSearch(mcpTools, 'mcp', ['name'], 10);
|
|
713
803
|
|
|
714
804
|
expect(result.tool_references.length).toBe(4);
|
|
715
805
|
expect(result.tool_references.map((r) => r.tool_name)).not.toContain(
|