@mcp-ts/sdk 1.5.2 → 1.6.0

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 (52) hide show
  1. package/README.md +89 -27
  2. package/dist/adapters/agui-adapter.d.mts +1 -1
  3. package/dist/adapters/agui-adapter.d.ts +1 -1
  4. package/dist/adapters/agui-adapter.js +76 -19
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +76 -19
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +5 -1
  9. package/dist/adapters/agui-middleware.d.ts +5 -1
  10. package/dist/adapters/agui-middleware.js +116 -49
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +117 -50
  13. package/dist/adapters/agui-middleware.mjs.map +1 -1
  14. package/dist/adapters/ai-adapter.d.mts +1 -1
  15. package/dist/adapters/ai-adapter.d.ts +1 -1
  16. package/dist/adapters/ai-adapter.js +76 -19
  17. package/dist/adapters/ai-adapter.js.map +1 -1
  18. package/dist/adapters/ai-adapter.mjs +76 -19
  19. package/dist/adapters/ai-adapter.mjs.map +1 -1
  20. package/dist/adapters/langchain-adapter.d.mts +1 -1
  21. package/dist/adapters/langchain-adapter.d.ts +1 -1
  22. package/dist/adapters/langchain-adapter.js +76 -19
  23. package/dist/adapters/langchain-adapter.js.map +1 -1
  24. package/dist/adapters/langchain-adapter.mjs +76 -19
  25. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  26. package/dist/client/react.js.map +1 -1
  27. package/dist/client/react.mjs.map +1 -1
  28. package/dist/index.d.mts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +207 -43
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +207 -44
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/server/index.js +1 -3
  35. package/dist/server/index.js.map +1 -1
  36. package/dist/server/index.mjs +1 -3
  37. package/dist/server/index.mjs.map +1 -1
  38. package/dist/shared/index.d.mts +15 -8
  39. package/dist/shared/index.d.ts +15 -8
  40. package/dist/shared/index.js +206 -40
  41. package/dist/shared/index.js.map +1 -1
  42. package/dist/shared/index.mjs +206 -41
  43. package/dist/shared/index.mjs.map +1 -1
  44. package/dist/{tool-router-DsKhRmJm.d.ts → tool-router-Bn9R0KWr.d.ts} +56 -7
  45. package/dist/{tool-router-DK0RJblO.d.mts → tool-router-_O2tIwf7.d.mts} +56 -7
  46. package/package.json +5 -3
  47. package/src/adapters/agui-middleware.ts +163 -59
  48. package/src/server/mcp/oauth-client.ts +4 -4
  49. package/src/shared/index.ts +4 -0
  50. package/src/shared/meta-tools.ts +172 -37
  51. package/src/shared/tool-index.ts +123 -7
  52. package/src/shared/tool-router.ts +40 -7
@@ -1126,9 +1126,10 @@ export class MCPClient {
1126
1126
  * @returns Server name or undefined
1127
1127
  */
1128
1128
  getServerName(): string | undefined {
1129
- const info = (this.client as any)?.getServerVersion();
1130
- console.log('server info ->', info);
1131
- return info?.title ?? info?.name ?? this.serverName;
1129
+ // Temporarily avoid deriving serverName from serverVersion metadata.
1130
+ // const info = (this.client as any)?.getServerVersion();
1131
+ // return info?.title ?? info?.name ?? this.serverName;
1132
+ return this.serverName;
1132
1133
  }
1133
1134
 
1134
1135
  /**
@@ -1228,4 +1229,3 @@ export class MCPClient {
1228
1229
  }
1229
1230
 
1230
1231
  }
1231
-
@@ -87,6 +87,9 @@ export {
87
87
  export {
88
88
  ToolIndex,
89
89
  type ToolSummary,
90
+ type ToolServerSummary,
91
+ type ToolSearchOptions,
92
+ type ToolListResult,
90
93
  type IndexedTool,
91
94
  type ToolIndexOptions,
92
95
  type EmbedFn,
@@ -100,6 +103,7 @@ export {
100
103
 
101
104
  export {
102
105
  createSearchToolDefinition,
106
+ createListServersToolDefinition,
103
107
  createRegexSearchToolDefinition,
104
108
  createGetSchemaToolDefinition,
105
109
  createExecuteToolDefinition,
@@ -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,13 +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, ToolLookupOptions } from './tool-index.js';
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Tool Definitions
23
24
  // ---------------------------------------------------------------------------
24
25
 
25
26
  /**
26
- * Creates the `mcp_search_tool_bm25` tool definition.
27
+ * Creates the `mcp_search_tools` tool definition.
27
28
  *
28
29
  * This tool lets the LLM search the full catalog of available MCP tools
29
30
  * using a BM25 natural-language query. Returns tool names and descriptions
@@ -31,7 +32,7 @@ import type { ToolRouter } from './tool-router.js';
31
32
  */
32
33
  export function createSearchToolDefinition(): Tool {
33
34
  return {
34
- name: 'mcp_search_tool_bm25',
35
+ name: 'mcp_search_tools',
35
36
  description:
36
37
  'Search the catalog of available tools. Returns tool names, descriptions, and server info. ' +
37
38
  'Use this FIRST to find relevant tools before calling them.\n\n' +
@@ -46,9 +47,28 @@ export function createSearchToolDefinition(): Tool {
46
47
  type: 'string',
47
48
  description: 'Query to find tools. Use "select:<tool_name>" for direct selection, or keywords to search. Prefix keywords with + to require them.',
48
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
+ },
49
65
  limit: {
50
66
  type: 'number',
51
- 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".',
52
72
  },
53
73
  },
54
74
  required: ['query'],
@@ -56,6 +76,31 @@ export function createSearchToolDefinition(): Tool {
56
76
  };
57
77
  }
58
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
+
59
104
  /**
60
105
  * Creates the `mcp_search_tool_regex` tool definition.
61
106
  *
@@ -88,7 +133,7 @@ export function createRegexSearchToolDefinition(): Tool {
88
133
  /**
89
134
  * Creates the `mcp_get_tool_schema` tool definition.
90
135
  *
91
- * After discovering tools via `mcp_search_tool_bm25` or
136
+ * After discovering tools via `mcp_search_tools` or
92
137
  * `mcp_search_tool_regex`, the LLM calls this to load the full
93
138
  * inputSchema for a specific tool so it can construct the correct
94
139
  * arguments.
@@ -98,19 +143,20 @@ export function createGetSchemaToolDefinition(): Tool {
98
143
  name: 'mcp_get_tool_schema',
99
144
  description:
100
145
  'Get the full input schema (parameters) for a specific tool. ' +
101
- 'Call this after mcp_search_tool_bm25 to get the parameter details ' +
102
- 'needed to call a tool correctly.',
146
+ 'Call this after mcp_search_tools to get the parameter details ' +
147
+ 'needed to call a tool correctly. ' +
148
+ 'Do NOT call the discovered tool directly; after reading the schema, call mcp_execute_tool.',
103
149
  inputSchema: {
104
150
  type: 'object' as const,
105
151
  properties: {
106
152
  toolName: {
107
153
  type: 'string',
108
- description: 'The exact tool name returned by mcp_search_tool_bm25.',
154
+ description: 'The exact tool name returned by mcp_search_tools.',
109
155
  },
110
156
  serverId: {
111
157
  type: 'string',
112
158
  description:
113
- '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.',
114
160
  },
115
161
  },
116
162
  required: ['toolName'],
@@ -122,7 +168,7 @@ export function createGetSchemaToolDefinition(): Tool {
122
168
  * Creates the `mcp_execute_tool` tool definition.
123
169
  *
124
170
  * This is the execution meta-tool — the LLM calls this to execute any
125
- * tool discovered via `mcp_search_tool_bm25` or `mcp_search_tool_regex`.
171
+ * tool discovered via `mcp_search_tools` or `mcp_search_tool_regex`.
126
172
  * The LLM should first call `mcp_get_tool_schema` to know the correct
127
173
  * arguments.
128
174
  *
@@ -133,7 +179,7 @@ export function createExecuteToolDefinition(): Tool {
133
179
  return {
134
180
  name: 'mcp_execute_tool',
135
181
  description:
136
- 'Execute a tool that was discovered via mcp_search_tool_bm25. ' +
182
+ 'Execute a tool that was discovered via mcp_search_tools. ' +
137
183
  'You MUST call mcp_get_tool_schema first to know the correct parameters. ' +
138
184
  'Pass the exact tool name and its arguments.',
139
185
  inputSchema: {
@@ -141,12 +187,12 @@ export function createExecuteToolDefinition(): Tool {
141
187
  properties: {
142
188
  toolName: {
143
189
  type: 'string',
144
- description: 'The exact tool name from mcp_search_tool_bm25 results.',
190
+ description: 'The exact tool name from mcp_search_tools results.',
145
191
  },
146
192
  serverId: {
147
193
  type: 'string',
148
194
  description:
149
- '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.',
150
196
  },
151
197
  args: {
152
198
  type: 'object',
@@ -177,7 +223,7 @@ export type CallToolFn = (
177
223
  /**
178
224
  * Execute a meta-tool call and return the result in MCP CallToolResult format.
179
225
  *
180
- * @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.)
181
227
  * @param args - The arguments from the LLM's tool call
182
228
  * @param router - The ToolRouter to query
183
229
  * @param callToolFn - Optional callback for executing real tools (required for mcp_execute_tool)
@@ -189,9 +235,13 @@ export async function executeMetaTool(
189
235
  router: ToolRouter,
190
236
  callToolFn?: CallToolFn
191
237
  ): Promise<CallToolResult | null> {
192
- const resolveToolSchema = (name: string, namespace?: string): { tool?: Tool; error?: CallToolResult } => {
238
+ const resolveToolSchema = (
239
+ name: string,
240
+ namespace?: string,
241
+ options?: ToolLookupOptions
242
+ ): { tool?: IndexedTool; error?: CallToolResult } => {
193
243
  try {
194
- return { tool: router.getToolSchema(name, namespace) };
244
+ return { tool: router.getToolSchema(name, namespace, options) };
195
245
  } catch (err) {
196
246
  const errorMessage = err instanceof Error ? err.message : String(err);
197
247
  return {
@@ -204,13 +254,61 @@ export async function executeMetaTool(
204
254
  };
205
255
 
206
256
  switch (toolName) {
207
- case 'mcp_search_tool_bm25': {
257
+ case 'mcp_search_tools': {
208
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
+
209
304
  const limit = Math.min(Number(args.limit) || 5, 20);
305
+ const searchOptions = { serverId, serverName };
210
306
 
211
307
  // Fast path: Check for select: prefix
212
308
  const selectMatch = query.match(/^select:(.+)$/i);
213
309
  if (selectMatch) {
310
+ await router.listTools({ serverId, serverName, limit: 1 });
311
+
214
312
  const requested = selectMatch[1]!
215
313
  .split(',')
216
314
  .map((s) => s.trim())
@@ -219,15 +317,19 @@ export async function executeMetaTool(
219
317
  const found: any[] = [];
220
318
  const errors: string[] = [];
221
319
 
320
+ const namespace = serverId ?? serverName;
321
+
222
322
  for (const requestedToolName of requested) {
223
- const { tool, error } = resolveToolSchema(requestedToolName);
323
+ const { tool, error } = resolveToolSchema(requestedToolName, namespace, {
324
+ allowServerNameFragment: Boolean(serverName && !serverId),
325
+ });
224
326
  if (error) {
225
327
  const errorMsg = error.content[0]?.type === 'text' ? error.content[0].text : 'Unknown error';
226
328
  errors.push(`- **${requestedToolName}**: ${errorMsg}`);
227
329
  } else if (tool) {
228
330
  found.push(tool);
229
331
  } else {
230
- 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.`);
231
333
  }
232
334
  }
233
335
 
@@ -255,16 +357,31 @@ export async function executeMetaTool(
255
357
  };
256
358
  }
257
359
 
258
- const results = await router.searchTools(query, limit);
360
+ const results = await router.searchTools(query, limit, searchOptions);
259
361
 
260
362
  const text = results.length === 0
261
- ? 'No tools found matching your query. Try different keywords.'
262
- : 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
263
381
  .map(
264
- (t, i) =>
265
- `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
266
- ` ${t.description}\n` +
267
- ` Estimated tokens: ${t.estimatedTokens}`
382
+ (server, i) =>
383
+ `${i + 1}. **${server.serverName}** (serverId: ${server.serverId}, sessionId: ${server.sessionId})\n` +
384
+ ` Tool count: ${server.toolCount}`
268
385
  )
269
386
  .join('\n');
270
387
 
@@ -282,14 +399,7 @@ export async function executeMetaTool(
282
399
 
283
400
  const text = results.length === 0
284
401
  ? 'No tools matched your regex pattern. Try a broader pattern.'
285
- : results
286
- .map(
287
- (t, i) =>
288
- `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
289
- ` ${t.description}\n` +
290
- ` Estimated tokens: ${t.estimatedTokens}`
291
- )
292
- .join('\n');
402
+ : formatToolSummaries(results).join('\n');
293
403
 
294
404
  return {
295
405
  content: [{ type: 'text', text }],
@@ -311,7 +421,7 @@ export async function executeMetaTool(
311
421
  content: [
312
422
  {
313
423
  type: 'text',
314
- 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.`,
315
425
  },
316
426
  ],
317
427
  isError: true,
@@ -322,6 +432,13 @@ export async function executeMetaTool(
322
432
  name: tool.name,
323
433
  description: tool.description,
324
434
  inputSchema: tool.inputSchema,
435
+ executionInstructions: {
436
+ nextTool: 'mcp_execute_tool',
437
+ toolName: tool.name,
438
+ serverId: tool.serverId,
439
+ note:
440
+ 'Do not call this discovered tool directly unless it was explicitly registered as a runtime tool. Execute it via mcp_execute_tool and pass these parameters inside args.',
441
+ },
325
442
  };
326
443
 
327
444
  return {
@@ -353,7 +470,7 @@ export async function executeMetaTool(
353
470
  content: [
354
471
  {
355
472
  type: 'text',
356
- 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.`,
357
474
  },
358
475
  ],
359
476
  isError: true,
@@ -395,10 +512,28 @@ export async function executeMetaTool(
395
512
  }
396
513
  }
397
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
+
398
532
  /** Check if a tool name is one of the meta-tools. */
399
533
  export function isMetaTool(toolName: string): boolean {
400
534
  return (
401
- toolName === 'mcp_search_tool_bm25' ||
535
+ toolName === 'mcp_search_tools' ||
536
+ toolName === 'mcp_list_servers' ||
402
537
  toolName === 'mcp_search_tool_regex' ||
403
538
  toolName === 'mcp_get_tool_schema' ||
404
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