@librechat/agents 3.0.67 → 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.
@@ -22,11 +22,12 @@ const SEARCH_TIMEOUT = 5000;
22
22
 
23
23
  /** Zod schema type for tool search parameters */
24
24
  type ToolSearchSchema = z.ZodObject<{
25
- query: z.ZodString;
25
+ query: z.ZodDefault<z.ZodOptional<z.ZodString>>;
26
26
  fields: z.ZodDefault<
27
27
  z.ZodOptional<z.ZodArray<z.ZodEnum<['name', 'description', 'parameters']>>>
28
28
  >;
29
29
  max_results: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
30
+ mcp_server: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString>]>>;
30
31
  }>;
31
32
 
32
33
  /**
@@ -37,11 +38,16 @@ type ToolSearchSchema = z.ZodObject<{
37
38
  function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
38
39
  const queryDescription =
39
40
  mode === 'local'
40
- ? 'Search term to find in tool names and descriptions. Case-insensitive substring matching.'
41
- : 'Regex pattern to search tool names and descriptions. Special regex characters will be sanitized for safety.';
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.';
42
43
 
43
44
  return z.object({
44
- query: z.string().min(1).max(MAX_PATTERN_LENGTH).describe(queryDescription),
45
+ query: z
46
+ .string()
47
+ .max(MAX_PATTERN_LENGTH)
48
+ .optional()
49
+ .default('')
50
+ .describe(queryDescription),
45
51
  fields: z
46
52
  .array(z.enum(['name', 'description', 'parameters']))
47
53
  .optional()
@@ -55,9 +61,71 @@ function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
55
61
  .optional()
56
62
  .default(10)
57
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
+ ),
58
70
  });
59
71
  }
60
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
+ }
128
+
61
129
  /**
62
130
  * Escapes special regex characters in a string to use as a literal pattern.
63
131
  * @param pattern - The string to escape
@@ -395,6 +463,78 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
395
463
  return response;
396
464
  }
397
465
 
466
+ /**
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
+
398
538
  /**
399
539
  * Creates a Tool Search tool for discovering tools from a large registry.
400
540
  *
@@ -447,6 +587,14 @@ function createToolSearch(
447
587
  const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
448
588
  const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
449
589
 
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
+
450
598
  const description =
451
599
  mode === 'local'
452
600
  ? `
@@ -458,6 +606,7 @@ Usage:
458
606
  - Use this when you need to discover tools for a specific task.
459
607
  - Results include tool names, match quality scores, and snippets showing where the match occurred.
460
608
  - Higher scores (0.95+) indicate name matches, medium scores (0.70+) indicate description matches.
609
+ ${mcpInstructions}
461
610
  `.trim()
462
611
  : `
463
612
  Searches through available tools to find ones matching your query pattern.
@@ -467,6 +616,7 @@ Usage:
467
616
  - Use this when you need to discover tools for a specific task.
468
617
  - Results include tool names, match quality scores, and snippets showing where the match occurred.
469
618
  - Higher scores (0.9+) indicate name matches, medium scores (0.7+) indicate description matches.
619
+ ${mcpInstructions}
470
620
  `.trim();
471
621
 
472
622
  return tool<typeof schema>(
@@ -475,11 +625,13 @@ Usage:
475
625
  query,
476
626
  fields = ['name', 'description'],
477
627
  max_results = 10,
628
+ mcp_server,
478
629
  } = params;
479
630
 
480
631
  const {
481
632
  toolRegistry: paramToolRegistry,
482
633
  onlyDeferred: paramOnlyDeferred,
634
+ mcpServer: paramMcpServer,
483
635
  } = config.toolCall ?? {};
484
636
 
485
637
  const toolRegistry = paramToolRegistry ?? initParams.toolRegistry;
@@ -487,6 +639,10 @@ Usage:
487
639
  paramOnlyDeferred !== undefined
488
640
  ? paramOnlyDeferred
489
641
  : defaultOnlyDeferred;
642
+ const rawServerFilter =
643
+ mcp_server ?? paramMcpServer ?? initParams.mcpServer;
644
+ const serverFilters = normalizeServerFilter(rawServerFilter);
645
+ const hasServerFilter = serverFilters.length > 0;
490
646
 
491
647
  if (toolRegistry == null) {
492
648
  return [
@@ -504,9 +660,18 @@ Usage:
504
660
 
505
661
  const toolsArray: t.LCTool[] = Array.from(toolRegistry.values());
506
662
  const deferredTools: t.ToolMetadata[] = toolsArray
507
- .filter((lcTool) =>
508
- onlyDeferred === true ? lcTool.defer_loading === true : true
509
- )
663
+ .filter((lcTool) => {
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;
672
+ }
673
+ return true;
674
+ })
510
675
  .map((lcTool) => ({
511
676
  name: lcTool.name,
512
677
  description: lcTool.description ?? '',
@@ -514,11 +679,39 @@ Usage:
514
679
  }));
515
680
 
516
681
  if (deferredTools.length === 0) {
682
+ const serverMsg = hasServerFilter
683
+ ? ` from MCP server(s): ${serverFilters.join(', ')}`
684
+ : '';
517
685
  return [
518
- 'No tools available to search. The tool registry is empty or no deferred tools are registered.',
686
+ `No tools available to search${serverMsg}. The tool registry is empty or no matching deferred tools are registered.`,
519
687
  {
520
688
  tool_references: [],
521
- metadata: { total_searched: 0, pattern: query },
689
+ metadata: {
690
+ total_searched: 0,
691
+ pattern: query,
692
+ mcp_server: serverFilters,
693
+ },
694
+ },
695
+ ];
696
+ }
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
+ },
522
715
  },
523
716
  ];
524
717
  }
@@ -539,6 +732,7 @@ Usage:
539
732
  metadata: {
540
733
  total_searched: searchResponse.total_tools_searched,
541
734
  pattern: searchResponse.pattern_used,
735
+ mcp_server: serverFilters.length > 0 ? serverFilters : undefined,
542
736
  },
543
737
  },
544
738
  ];
@@ -648,6 +842,12 @@ Usage:
648
842
  export {
649
843
  createToolSearch,
650
844
  performLocalSearch,
845
+ extractMcpServerName,
846
+ isFromMcpServer,
847
+ isFromAnyMcpServer,
848
+ normalizeServerFilter,
849
+ getBaseToolName,
850
+ formatServerListing,
651
851
  sanitizeRegex,
652
852
  escapeRegexSpecialChars,
653
853
  isDangerousPattern,
@@ -1,6 +1,6 @@
1
1
  // src/tools/__tests__/ToolSearch.test.ts
2
2
  /**
3
- * Unit tests for Tool Search Regex.
3
+ * Unit tests for Tool Search.
4
4
  * Tests helper functions and sanitization logic without hitting the API.
5
5
  */
6
6
  import { describe, it, expect } from '@jest/globals';
@@ -11,6 +11,12 @@ import {
11
11
  countNestedGroups,
12
12
  hasNestedQuantifiers,
13
13
  performLocalSearch,
14
+ extractMcpServerName,
15
+ isFromMcpServer,
16
+ isFromAnyMcpServer,
17
+ normalizeServerFilter,
18
+ getBaseToolName,
19
+ formatServerListing,
14
20
  } from '../ToolSearch';
15
21
  import type { ToolMetadata } from '@/types';
16
22
 
@@ -438,4 +444,291 @@ describe('ToolSearch', () => {
438
444
  expect(result.tool_references[0].snippet.length).toBeGreaterThan(0);
439
445
  });
440
446
  });
447
+
448
+ describe('extractMcpServerName', () => {
449
+ it('extracts server name from MCP tool name', () => {
450
+ expect(extractMcpServerName('get_weather_mcp_weather-server')).toBe(
451
+ 'weather-server'
452
+ );
453
+ expect(extractMcpServerName('send_email_mcp_gmail')).toBe('gmail');
454
+ expect(extractMcpServerName('query_database_mcp_postgres-mcp')).toBe(
455
+ 'postgres-mcp'
456
+ );
457
+ });
458
+
459
+ it('returns undefined for non-MCP tools', () => {
460
+ expect(extractMcpServerName('get_weather')).toBeUndefined();
461
+ expect(extractMcpServerName('send_email')).toBeUndefined();
462
+ expect(extractMcpServerName('regular_tool_name')).toBeUndefined();
463
+ });
464
+
465
+ it('handles edge cases', () => {
466
+ expect(extractMcpServerName('_mcp_server')).toBe('server');
467
+ expect(extractMcpServerName('tool_mcp_')).toBe('');
468
+ });
469
+ });
470
+
471
+ describe('getBaseToolName', () => {
472
+ it('extracts base name from MCP tool name', () => {
473
+ expect(getBaseToolName('get_weather_mcp_weather-server')).toBe(
474
+ 'get_weather'
475
+ );
476
+ expect(getBaseToolName('send_email_mcp_gmail')).toBe('send_email');
477
+ });
478
+
479
+ it('returns full name for non-MCP tools', () => {
480
+ expect(getBaseToolName('get_weather')).toBe('get_weather');
481
+ expect(getBaseToolName('regular_tool')).toBe('regular_tool');
482
+ });
483
+ });
484
+
485
+ describe('isFromMcpServer', () => {
486
+ it('returns true for matching MCP server', () => {
487
+ expect(
488
+ isFromMcpServer('get_weather_mcp_weather-server', 'weather-server')
489
+ ).toBe(true);
490
+ expect(isFromMcpServer('send_email_mcp_gmail', 'gmail')).toBe(true);
491
+ });
492
+
493
+ it('returns false for non-matching MCP server', () => {
494
+ expect(
495
+ isFromMcpServer('get_weather_mcp_weather-server', 'other-server')
496
+ ).toBe(false);
497
+ expect(isFromMcpServer('send_email_mcp_gmail', 'outlook')).toBe(false);
498
+ });
499
+
500
+ it('returns false for non-MCP tools', () => {
501
+ expect(isFromMcpServer('get_weather', 'weather-server')).toBe(false);
502
+ expect(isFromMcpServer('regular_tool', 'any-server')).toBe(false);
503
+ });
504
+ });
505
+
506
+ describe('isFromAnyMcpServer', () => {
507
+ it('returns true if tool is from any of the specified servers', () => {
508
+ expect(
509
+ isFromAnyMcpServer('get_weather_mcp_weather-api', [
510
+ 'weather-api',
511
+ 'gmail',
512
+ ])
513
+ ).toBe(true);
514
+ expect(
515
+ isFromAnyMcpServer('send_email_mcp_gmail', ['weather-api', 'gmail'])
516
+ ).toBe(true);
517
+ });
518
+
519
+ it('returns false if tool is not from any specified server', () => {
520
+ expect(
521
+ isFromAnyMcpServer('get_weather_mcp_weather-api', ['gmail', 'slack'])
522
+ ).toBe(false);
523
+ });
524
+
525
+ it('returns false for non-MCP tools', () => {
526
+ expect(isFromAnyMcpServer('regular_tool', ['weather-api', 'gmail'])).toBe(
527
+ false
528
+ );
529
+ });
530
+
531
+ it('returns false for empty server list', () => {
532
+ expect(isFromAnyMcpServer('get_weather_mcp_weather-api', [])).toBe(false);
533
+ });
534
+ });
535
+
536
+ describe('normalizeServerFilter', () => {
537
+ it('converts string to single-element array', () => {
538
+ expect(normalizeServerFilter('gmail')).toEqual(['gmail']);
539
+ });
540
+
541
+ it('passes through arrays unchanged', () => {
542
+ expect(normalizeServerFilter(['gmail', 'slack'])).toEqual([
543
+ 'gmail',
544
+ 'slack',
545
+ ]);
546
+ });
547
+
548
+ it('returns empty array for undefined', () => {
549
+ expect(normalizeServerFilter(undefined)).toEqual([]);
550
+ });
551
+
552
+ it('returns empty array for empty string', () => {
553
+ expect(normalizeServerFilter('')).toEqual([]);
554
+ });
555
+
556
+ it('filters out empty strings from arrays', () => {
557
+ expect(normalizeServerFilter(['gmail', '', 'slack'])).toEqual([
558
+ 'gmail',
559
+ 'slack',
560
+ ]);
561
+ });
562
+ });
563
+
564
+ describe('performLocalSearch with MCP tools', () => {
565
+ const mcpTools: ToolMetadata[] = [
566
+ {
567
+ name: 'get_weather_mcp_weather-server',
568
+ description: 'Get weather from MCP server',
569
+ parameters: undefined,
570
+ },
571
+ {
572
+ name: 'get_forecast_mcp_weather-server',
573
+ description: 'Get forecast from MCP server',
574
+ parameters: undefined,
575
+ },
576
+ {
577
+ name: 'send_email_mcp_gmail',
578
+ description: 'Send email via Gmail MCP',
579
+ parameters: undefined,
580
+ },
581
+ {
582
+ name: 'read_inbox_mcp_gmail',
583
+ description: 'Read inbox via Gmail MCP',
584
+ parameters: undefined,
585
+ },
586
+ {
587
+ name: 'get_weather',
588
+ description: 'Regular weather tool (not MCP)',
589
+ parameters: undefined,
590
+ },
591
+ ];
592
+
593
+ it('searches across all tools including MCP tools', () => {
594
+ const result = performLocalSearch(
595
+ mcpTools,
596
+ 'weather',
597
+ ['name', 'description'],
598
+ 10
599
+ );
600
+
601
+ expect(result.tool_references.length).toBe(3);
602
+ expect(result.tool_references.map((r) => r.tool_name)).toContain(
603
+ 'get_weather_mcp_weather-server'
604
+ );
605
+ expect(result.tool_references.map((r) => r.tool_name)).toContain(
606
+ 'get_weather'
607
+ );
608
+ });
609
+
610
+ it('finds MCP tools by searching the full name including server suffix', () => {
611
+ const result = performLocalSearch(mcpTools, 'gmail', ['name'], 10);
612
+
613
+ expect(result.tool_references.length).toBe(2);
614
+ expect(result.tool_references.map((r) => r.tool_name)).toContain(
615
+ 'send_email_mcp_gmail'
616
+ );
617
+ expect(result.tool_references.map((r) => r.tool_name)).toContain(
618
+ 'read_inbox_mcp_gmail'
619
+ );
620
+ });
621
+
622
+ it('can search for tools by MCP delimiter', () => {
623
+ const result = performLocalSearch(mcpTools, '_mcp_', ['name'], 10);
624
+
625
+ expect(result.tool_references.length).toBe(4);
626
+ expect(result.tool_references.map((r) => r.tool_name)).not.toContain(
627
+ 'get_weather'
628
+ );
629
+ });
630
+ });
631
+
632
+ describe('formatServerListing', () => {
633
+ const serverTools: ToolMetadata[] = [
634
+ {
635
+ name: 'get_weather_mcp_weather-api',
636
+ description: 'Get current weather conditions for a location',
637
+ parameters: undefined,
638
+ },
639
+ {
640
+ name: 'get_forecast_mcp_weather-api',
641
+ description: 'Get weather forecast for the next 7 days',
642
+ parameters: undefined,
643
+ },
644
+ ];
645
+
646
+ it('formats server listing with tool names and descriptions', () => {
647
+ const result = formatServerListing(serverTools, 'weather-api');
648
+
649
+ expect(result).toContain('Tools from MCP server: weather-api');
650
+ expect(result).toContain('2 tool(s)');
651
+ expect(result).toContain('get_weather');
652
+ expect(result).toContain('get_forecast');
653
+ expect(result).toContain('preview only');
654
+ });
655
+
656
+ it('includes hint to search for specific tool to load it', () => {
657
+ const result = formatServerListing(serverTools, 'weather-api');
658
+
659
+ expect(result).toContain('To use a tool, search for it by name');
660
+ });
661
+
662
+ it('uses base tool name (without MCP suffix) in display', () => {
663
+ const result = formatServerListing(serverTools, 'weather-api');
664
+
665
+ expect(result).toContain('**get_weather**');
666
+ expect(result).not.toContain('**get_weather_mcp_weather-api**');
667
+ });
668
+
669
+ it('handles empty tools array', () => {
670
+ const result = formatServerListing([], 'empty-server');
671
+
672
+ expect(result).toContain('No tools found');
673
+ expect(result).toContain('empty-server');
674
+ });
675
+
676
+ it('truncates long descriptions', () => {
677
+ const toolsWithLongDesc: ToolMetadata[] = [
678
+ {
679
+ name: 'long_tool_mcp_server',
680
+ description:
681
+ 'This is a very long description that exceeds 80 characters and should be truncated to keep the listing compact and readable.',
682
+ parameters: undefined,
683
+ },
684
+ ];
685
+
686
+ const result = formatServerListing(toolsWithLongDesc, 'server');
687
+
688
+ expect(result).toContain('...');
689
+ expect(result.length).toBeLessThan(
690
+ toolsWithLongDesc[0].description.length + 200
691
+ );
692
+ });
693
+
694
+ it('handles multiple servers with grouped output', () => {
695
+ const multiServerTools: ToolMetadata[] = [
696
+ {
697
+ name: 'get_weather_mcp_weather-api',
698
+ description: 'Get weather',
699
+ parameters: undefined,
700
+ },
701
+ {
702
+ name: 'send_email_mcp_gmail',
703
+ description: 'Send email',
704
+ parameters: undefined,
705
+ },
706
+ {
707
+ name: 'read_inbox_mcp_gmail',
708
+ description: 'Read inbox',
709
+ parameters: undefined,
710
+ },
711
+ ];
712
+
713
+ const result = formatServerListing(multiServerTools, [
714
+ 'weather-api',
715
+ 'gmail',
716
+ ]);
717
+
718
+ expect(result).toContain('Tools from MCP servers: weather-api, gmail');
719
+ expect(result).toContain('3 tool(s)');
720
+ expect(result).toContain('### weather-api');
721
+ expect(result).toContain('### gmail');
722
+ expect(result).toContain('get_weather');
723
+ expect(result).toContain('send_email');
724
+ expect(result).toContain('read_inbox');
725
+ });
726
+
727
+ it('accepts single server as array', () => {
728
+ const result = formatServerListing(serverTools, ['weather-api']);
729
+
730
+ expect(result).toContain('Tools from MCP server: weather-api');
731
+ expect(result).not.toContain('###');
732
+ });
733
+ });
441
734
  });
@@ -131,7 +131,7 @@ export type ProgrammaticCache = { toolMap: ToolMap; toolDefs: LCTool[] };
131
131
  /** Search mode: code_interpreter uses external sandbox, local uses safe substring matching */
132
132
  export type ToolSearchMode = 'code_interpreter' | 'local';
133
133
 
134
- /** Parameters for creating a Tool Search Regex tool */
134
+ /** Parameters for creating a Tool Search tool */
135
135
  export type ToolSearchParams = {
136
136
  apiKey?: string;
137
137
  toolRegistry?: LCToolRegistry;
@@ -139,6 +139,8 @@ export type ToolSearchParams = {
139
139
  baseUrl?: string;
140
140
  /** Search mode: 'code_interpreter' (default) uses sandbox for regex, 'local' uses safe substring matching */
141
141
  mode?: ToolSearchMode;
142
+ /** Filter tools to only those from specific MCP server(s). Can be a single name or array of names. */
143
+ mcpServer?: string | string[];
142
144
  [key: string]: unknown;
143
145
  };
144
146