@mcp-ts/sdk 1.5.1 → 1.5.3

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 (61) 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 +50 -10
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +50 -10
  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 +49 -9
  17. package/dist/adapters/ai-adapter.js.map +1 -1
  18. package/dist/adapters/ai-adapter.mjs +49 -9
  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 +49 -9
  23. package/dist/adapters/langchain-adapter.js.map +1 -1
  24. package/dist/adapters/langchain-adapter.mjs +49 -9
  25. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  26. package/dist/client/react.d.mts +10 -0
  27. package/dist/client/react.d.ts +10 -0
  28. package/dist/client/react.js +23 -0
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +23 -0
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +10 -0
  33. package/dist/client/vue.d.ts +10 -0
  34. package/dist/client/vue.js +17 -0
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +17 -0
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/index.d.mts +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +123 -28
  41. package/dist/index.js.map +1 -1
  42. package/dist/index.mjs +123 -28
  43. package/dist/index.mjs.map +1 -1
  44. package/dist/shared/index.d.mts +2 -2
  45. package/dist/shared/index.d.ts +2 -2
  46. package/dist/shared/index.js +123 -28
  47. package/dist/shared/index.js.map +1 -1
  48. package/dist/shared/index.mjs +123 -28
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-XnWVxPzv.d.mts → tool-router-DK0RJblO.d.mts} +3 -0
  51. package/dist/{tool-router-Bo8qZbsD.d.ts → tool-router-DsKhRmJm.d.ts} +3 -0
  52. package/package.json +3 -3
  53. package/src/adapters/agui-adapter.ts +7 -7
  54. package/src/adapters/agui-middleware.ts +163 -59
  55. package/src/adapters/ai-adapter.ts +5 -5
  56. package/src/adapters/langchain-adapter.ts +5 -5
  57. package/src/client/react/use-mcp.ts +48 -0
  58. package/src/client/vue/use-mcp.ts +42 -0
  59. package/src/shared/meta-tools.ts +73 -15
  60. package/src/shared/tool-index.ts +85 -12
  61. package/src/shared/tool-router.ts +8 -7
@@ -17,6 +17,7 @@
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
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Tool Definitions
@@ -33,16 +34,18 @@ export function createSearchToolDefinition(): Tool {
33
34
  return {
34
35
  name: 'mcp_search_tool_bm25',
35
36
  description:
36
- 'Search the catalog of available tools using BM25 natural language ranking. ' +
37
- 'Returns tool names, descriptions, and server info. ' +
38
- 'Use this FIRST to find relevant tools before calling them. ' +
39
- 'Example queries: "database query", "send email", "github pull request".',
37
+ 'Search the catalog of available tools. Returns tool names, descriptions, and server info. ' +
38
+ 'Use this FIRST to find relevant tools before calling them.\n\n' +
39
+ 'Query forms:\n' +
40
+ '- "select:Read,Edit,Grep" fetch these exact tools by name\n' +
41
+ '- "notebook jupyter" — keyword search, up to limit best matches\n' +
42
+ '- "+slack send" — require "slack" in the name, rank by remaining terms',
40
43
  inputSchema: {
41
44
  type: 'object' as const,
42
45
  properties: {
43
46
  query: {
44
47
  type: 'string',
45
- description: 'Natural language description of the capability you need.',
48
+ description: 'Query to find tools. Use "select:<tool_name>" for direct selection, or keywords to search. Prefix keywords with + to require them.',
46
49
  },
47
50
  limit: {
48
51
  type: 'number',
@@ -97,7 +100,8 @@ export function createGetSchemaToolDefinition(): Tool {
97
100
  description:
98
101
  'Get the full input schema (parameters) for a specific tool. ' +
99
102
  'Call this after mcp_search_tool_bm25 to get the parameter details ' +
100
- 'needed to call a tool correctly.',
103
+ 'needed to call a tool correctly. ' +
104
+ 'Do NOT call the discovered tool directly; after reading the schema, call mcp_execute_tool.',
101
105
  inputSchema: {
102
106
  type: 'object' as const,
103
107
  properties: {
@@ -105,10 +109,10 @@ export function createGetSchemaToolDefinition(): Tool {
105
109
  type: 'string',
106
110
  description: 'The exact tool name returned by mcp_search_tool_bm25.',
107
111
  },
108
- serverName: {
112
+ serverId: {
109
113
  type: 'string',
110
114
  description:
111
- 'Optional: The server name provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
115
+ 'Optional: The server ID provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
112
116
  },
113
117
  },
114
118
  required: ['toolName'],
@@ -141,10 +145,10 @@ export function createExecuteToolDefinition(): Tool {
141
145
  type: 'string',
142
146
  description: 'The exact tool name from mcp_search_tool_bm25 results.',
143
147
  },
144
- serverName: {
148
+ serverId: {
145
149
  type: 'string',
146
150
  description:
147
- 'Optional: The server name provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
151
+ 'Optional: The server ID provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
148
152
  },
149
153
  args: {
150
154
  type: 'object',
@@ -187,7 +191,7 @@ export async function executeMetaTool(
187
191
  router: ToolRouter,
188
192
  callToolFn?: CallToolFn
189
193
  ): Promise<CallToolResult | null> {
190
- const resolveToolSchema = (name: string, namespace?: string): { tool?: Tool; error?: CallToolResult } => {
194
+ const resolveToolSchema = (name: string, namespace?: string): { tool?: IndexedTool; error?: CallToolResult } => {
191
195
  try {
192
196
  return { tool: router.getToolSchema(name, namespace) };
193
197
  } catch (err) {
@@ -206,6 +210,53 @@ export async function executeMetaTool(
206
210
  const query = String(args.query ?? '');
207
211
  const limit = Math.min(Number(args.limit) || 5, 20);
208
212
 
213
+ // Fast path: Check for select: prefix
214
+ const selectMatch = query.match(/^select:(.+)$/i);
215
+ if (selectMatch) {
216
+ const requested = selectMatch[1]!
217
+ .split(',')
218
+ .map((s) => s.trim())
219
+ .filter(Boolean);
220
+
221
+ const found: any[] = [];
222
+ const errors: string[] = [];
223
+
224
+ for (const requestedToolName of requested) {
225
+ const { tool, error } = resolveToolSchema(requestedToolName);
226
+ if (error) {
227
+ const errorMsg = error.content[0]?.type === 'text' ? error.content[0].text : 'Unknown error';
228
+ errors.push(`- **${requestedToolName}**: ${errorMsg}`);
229
+ } else if (tool) {
230
+ found.push(tool);
231
+ } else {
232
+ errors.push(`- **${requestedToolName}**: Tool not found. Try searching with mcp_search_tool_bm25.`);
233
+ }
234
+ }
235
+
236
+ const lines: string[] = [];
237
+
238
+ if (found.length > 0) {
239
+ lines.push(...found.map((t, i) =>
240
+ `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n ${t.description}`
241
+ ));
242
+ }
243
+
244
+ if (errors.length > 0) {
245
+ if (lines.length > 0) lines.push(""); // Add empty line spacing
246
+ lines.push("Errors resolving some tools:");
247
+ lines.push(...errors);
248
+ }
249
+
250
+ const text = lines.length > 0
251
+ ? lines.join('\n')
252
+ : `No tools found matching select query: ${requested.join(', ')}`;
253
+
254
+ return {
255
+ content: [{ type: 'text', text }],
256
+ isError: found.length === 0,
257
+ };
258
+ }
259
+
209
260
  const results = await router.searchTools(query, limit);
210
261
 
211
262
  const text = results.length === 0
@@ -213,7 +264,7 @@ export async function executeMetaTool(
213
264
  : results
214
265
  .map(
215
266
  (t, i) =>
216
- `${i + 1}. **${t.name}** (server: ${t.serverName})\n` +
267
+ `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
217
268
  ` ${t.description}\n` +
218
269
  ` Estimated tokens: ${t.estimatedTokens}`
219
270
  )
@@ -236,7 +287,7 @@ export async function executeMetaTool(
236
287
  : results
237
288
  .map(
238
289
  (t, i) =>
239
- `${i + 1}. **${t.name}** (server: ${t.serverName})\n` +
290
+ `${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
240
291
  ` ${t.description}\n` +
241
292
  ` Estimated tokens: ${t.estimatedTokens}`
242
293
  )
@@ -250,7 +301,7 @@ export async function executeMetaTool(
250
301
 
251
302
  case 'mcp_get_tool_schema': {
252
303
  const name = String(args.toolName ?? '');
253
- const namespace = String(args.serverName ?? '') || undefined;
304
+ const namespace = String(args.serverId ?? '') || undefined;
254
305
  const { tool, error } = resolveToolSchema(name, namespace);
255
306
 
256
307
  if (error) {
@@ -273,6 +324,13 @@ export async function executeMetaTool(
273
324
  name: tool.name,
274
325
  description: tool.description,
275
326
  inputSchema: tool.inputSchema,
327
+ executionInstructions: {
328
+ nextTool: 'mcp_execute_tool',
329
+ toolName: tool.name,
330
+ serverId: tool.serverId,
331
+ note:
332
+ '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.',
333
+ },
276
334
  };
277
335
 
278
336
  return {
@@ -283,7 +341,7 @@ export async function executeMetaTool(
283
341
 
284
342
  case 'mcp_execute_tool': {
285
343
  const targetToolName = String(args.toolName ?? '');
286
- const namespace = String(args.serverName ?? '') || undefined;
344
+ const namespace = String(args.serverId ?? '') || undefined;
287
345
  const toolArgs = (args.args as Record<string, unknown>) ?? {};
288
346
 
289
347
  if (!targetToolName) {
@@ -24,6 +24,8 @@ export interface ToolSummary {
24
24
  description: string;
25
25
  /** Server that owns this tool */
26
26
  serverName: string;
27
+ /** Unique ID of the server */
28
+ serverId: string;
27
29
  /** Session the tool belongs to */
28
30
  sessionId: string;
29
31
  /** Estimated token cost of the full inputSchema */
@@ -33,6 +35,7 @@ export interface ToolSummary {
33
35
  /** A tool with routing metadata attached during indexing. */
34
36
  export interface IndexedTool extends Tool {
35
37
  sessionId: string;
38
+ serverId: string;
36
39
  serverName: string;
37
40
  }
38
41
 
@@ -177,6 +180,7 @@ export class ToolIndex {
177
180
  name: tool.name,
178
181
  description: tool.description ?? '',
179
182
  serverName: tool.serverName,
183
+ serverId: tool.serverId,
180
184
  sessionId: tool.sessionId,
181
185
  estimatedTokens,
182
186
  });
@@ -258,8 +262,55 @@ export class ToolIndex {
258
262
  async search(query: string, topK = 5): Promise<ToolSummary[]> {
259
263
  if (this.tools.size === 0) return [];
260
264
 
261
- const queryLower = query.toLowerCase();
262
- const queryTokens = this.tokenize(queryLower);
265
+ const queryLower = query.toLowerCase().trim();
266
+
267
+ // Fast path: Exact tool name match (supports duplicate names across servers)
268
+ const exactMatches = [...this.toolSummaries.values()].filter(
269
+ (summary) => summary.name.toLowerCase() === queryLower
270
+ );
271
+ if (exactMatches.length > 0) {
272
+ return exactMatches.slice(0, topK);
273
+ }
274
+
275
+ // Fast path: MCP prefix match (e.g. "mcp__github")
276
+ if (queryLower.startsWith('mcp__') && queryLower.length > 5) {
277
+ const prefixMatches = [...this.toolSummaries.values()]
278
+ .filter((t) => t.name.toLowerCase().startsWith(queryLower))
279
+ .slice(0, topK);
280
+ if (prefixMatches.length > 0) return prefixMatches;
281
+ }
282
+
283
+ const queryTermsRaw = queryLower.split(/\s+/).filter((t) => t.length > 0);
284
+ const requiredTerms: string[] = [];
285
+ const optionalTerms: string[] = [];
286
+
287
+ for (const term of queryTermsRaw) {
288
+ if (term.startsWith('+') && term.length > 1) {
289
+ requiredTerms.push(term.slice(1));
290
+ } else {
291
+ optionalTerms.push(term);
292
+ }
293
+ }
294
+
295
+ const allScoringTerms =
296
+ requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTermsRaw;
297
+ const normalizedQueryText = allScoringTerms.join(' ').trim();
298
+ const queryTokens = this.tokenize(allScoringTerms.join(' '));
299
+
300
+ // Pre-filter: only keep documents that contain ALL required terms
301
+ const candidateKeys = new Set<string>();
302
+ for (const docKey of this.toolSummaries.keys()) {
303
+ if (requiredTerms.length > 0) {
304
+ const text = this.searchTexts.get(docKey) || '';
305
+ const summary = this.toolSummaries.get(docKey)!;
306
+ const nameLower = summary.name.toLowerCase();
307
+ const matchesAll = requiredTerms.every(
308
+ (term) => text.includes(term) || nameLower.includes(term)
309
+ );
310
+ if (!matchesAll) continue;
311
+ }
312
+ candidateKeys.add(docKey);
313
+ }
263
314
 
264
315
  // 1. Keyword scores (BM25)
265
316
  const keywordScores = new Map<string, number>();
@@ -267,7 +318,12 @@ export class ToolIndex {
267
318
  const k1 = 1.2;
268
319
  const b = 0.75;
269
320
 
270
- for (const [docKey, docTf] of this.tfVectors) {
321
+ for (const docKey of candidateKeys) {
322
+ const docTf = this.tfVectors.get(docKey);
323
+ if (!docTf) continue;
324
+
325
+ const summary = this.toolSummaries.get(docKey)!;
326
+
271
327
  let score = 0;
272
328
  const docLen = this.docLengths.get(docKey) ?? 0;
273
329
 
@@ -276,16 +332,30 @@ export class ToolIndex {
276
332
  if (tfVal === 0) continue;
277
333
 
278
334
  const idf = this.idf.get(tok) ?? 0;
279
-
280
335
  // BM25 formula:
281
336
  // score = idf * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLen / avgDocLength)))
282
337
  const numerator = tfVal * (k1 + 1);
283
338
  const denominator = tfVal + k1 * (1 - b + b * (docLen / this.avgDocLength));
284
-
339
+
285
340
  score += idf * (numerator / denominator);
286
341
  }
287
342
 
288
- keywordScores.set(docKey, score);
343
+ // Name heuristics: give massive boosts for exact server/tool name matches
344
+ const serverLower = (summary.serverName || summary.serverId || '').toLowerCase();
345
+ const toolLower = summary.name.toLowerCase();
346
+
347
+ for (const term of allScoringTerms) {
348
+ if (serverLower.includes(term)) {
349
+ score += 10;
350
+ }
351
+ if (toolLower.includes(term)) {
352
+ score += 5;
353
+ }
354
+ }
355
+
356
+ if (score > 0) {
357
+ keywordScores.set(docKey, score);
358
+ }
289
359
  }
290
360
 
291
361
  // 2. Embedding scores (optional)
@@ -293,11 +363,14 @@ export class ToolIndex {
293
363
 
294
364
  if (this.options.embedFn && this.embeddings.size > 0) {
295
365
  try {
296
- const [queryEmbedding] = await this.options.embedFn([queryLower]);
366
+ const [queryEmbedding] = await this.options.embedFn([normalizedQueryText]);
297
367
  if (queryEmbedding) {
298
368
  embeddingScores = new Map();
299
- for (const [docKey, vec] of this.embeddings) {
300
- embeddingScores.set(docKey, this.cosineSimilarity(queryEmbedding, vec));
369
+ for (const docKey of candidateKeys) {
370
+ const vec = this.embeddings.get(docKey);
371
+ if (vec) {
372
+ embeddingScores.set(docKey, this.cosineSimilarity(queryEmbedding, vec));
373
+ }
301
374
  }
302
375
  }
303
376
  } catch {
@@ -309,7 +382,7 @@ export class ToolIndex {
309
382
  const kw = this.options.keywordWeight;
310
383
  const finalScores: Array<{ docKey: string; score: number }> = [];
311
384
 
312
- for (const docKey of this.toolSummaries.keys()) {
385
+ for (const docKey of candidateKeys) {
313
386
  const kwScore = keywordScores.get(docKey) ?? 0;
314
387
  const embScore = embeddingScores?.get(docKey) ?? 0;
315
388
 
@@ -389,7 +462,7 @@ export class ToolIndex {
389
462
  const list = this.tools.get(name) ?? [];
390
463
  if (!namespace) return list;
391
464
 
392
- return list.filter((t) => t.sessionId === namespace || t.serverName === namespace);
465
+ return list.filter((t) => t.sessionId === namespace || t.serverId === namespace);
393
466
  }
394
467
 
395
468
  /** All indexed tool names. */
@@ -463,7 +536,7 @@ export class ToolIndex {
463
536
  }
464
537
 
465
538
  private getDocumentKey(tool: IndexedTool): string {
466
- return `${tool.sessionId}::${tool.serverName}::${tool.name}`;
539
+ return `${tool.sessionId}::${tool.serverId}::${tool.name}`;
467
540
  }
468
541
 
469
542
  /** Simple whitespace + camelCase + snake_case tokenizer. */
@@ -219,10 +219,10 @@ export class ToolRouter {
219
219
  if (matches.length === 0) return undefined;
220
220
 
221
221
  if (matches.length > 1) {
222
- const servers = matches.map((m) => m.serverName).join(', ');
222
+ const servers = matches.map((m) => m.serverId).join(', ');
223
223
  throw new Error(
224
224
  `Tool "${toolName}" is provided by multiple servers: [${servers}]. ` +
225
- `Please specify the desired "serverName" as a namespace.`
225
+ `Please specify the desired "serverId" as a namespace.`
226
226
  );
227
227
  }
228
228
 
@@ -372,6 +372,7 @@ export class ToolRouter {
372
372
  for (const tool of tools) {
373
373
  result.push({
374
374
  ...tool,
375
+ serverId,
375
376
  serverName: serverName,
376
377
  sessionId,
377
378
  });
@@ -409,20 +410,20 @@ export class ToolRouter {
409
410
  });
410
411
  }
411
412
  } else {
412
- // Auto-group by server name
413
+ // Auto-group by server ID
413
414
  const serverTools = new Map<string, string[]>();
414
415
  for (const tool of this.allTools) {
415
- const group = tool.serverName;
416
+ const group = tool.serverId;
416
417
  if (!serverTools.has(group)) {
417
418
  serverTools.set(group, []);
418
419
  }
419
420
  serverTools.get(group)!.push(tool.name);
420
421
  }
421
422
 
422
- for (const [serverName, tools] of serverTools) {
423
- this.groupsMap.set(serverName, {
423
+ for (const [serverId, tools] of serverTools) {
424
+ this.groupsMap.set(serverId, {
424
425
  tools,
425
- active: this.activeGroups.size === 0 || this.activeGroups.has(serverName),
426
+ active: this.activeGroups.size === 0 || this.activeGroups.has(serverId),
426
427
  });
427
428
  }
428
429
  }