@mcp-ts/sdk 1.5.3 → 1.6.1

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.
Files changed (60) hide show
  1. package/dist/adapters/agui-adapter.d.mts +2 -2
  2. package/dist/adapters/agui-adapter.d.ts +2 -2
  3. package/dist/adapters/agui-adapter.js +69 -18
  4. package/dist/adapters/agui-adapter.js.map +1 -1
  5. package/dist/adapters/agui-adapter.mjs +69 -18
  6. package/dist/adapters/agui-adapter.mjs.map +1 -1
  7. package/dist/adapters/agui-middleware.d.mts +2 -2
  8. package/dist/adapters/agui-middleware.d.ts +2 -2
  9. package/dist/adapters/ai-adapter.d.mts +2 -2
  10. package/dist/adapters/ai-adapter.d.ts +2 -2
  11. package/dist/adapters/ai-adapter.js +69 -18
  12. package/dist/adapters/ai-adapter.js.map +1 -1
  13. package/dist/adapters/ai-adapter.mjs +69 -18
  14. package/dist/adapters/ai-adapter.mjs.map +1 -1
  15. package/dist/adapters/langchain-adapter.d.mts +2 -2
  16. package/dist/adapters/langchain-adapter.d.ts +2 -2
  17. package/dist/adapters/langchain-adapter.js +69 -18
  18. package/dist/adapters/langchain-adapter.js.map +1 -1
  19. package/dist/adapters/langchain-adapter.mjs +69 -18
  20. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  21. package/dist/client/index.d.mts +2 -2
  22. package/dist/client/index.d.ts +2 -2
  23. package/dist/client/react.d.mts +4 -4
  24. package/dist/client/react.d.ts +4 -4
  25. package/dist/client/react.js.map +1 -1
  26. package/dist/client/react.mjs.map +1 -1
  27. package/dist/client/vue.d.mts +4 -4
  28. package/dist/client/vue.d.ts +4 -4
  29. package/dist/{index-GfC_eNEv.d.ts → index-DhA-OEAe.d.ts} +1 -1
  30. package/dist/{index-DcYfpY3H.d.mts → index-bFL4ZF2N.d.mts} +1 -1
  31. package/dist/index.d.mts +4 -4
  32. package/dist/index.d.ts +4 -4
  33. package/dist/index.js +212 -44
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +212 -45
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/server/index.d.mts +2 -2
  38. package/dist/server/index.d.ts +2 -2
  39. package/dist/server/index.js +13 -5
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/index.mjs +13 -5
  42. package/dist/server/index.mjs.map +1 -1
  43. package/dist/shared/index.d.mts +17 -10
  44. package/dist/shared/index.d.ts +17 -10
  45. package/dist/shared/index.js +199 -39
  46. package/dist/shared/index.js.map +1 -1
  47. package/dist/shared/index.mjs +199 -40
  48. package/dist/shared/index.mjs.map +1 -1
  49. package/dist/{tool-router-DsKhRmJm.d.ts → tool-router-BVaV1udm.d.mts} +57 -8
  50. package/dist/{tool-router-DK0RJblO.d.mts → tool-router-Dh2804tM.d.ts} +57 -8
  51. package/dist/{types-CfCoIsWI.d.mts → types-rIuN1CQi.d.mts} +1 -0
  52. package/dist/{types-CfCoIsWI.d.ts → types-rIuN1CQi.d.ts} +1 -0
  53. package/package.json +3 -1
  54. package/src/server/handlers/sse-handler.ts +12 -0
  55. package/src/server/mcp/oauth-client.ts +10 -6
  56. package/src/shared/index.ts +4 -0
  57. package/src/shared/meta-tools.ts +163 -37
  58. package/src/shared/tool-index.ts +123 -7
  59. package/src/shared/tool-router.ts +40 -7
  60. package/src/shared/types.ts +1 -0
@@ -7,7 +7,7 @@
7
7
  * only the tools it actually needs.
8
8
  *
9
9
  * Meta-tools:
10
- * • `mcp_search_tool_bm25` BM25 natural language search
10
+ * • `mcp_search_tools` Search/list available tools
11
11
  * • `mcp_search_tool_regex` — Regex pattern search
12
12
  * • `mcp_get_tool_schema` — Get full inputSchema for a discovered tool
13
13
  * • `mcp_execute_tool` — Execute a discovered tool
@@ -17,14 +17,14 @@
17
17
 
18
18
  import type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
19
19
  import type { ToolRouter } from './tool-router.js';
20
- import type { IndexedTool } from './tool-index.js';
20
+ import type { IndexedTool, ToolLookupOptions } from './tool-index.js';
21
21
 
22
22
  // ---------------------------------------------------------------------------
23
23
  // Tool Definitions
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
26
  /**
27
- * Creates the `mcp_search_tool_bm25` tool definition.
27
+ * Creates the `mcp_search_tools` tool definition.
28
28
  *
29
29
  * This tool lets the LLM search the full catalog of available MCP tools
30
30
  * using a BM25 natural-language query. Returns tool names and descriptions
@@ -32,7 +32,7 @@ import type { IndexedTool } from './tool-index.js';
32
32
  */
33
33
  export function createSearchToolDefinition(): Tool {
34
34
  return {
35
- name: 'mcp_search_tool_bm25',
35
+ name: 'mcp_search_tools',
36
36
  description:
37
37
  'Search the catalog of available tools. Returns tool names, descriptions, and server info. ' +
38
38
  'Use this FIRST to find relevant tools before calling them.\n\n' +
@@ -47,9 +47,28 @@ export function createSearchToolDefinition(): Tool {
47
47
  type: 'string',
48
48
  description: 'Query to find tools. Use "select:<tool_name>" for direct selection, or keywords to search. Prefix keywords with + to require them.',
49
49
  },
50
+ operation: {
51
+ type: 'string',
52
+ enum: ['search', 'list'],
53
+ description:
54
+ 'Operation to perform. Use "search" to find relevant tools by capability. Use "list" with serverId or serverName when the user asks for every tool from a connected MCP server.',
55
+ },
56
+ serverId: {
57
+ type: 'string',
58
+ description: 'Optional server ID to restrict search/list results to one MCP server.',
59
+ },
60
+ serverName: {
61
+ type: 'string',
62
+ description:
63
+ 'Optional server name fragment to restrict search/list results to matching MCP servers, e.g. "supabase".',
64
+ },
50
65
  limit: {
51
66
  type: 'number',
52
- description: 'Maximum number of results to return (default: 5, max: 20).',
67
+ description: 'Maximum number of results to return (default: 5 for search, 20 for list; max: 20 for search, 100 for list).',
68
+ },
69
+ cursor: {
70
+ type: 'string',
71
+ description: 'Optional pagination cursor returned by operation "list".',
53
72
  },
54
73
  },
55
74
  required: ['query'],
@@ -57,6 +76,31 @@ export function createSearchToolDefinition(): Tool {
57
76
  };
58
77
  }
59
78
 
79
+ /**
80
+ * Creates the `mcp_list_servers` tool definition.
81
+ *
82
+ * This tool lets the LLM inspect connected MCP servers before doing
83
+ * server-scoped tool discovery.
84
+ */
85
+ export function createListServersToolDefinition(): Tool {
86
+ return {
87
+ name: 'mcp_list_servers',
88
+ description:
89
+ 'List connected MCP servers and their tool counts. ' +
90
+ 'Use this when mcp_search_tools returns no matches, then retry mcp_search_tools with serverId or serverName.',
91
+ inputSchema: {
92
+ type: 'object' as const,
93
+ properties: {
94
+ query: {
95
+ type: 'string',
96
+ description:
97
+ 'Optional server filter text. Matches server name or serverId, e.g. "web" or "supabase".',
98
+ },
99
+ },
100
+ },
101
+ };
102
+ }
103
+
60
104
  /**
61
105
  * Creates the `mcp_search_tool_regex` tool definition.
62
106
  *
@@ -89,7 +133,7 @@ export function createRegexSearchToolDefinition(): Tool {
89
133
  /**
90
134
  * Creates the `mcp_get_tool_schema` tool definition.
91
135
  *
92
- * After discovering tools via `mcp_search_tool_bm25` or
136
+ * After discovering tools via `mcp_search_tools` or
93
137
  * `mcp_search_tool_regex`, the LLM calls this to load the full
94
138
  * inputSchema for a specific tool so it can construct the correct
95
139
  * arguments.
@@ -99,7 +143,7 @@ export function createGetSchemaToolDefinition(): Tool {
99
143
  name: 'mcp_get_tool_schema',
100
144
  description:
101
145
  'Get the full input schema (parameters) for a specific tool. ' +
102
- 'Call this after mcp_search_tool_bm25 to get the parameter details ' +
146
+ 'Call this after mcp_search_tools to get the parameter details ' +
103
147
  'needed to call a tool correctly. ' +
104
148
  'Do NOT call the discovered tool directly; after reading the schema, call mcp_execute_tool.',
105
149
  inputSchema: {
@@ -107,12 +151,12 @@ export function createGetSchemaToolDefinition(): Tool {
107
151
  properties: {
108
152
  toolName: {
109
153
  type: 'string',
110
- description: 'The exact tool name returned by mcp_search_tool_bm25.',
154
+ description: 'The exact tool name returned by mcp_search_tools.',
111
155
  },
112
156
  serverId: {
113
157
  type: 'string',
114
158
  description:
115
- 'Optional: The server ID provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
159
+ 'Optional: The server ID provided in mcp_search_tools. Required if multiple tools have the same name.',
116
160
  },
117
161
  },
118
162
  required: ['toolName'],
@@ -124,7 +168,7 @@ export function createGetSchemaToolDefinition(): Tool {
124
168
  * Creates the `mcp_execute_tool` tool definition.
125
169
  *
126
170
  * This is the execution meta-tool — the LLM calls this to execute any
127
- * tool discovered via `mcp_search_tool_bm25` or `mcp_search_tool_regex`.
171
+ * tool discovered via `mcp_search_tools` or `mcp_search_tool_regex`.
128
172
  * The LLM should first call `mcp_get_tool_schema` to know the correct
129
173
  * arguments.
130
174
  *
@@ -135,7 +179,7 @@ export function createExecuteToolDefinition(): Tool {
135
179
  return {
136
180
  name: 'mcp_execute_tool',
137
181
  description:
138
- 'Execute a tool that was discovered via mcp_search_tool_bm25. ' +
182
+ 'Execute a tool that was discovered via mcp_search_tools. ' +
139
183
  'You MUST call mcp_get_tool_schema first to know the correct parameters. ' +
140
184
  'Pass the exact tool name and its arguments.',
141
185
  inputSchema: {
@@ -143,12 +187,12 @@ export function createExecuteToolDefinition(): Tool {
143
187
  properties: {
144
188
  toolName: {
145
189
  type: 'string',
146
- description: 'The exact tool name from mcp_search_tool_bm25 results.',
190
+ description: 'The exact tool name from mcp_search_tools results.',
147
191
  },
148
192
  serverId: {
149
193
  type: 'string',
150
194
  description:
151
- 'Optional: The server ID provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
195
+ 'Optional: The server ID provided in mcp_search_tools. Required if multiple tools have the same name.',
152
196
  },
153
197
  args: {
154
198
  type: 'object',
@@ -179,7 +223,7 @@ export type CallToolFn = (
179
223
  /**
180
224
  * Execute a meta-tool call and return the result in MCP CallToolResult format.
181
225
  *
182
- * @param toolName - One of the meta-tool names (mcp_search_tool_bm25, mcp_search_tool_regex, etc.)
226
+ * @param toolName - One of the meta-tool names (mcp_search_tools, mcp_list_servers, mcp_search_tool_regex, etc.)
183
227
  * @param args - The arguments from the LLM's tool call
184
228
  * @param router - The ToolRouter to query
185
229
  * @param callToolFn - Optional callback for executing real tools (required for mcp_execute_tool)
@@ -191,9 +235,13 @@ export async function executeMetaTool(
191
235
  router: ToolRouter,
192
236
  callToolFn?: CallToolFn
193
237
  ): Promise<CallToolResult | null> {
194
- const resolveToolSchema = (name: string, namespace?: string): { tool?: IndexedTool; error?: CallToolResult } => {
238
+ const resolveToolSchema = (
239
+ name: string,
240
+ namespace?: string,
241
+ options?: ToolLookupOptions
242
+ ): { tool?: IndexedTool; error?: CallToolResult } => {
195
243
  try {
196
- return { tool: router.getToolSchema(name, namespace) };
244
+ return { tool: router.getToolSchema(name, namespace, options) };
197
245
  } catch (err) {
198
246
  const errorMessage = err instanceof Error ? err.message : String(err);
199
247
  return {
@@ -206,13 +254,61 @@ export async function executeMetaTool(
206
254
  };
207
255
 
208
256
  switch (toolName) {
209
- case 'mcp_search_tool_bm25': {
257
+ case 'mcp_search_tools': {
210
258
  const query = String(args.query ?? '');
259
+ const operation = String(args.operation ?? 'search');
260
+ const serverId = String(args.serverId ?? '') || undefined;
261
+ const serverName = String(args.serverName ?? '') || undefined;
262
+
263
+ if (operation === 'list') {
264
+ const limit = Math.min(Number(args.limit) || 20, 100);
265
+ const cursor = String(args.cursor ?? '') || undefined;
266
+ const result = await router.listTools({
267
+ serverId,
268
+ serverName: serverName ?? (!serverId && query ? query : undefined),
269
+ limit,
270
+ cursor,
271
+ });
272
+
273
+ const serverText = result.servers.length > 0
274
+ ? result.servers
275
+ .map((server) => `${server.serverName} (serverId: ${server.serverId}, tools: ${server.toolCount})`)
276
+ .join(', ')
277
+ : 'none';
278
+
279
+ const lines: string[] = [
280
+ 'operation: list',
281
+ `servers: ${serverText}`,
282
+ `totalCount: ${result.totalCount}`,
283
+ `returnedCount: ${result.returnedCount}`,
284
+ `nextCursor: ${result.nextCursor ?? 'null'}`,
285
+ '',
286
+ ];
287
+
288
+ if (result.tools.length > 0) {
289
+ lines.push(...formatToolSummaries(result.tools));
290
+ } else {
291
+ lines.push(
292
+ serverId || serverName
293
+ ? 'No tools found for the requested server scope.'
294
+ : 'No tools found. Try operation "search" or provide serverId/serverName.'
295
+ );
296
+ }
297
+
298
+ return {
299
+ content: [{ type: 'text', text: lines.join('\n') }],
300
+ isError: false,
301
+ };
302
+ }
303
+
211
304
  const limit = Math.min(Number(args.limit) || 5, 20);
305
+ const searchOptions = { serverId, serverName };
212
306
 
213
307
  // Fast path: Check for select: prefix
214
308
  const selectMatch = query.match(/^select:(.+)$/i);
215
309
  if (selectMatch) {
310
+ await router.listTools({ serverId, serverName, limit: 1 });
311
+
216
312
  const requested = selectMatch[1]!
217
313
  .split(',')
218
314
  .map((s) => s.trim())
@@ -221,15 +317,19 @@ export async function executeMetaTool(
221
317
  const found: any[] = [];
222
318
  const errors: string[] = [];
223
319
 
320
+ const namespace = serverId ?? serverName;
321
+
224
322
  for (const requestedToolName of requested) {
225
- const { tool, error } = resolveToolSchema(requestedToolName);
323
+ const { tool, error } = resolveToolSchema(requestedToolName, namespace, {
324
+ allowServerNameFragment: Boolean(serverName && !serverId),
325
+ });
226
326
  if (error) {
227
327
  const errorMsg = error.content[0]?.type === 'text' ? error.content[0].text : 'Unknown error';
228
328
  errors.push(`- **${requestedToolName}**: ${errorMsg}`);
229
329
  } else if (tool) {
230
330
  found.push(tool);
231
331
  } else {
232
- errors.push(`- **${requestedToolName}**: Tool not found. Try searching with mcp_search_tool_bm25.`);
332
+ errors.push(`- **${requestedToolName}**: Tool not found. Try searching with mcp_search_tools.`);
233
333
  }
234
334
  }
235
335
 
@@ -257,16 +357,31 @@ export async function executeMetaTool(
257
357
  };
258
358
  }
259
359
 
260
- const results = await router.searchTools(query, limit);
360
+ const results = await router.searchTools(query, limit, searchOptions);
261
361
 
262
362
  const text = results.length === 0
263
- ? 'No tools found matching your query. Try different keywords.'
264
- : results
363
+ ? 'No tools found matching your query. Call mcp_list_servers to inspect connected servers, then retry mcp_search_tools with serverId or serverName.'
364
+ : formatToolSummaries(results).join('\n');
365
+
366
+ return {
367
+ content: [{ type: 'text', text }],
368
+ isError: false,
369
+ };
370
+ }
371
+
372
+ case 'mcp_list_servers': {
373
+ const query = String(args.query ?? '').trim();
374
+ const servers = await router.listServers({
375
+ serverName: query || undefined,
376
+ });
377
+
378
+ const text = servers.length === 0
379
+ ? 'No connected servers found.'
380
+ : servers
265
381
  .map(
266
- (t, i) =>
267
- `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
268
- ` ${t.description}\n` +
269
- ` Estimated tokens: ${t.estimatedTokens}`
382
+ (server, i) =>
383
+ `${i + 1}. **${server.serverName}** (serverId: ${server.serverId}, sessionId: ${server.sessionId})\n` +
384
+ ` Tool count: ${server.toolCount}`
270
385
  )
271
386
  .join('\n');
272
387
 
@@ -284,14 +399,7 @@ export async function executeMetaTool(
284
399
 
285
400
  const text = results.length === 0
286
401
  ? 'No tools matched your regex pattern. Try a broader pattern.'
287
- : results
288
- .map(
289
- (t, i) =>
290
- `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
291
- ` ${t.description}\n` +
292
- ` Estimated tokens: ${t.estimatedTokens}`
293
- )
294
- .join('\n');
402
+ : formatToolSummaries(results).join('\n');
295
403
 
296
404
  return {
297
405
  content: [{ type: 'text', text }],
@@ -313,7 +421,7 @@ export async function executeMetaTool(
313
421
  content: [
314
422
  {
315
423
  type: 'text',
316
- text: `Tool "${name}" not found. Use mcp_search_tool_bm25 to find available tools first.`,
424
+ text: `Tool "${name}" not found. Use mcp_search_tools to find available tools first.`,
317
425
  },
318
426
  ],
319
427
  isError: true,
@@ -362,7 +470,7 @@ export async function executeMetaTool(
362
470
  content: [
363
471
  {
364
472
  type: 'text',
365
- text: `Tool "${targetToolName}" not found. Use mcp_search_tool_bm25 to discover available tools first.`,
473
+ text: `Tool "${targetToolName}" not found. Use mcp_search_tools to discover available tools first.`,
366
474
  },
367
475
  ],
368
476
  isError: true,
@@ -404,10 +512,28 @@ export async function executeMetaTool(
404
512
  }
405
513
  }
406
514
 
515
+ function formatToolSummaries(
516
+ tools: Array<{
517
+ name: string;
518
+ description: string;
519
+ serverName: string;
520
+ serverId: string;
521
+ estimatedTokens: number;
522
+ }>
523
+ ): string[] {
524
+ return tools.map(
525
+ (t, i) =>
526
+ `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
527
+ ` ${t.description}\n` +
528
+ ` Estimated tokens: ${t.estimatedTokens}`
529
+ );
530
+ }
531
+
407
532
  /** Check if a tool name is one of the meta-tools. */
408
533
  export function isMetaTool(toolName: string): boolean {
409
534
  return (
410
- toolName === 'mcp_search_tool_bm25' ||
535
+ toolName === 'mcp_search_tools' ||
536
+ toolName === 'mcp_list_servers' ||
411
537
  toolName === 'mcp_search_tool_regex' ||
412
538
  toolName === 'mcp_get_tool_schema' ||
413
539
  toolName === 'mcp_execute_tool'
@@ -32,6 +32,43 @@ export interface ToolSummary {
32
32
  estimatedTokens: number;
33
33
  }
34
34
 
35
+ /** Server-level summary derived from indexed tools. */
36
+ export interface ToolServerSummary {
37
+ /** Human-readable server name */
38
+ serverName: string;
39
+ /** Stable server identifier */
40
+ serverId: string;
41
+ /** Session the server belongs to */
42
+ sessionId: string;
43
+ /** Number of indexed tools for this server */
44
+ toolCount: number;
45
+ }
46
+
47
+ /** Optional filters for search and listing. */
48
+ export interface ToolSearchOptions {
49
+ /** Restrict results to this server ID. */
50
+ serverId?: string;
51
+ /** Restrict results to servers whose name or ID matches this value. */
52
+ serverName?: string;
53
+ }
54
+
55
+ /** Paginated tool listing result. */
56
+ export interface ToolListResult {
57
+ tools: ToolSummary[];
58
+ totalCount: number;
59
+ returnedCount: number;
60
+ nextCursor?: string;
61
+ servers: ToolServerSummary[];
62
+ }
63
+
64
+ export interface ToolLookupOptions {
65
+ /**
66
+ * Allow namespace to match a fragment of serverName after exact
67
+ * sessionId/serverId matching fails.
68
+ */
69
+ allowServerNameFragment?: boolean;
70
+ }
71
+
35
72
  /** A tool with routing metadata attached during indexing. */
36
73
  export interface IndexedTool extends Tool {
37
74
  sessionId: string;
@@ -259,14 +296,14 @@ export class ToolIndex {
259
296
  *
260
297
  * `score = keywordWeight × keyword_score + (1 - keywordWeight) × cosine_score`
261
298
  */
262
- async search(query: string, topK = 5): Promise<ToolSummary[]> {
299
+ async search(query: string, topK = 5, options: ToolSearchOptions = {}): Promise<ToolSummary[]> {
263
300
  if (this.tools.size === 0) return [];
264
301
 
265
302
  const queryLower = query.toLowerCase().trim();
266
303
 
267
304
  // Fast path: Exact tool name match (supports duplicate names across servers)
268
305
  const exactMatches = [...this.toolSummaries.values()].filter(
269
- (summary) => summary.name.toLowerCase() === queryLower
306
+ (summary) => summary.name.toLowerCase() === queryLower && this.matchesServer(summary, options)
270
307
  );
271
308
  if (exactMatches.length > 0) {
272
309
  return exactMatches.slice(0, topK);
@@ -275,7 +312,7 @@ export class ToolIndex {
275
312
  // Fast path: MCP prefix match (e.g. "mcp__github")
276
313
  if (queryLower.startsWith('mcp__') && queryLower.length > 5) {
277
314
  const prefixMatches = [...this.toolSummaries.values()]
278
- .filter((t) => t.name.toLowerCase().startsWith(queryLower))
315
+ .filter((t) => t.name.toLowerCase().startsWith(queryLower) && this.matchesServer(t, options))
279
316
  .slice(0, topK);
280
317
  if (prefixMatches.length > 0) return prefixMatches;
281
318
  }
@@ -300,9 +337,11 @@ export class ToolIndex {
300
337
  // Pre-filter: only keep documents that contain ALL required terms
301
338
  const candidateKeys = new Set<string>();
302
339
  for (const docKey of this.toolSummaries.keys()) {
340
+ const summary = this.toolSummaries.get(docKey)!;
341
+ if (!this.matchesServer(summary, options)) continue;
342
+
303
343
  if (requiredTerms.length > 0) {
304
344
  const text = this.searchTexts.get(docKey) || '';
305
- const summary = this.toolSummaries.get(docKey)!;
306
345
  const nameLower = summary.name.toLowerCase();
307
346
  const matchesAll = requiredTerms.every(
308
347
  (term) => text.includes(term) || nameLower.includes(term)
@@ -456,13 +495,22 @@ export class ToolIndex {
456
495
 
457
496
  /**
458
497
  * Get tool definition(s) by name.
459
- * If namespace is provided, it tries to match sessionId or serverName.
498
+ * If namespace is provided, exact sessionId/serverId matches take precedence.
499
+ * Falls back to serverName fragment matching only when explicitly allowed.
460
500
  */
461
- getTool(name: string, namespace?: string): IndexedTool[] {
501
+ getTool(name: string, namespace?: string, options: ToolLookupOptions = {}): IndexedTool[] {
462
502
  const list = this.tools.get(name) ?? [];
463
503
  if (!namespace) return list;
464
504
 
465
- return list.filter((t) => t.sessionId === namespace || t.serverId === namespace);
505
+ const exactMatches = list.filter(
506
+ (t) => t.sessionId === namespace || t.serverId === namespace
507
+ );
508
+ if (exactMatches.length > 0) return exactMatches;
509
+
510
+ if (!options.allowServerNameFragment) return [];
511
+
512
+ const namespaceLower = namespace.toLowerCase();
513
+ return list.filter((t) => t.serverName.toLowerCase().includes(namespaceLower));
466
514
  }
467
515
 
468
516
  /** All indexed tool names. */
@@ -470,6 +518,57 @@ export class ToolIndex {
470
518
  return [...this.tools.keys()];
471
519
  }
472
520
 
521
+ /** List indexed servers with tool counts. */
522
+ listServers(options: ToolSearchOptions = {}): ToolServerSummary[] {
523
+ const servers = new Map<string, ToolServerSummary>();
524
+
525
+ for (const summary of this.toolSummaries.values()) {
526
+ if (!this.matchesServer(summary, options)) continue;
527
+
528
+ const key = `${summary.sessionId}::${summary.serverId}`;
529
+ const existing = servers.get(key);
530
+ if (existing) {
531
+ existing.toolCount += 1;
532
+ } else {
533
+ servers.set(key, {
534
+ serverName: summary.serverName,
535
+ serverId: summary.serverId,
536
+ sessionId: summary.sessionId,
537
+ toolCount: 1,
538
+ });
539
+ }
540
+ }
541
+
542
+ return [...servers.values()].sort((a, b) => {
543
+ const byName = a.serverName.localeCompare(b.serverName);
544
+ return byName !== 0 ? byName : a.serverId.localeCompare(b.serverId);
545
+ });
546
+ }
547
+
548
+ /** List tools deterministically, optionally scoped to a server. */
549
+ listTools(options: ToolSearchOptions & { limit?: number; cursor?: string } = {}): ToolListResult {
550
+ const offset = Math.max(Number(options.cursor) || 0, 0);
551
+ const limit = Math.max(Number(options.limit) || 20, 1);
552
+ const tools = [...this.toolSummaries.values()]
553
+ .filter((summary) => this.matchesServer(summary, options))
554
+ .sort((a, b) => {
555
+ const byServer = a.serverName.localeCompare(b.serverName);
556
+ if (byServer !== 0) return byServer;
557
+ return a.name.localeCompare(b.name);
558
+ });
559
+
560
+ const page = tools.slice(offset, offset + limit);
561
+ const nextOffset = offset + page.length;
562
+
563
+ return {
564
+ tools: page,
565
+ totalCount: tools.length,
566
+ returnedCount: page.length,
567
+ nextCursor: nextOffset < tools.length ? String(nextOffset) : undefined,
568
+ servers: this.listServers(options),
569
+ };
570
+ }
571
+
473
572
  /** Number of indexed tools (including duplicates). */
474
573
  get size(): number {
475
574
  let count = 0;
@@ -539,6 +638,23 @@ export class ToolIndex {
539
638
  return `${tool.sessionId}::${tool.serverId}::${tool.name}`;
540
639
  }
541
640
 
641
+ private matchesServer(summary: ToolSummary, options: ToolSearchOptions): boolean {
642
+ if (options.serverId && summary.serverId !== options.serverId) {
643
+ return false;
644
+ }
645
+
646
+ if (options.serverName) {
647
+ const serverNameQuery = options.serverName.toLowerCase();
648
+ const serverName = summary.serverName.toLowerCase();
649
+ const serverId = summary.serverId.toLowerCase();
650
+ if (!serverName.includes(serverNameQuery) && !serverId.includes(serverNameQuery)) {
651
+ return false;
652
+ }
653
+ }
654
+
655
+ return true;
656
+ }
657
+
542
658
  /** Simple whitespace + camelCase + snake_case tokenizer. */
543
659
  private tokenize(text: string): string[] {
544
660
  return text
@@ -28,10 +28,20 @@
28
28
 
29
29
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
30
30
  import type { ToolClient, ToolClientProvider } from './types.js';
31
- import { ToolIndex, type IndexedTool, type ToolSummary, type EmbedFn } from './tool-index.js';
31
+ import {
32
+ ToolIndex,
33
+ type IndexedTool,
34
+ type ToolLookupOptions,
35
+ type ToolListResult,
36
+ type ToolSearchOptions,
37
+ type ToolServerSummary,
38
+ type ToolSummary,
39
+ type EmbedFn,
40
+ } from './tool-index.js';
32
41
  import { SchemaCompressor, type CompactTool } from './schema-compressor.js';
33
42
  import {
34
43
  createSearchToolDefinition,
44
+ createListServersToolDefinition,
35
45
  createRegexSearchToolDefinition,
36
46
  createGetSchemaToolDefinition,
37
47
  createExecuteToolDefinition,
@@ -159,7 +169,7 @@ export class ToolRouter {
159
169
  * This is the main method adapters should call.
160
170
  *
161
171
  * - `all` → returns all tools (unchanged behavior)
162
- * - `search` → returns only meta-tools (mcp_search_tool_bm25, mcp_get_tool_schema, mcp_execute_tool)
172
+ * - `search` → returns only meta-tools (mcp_search_tools, mcp_get_tool_schema, mcp_execute_tool)
163
173
  * - `groups` → returns tools from active groups only
164
174
  */
165
175
  async getFilteredTools(): Promise<Tool[]> {
@@ -195,9 +205,14 @@ export class ToolRouter {
195
205
  * Search tools by natural-language query.
196
206
  * Works regardless of strategy.
197
207
  */
198
- async searchTools(query: string, topK?: number): Promise<ToolSummary[]> {
208
+ async searchTools(
209
+ query: string,
210
+ topK?: number,
211
+ options: ToolSearchOptions = {}
212
+ ): Promise<ToolSummary[]> {
199
213
  await this.ensureInitialized();
200
- return this.index.search(query, topK ?? this.maxTools);
214
+ const limit = topK ?? this.maxTools;
215
+ return this.index.search(query, limit, options);
201
216
  }
202
217
 
203
218
  /**
@@ -209,12 +224,28 @@ export class ToolRouter {
209
224
  return this.index.searchRegex(pattern, topK ?? this.maxTools);
210
225
  }
211
226
 
227
+ /** List connected MCP servers with indexed tool counts. */
228
+ async listServers(options: ToolSearchOptions = {}): Promise<ToolServerSummary[]> {
229
+ await this.ensureInitialized();
230
+ return this.index.listServers(options);
231
+ }
232
+
233
+ /** List tools deterministically, optionally scoped to a server. */
234
+ async listTools(options: ToolSearchOptions & { limit?: number; cursor?: string } = {}): Promise<ToolListResult> {
235
+ await this.ensureInitialized();
236
+ return this.index.listTools(options);
237
+ }
238
+
212
239
  /**
213
240
  * Get the full tool definition by name.
214
241
  * If tool name is ambiguous, use namespace to specify the server.
215
242
  */
216
- getToolSchema(toolName: string, namespace?: string): IndexedTool | undefined {
217
- const matches = this.index.getTool(toolName, namespace);
243
+ getToolSchema(
244
+ toolName: string,
245
+ namespace?: string,
246
+ options: ToolLookupOptions = {}
247
+ ): IndexedTool | undefined {
248
+ const matches = this.index.getTool(toolName, namespace, options);
218
249
 
219
250
  if (matches.length === 0) return undefined;
220
251
 
@@ -318,7 +349,7 @@ export class ToolRouter {
318
349
  throw new Error(
319
350
  `Tool "${toolName}" not found${
320
351
  namespace ? ` on server "${namespace}"` : ''
321
- }. Use mcp_search_tool_bm25 or mcp_search_tool_regex to discover available tools.`
352
+ }. Use mcp_search_tools or mcp_search_tool_regex to discover available tools.`
322
353
  );
323
354
  }
324
355
 
@@ -462,9 +493,11 @@ export class ToolRouter {
462
493
  private getMetaToolDefinitions(): Tool[] {
463
494
  return [
464
495
  createSearchToolDefinition(),
496
+ createListServersToolDefinition(),
465
497
  createRegexSearchToolDefinition(),
466
498
  createGetSchemaToolDefinition(),
467
499
  createExecuteToolDefinition(),
468
500
  ];
469
501
  }
502
+
470
503
  }
@@ -204,6 +204,7 @@ export interface ConnectParams {
204
204
  serverUrl: string;
205
205
  callbackUrl: string;
206
206
  transportType?: TransportType;
207
+ headers?: Record<string, string>;
207
208
  }
208
209
 
209
210
  export interface DisconnectParams {