@mcp-ts/sdk 1.4.0 → 1.5.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 (71) hide show
  1. package/README.md +20 -27
  2. package/dist/adapters/agui-adapter.d.mts +16 -0
  3. package/dist/adapters/agui-adapter.d.ts +16 -0
  4. package/dist/adapters/agui-adapter.js +185 -0
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +185 -0
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +2 -0
  9. package/dist/adapters/agui-middleware.d.ts +2 -0
  10. package/dist/adapters/agui-middleware.js.map +1 -1
  11. package/dist/adapters/agui-middleware.mjs.map +1 -1
  12. package/dist/adapters/ai-adapter.d.mts +21 -0
  13. package/dist/adapters/ai-adapter.d.ts +21 -0
  14. package/dist/adapters/ai-adapter.js +175 -0
  15. package/dist/adapters/ai-adapter.js.map +1 -1
  16. package/dist/adapters/ai-adapter.mjs +175 -0
  17. package/dist/adapters/ai-adapter.mjs.map +1 -1
  18. package/dist/adapters/langchain-adapter.d.mts +16 -0
  19. package/dist/adapters/langchain-adapter.d.ts +16 -0
  20. package/dist/adapters/langchain-adapter.js +179 -0
  21. package/dist/adapters/langchain-adapter.js.map +1 -1
  22. package/dist/adapters/langchain-adapter.mjs +179 -0
  23. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  24. package/dist/client/index.d.mts +2 -2
  25. package/dist/client/index.d.ts +2 -2
  26. package/dist/client/react.d.mts +14 -7
  27. package/dist/client/react.d.ts +14 -7
  28. package/dist/client/react.js +48 -23
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +47 -24
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +4 -4
  33. package/dist/client/vue.d.ts +4 -4
  34. package/dist/{index-CQr9q0bF.d.mts → index-DcYfpY3H.d.mts} +1 -1
  35. package/dist/{index-nE_7Io0I.d.ts → index-GfC_eNEv.d.ts} +1 -1
  36. package/dist/index.d.mts +4 -3
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +883 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +868 -2
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/server/index.d.mts +2 -2
  43. package/dist/server/index.d.ts +2 -2
  44. package/dist/server/index.js +3 -1
  45. package/dist/server/index.js.map +1 -1
  46. package/dist/server/index.mjs +3 -1
  47. package/dist/server/index.mjs.map +1 -1
  48. package/dist/shared/index.d.mts +86 -4
  49. package/dist/shared/index.d.ts +86 -4
  50. package/dist/shared/index.js +874 -0
  51. package/dist/shared/index.js.map +1 -1
  52. package/dist/shared/index.mjs +865 -1
  53. package/dist/shared/index.mjs.map +1 -1
  54. package/dist/tool-router-Bo8qZbsD.d.ts +325 -0
  55. package/dist/tool-router-XnWVxPzv.d.mts +325 -0
  56. package/dist/{types-CW6lghof.d.mts → types-CfCoIsWI.d.mts} +27 -1
  57. package/dist/{types-CW6lghof.d.ts → types-CfCoIsWI.d.ts} +27 -1
  58. package/package.json +3 -2
  59. package/src/adapters/agui-adapter.ts +79 -0
  60. package/src/adapters/ai-adapter.ts +75 -0
  61. package/src/adapters/langchain-adapter.ts +74 -0
  62. package/src/client/react/index.ts +2 -0
  63. package/src/client/react/use-mcp-apps.tsx +50 -32
  64. package/src/server/index.ts +2 -0
  65. package/src/server/mcp/oauth-client.ts +3 -1
  66. package/src/shared/index.ts +36 -0
  67. package/src/shared/meta-tools.ts +387 -0
  68. package/src/shared/schema-compressor.ts +124 -0
  69. package/src/shared/tool-index.ts +499 -0
  70. package/src/shared/tool-router.ts +469 -0
  71. package/src/shared/types.ts +30 -0
@@ -0,0 +1,499 @@
1
+ /**
2
+ * ToolIndex — Lightweight in-memory search index for MCP tool discovery.
3
+ *
4
+ * Supports two search methods:
5
+ * • BM25 – Okapi BM25 ranking over tokenized tool metadata (zero external deps)
6
+ * • regex – Pattern matching against tool names, descriptions, and parameters
7
+ * • embedding – (optional) cosine-similarity over caller-supplied vectors,
8
+ * blended with BM25 scores
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Public Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Compact summary returned by search — intentionally lightweight. */
20
+ export interface ToolSummary {
21
+ /** Fully qualified tool name (e.g. "tool_github_create_pr") */
22
+ name: string;
23
+ /** Human-readable description */
24
+ description: string;
25
+ /** Server that owns this tool */
26
+ serverName: string;
27
+ /** Session the tool belongs to */
28
+ sessionId: string;
29
+ /** Estimated token cost of the full inputSchema */
30
+ estimatedTokens: number;
31
+ }
32
+
33
+ /** A tool with routing metadata attached during indexing. */
34
+ export interface IndexedTool extends Tool {
35
+ sessionId: string;
36
+ serverName: string;
37
+ }
38
+
39
+ /**
40
+ * An optional embedding function supplied by the consumer.
41
+ * Should accept an array of strings and return a matching array of
42
+ * float-number arrays (one embedding vector per input string).
43
+ */
44
+ export type EmbedFn = (texts: string[]) => Promise<number[][]>;
45
+
46
+ export interface ToolIndexOptions {
47
+ /**
48
+ * Custom embedding function for semantic search.
49
+ * When provided, `search()` uses cosine-similarity in addition to keywords.
50
+ * @example
51
+ * ```ts
52
+ * import { embed } from 'ai';
53
+ * const embedFn: EmbedFn = async (texts) => {
54
+ * const { embeddings } = await embed({ model: openai('text-embedding-3-small'), values: texts });
55
+ * return embeddings;
56
+ * };
57
+ * ```
58
+ */
59
+ embedFn?: EmbedFn;
60
+
61
+ /**
62
+ * Relative weight of keyword score vs embedding score when both are active.
63
+ * 0 = embedding only · 1 = keyword only · 0.4 (default) blends both.
64
+ * @default 0.4
65
+ */
66
+ keywordWeight?: number;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Token Estimation
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Character-class weights for accurate-ish token estimation without a real
75
+ * tokenizer. Empirically calibrated against cl100k_base on typical JSON
76
+ * Schema payloads.
77
+ *
78
+ * | Char class | Approx chars per token |
79
+ * |--------------------|------------------------|
80
+ * | Whitespace / punct | 1–2 |
81
+ * | English words | ~4 |
82
+ * | JSON keys/values | ~3.5 |
83
+ *
84
+ * We walk the string once and accumulate a weighted character count, then
85
+ * divide by a calibrated divisor.
86
+ */
87
+ const CALIBRATION_DIVISOR = 3.6;
88
+
89
+ function classifyChar(ch: string): number {
90
+ const code = ch.charCodeAt(0);
91
+ // whitespace / common JSON structural chars → high token density
92
+ if (code <= 0x20 || ch === '{' || ch === '}' || ch === '[' || ch === ']' || ch === ':' || ch === ',') return 1.0;
93
+ // digits and symbols
94
+ if (code >= 0x21 && code <= 0x2f) return 1.5;
95
+ if (code >= 0x30 && code <= 0x39) return 2.0;
96
+ // uppercase (often JSON keys)
97
+ if (code >= 0x41 && code <= 0x5a) return 3.5;
98
+ // lowercase (natural language in descriptions)
99
+ if (code >= 0x61 && code <= 0x7a) return 4.0;
100
+ // everything else (unicode, emojis, etc.)
101
+ return 2.5;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // ToolIndex
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export class ToolIndex {
109
+ /** All indexed tools keyed by name (supports duplicates). */
110
+ private tools = new Map<string, IndexedTool[]>();
111
+
112
+ /** Precomputed lightweight summaries keyed by document. */
113
+ private toolSummaries = new Map<string, ToolSummary>();
114
+
115
+ /** Pre-computed search text for keyword matching (lowercase), keyed by document. */
116
+ private searchTexts = new Map<string, string>();
117
+
118
+ /** Pre-computed IDF values per token (computed once on build). */
119
+ private idf = new Map<string, number>();
120
+
121
+ /** Per-tool TF vectors (Map<token, tf>). */
122
+ private tfVectors = new Map<string, Map<string, number>>();
123
+
124
+ /** Optional: pre-computed embedding vectors per tool. */
125
+ private embeddings = new Map<string, number[]>();
126
+
127
+ /** BM25: document lengths in tokens for each tool. */
128
+ private docLengths = new Map<string, number>();
129
+
130
+ /** BM25: average document length across the entire index. */
131
+ private avgDocLength = 0;
132
+
133
+ /** Cached total estimated token cost across all indexed tools. */
134
+ private totalTokenCost = 0;
135
+
136
+ private options: Required<ToolIndexOptions>;
137
+
138
+ constructor(options: ToolIndexOptions = {}) {
139
+ this.options = {
140
+ embedFn: options.embedFn ?? (undefined as unknown as EmbedFn),
141
+ keywordWeight: options.keywordWeight ?? 0.4,
142
+ };
143
+ }
144
+
145
+ // -----------------------------------------------------------------------
146
+ // Indexing
147
+ // -----------------------------------------------------------------------
148
+
149
+ /**
150
+ * Build (or rebuild) the index from the given tool set.
151
+ * Call this after connecting / reconnecting to MCP servers.
152
+ */
153
+ async buildIndex(tools: IndexedTool[]): Promise<void> {
154
+ this.tools.clear();
155
+ this.toolSummaries.clear();
156
+ this.searchTexts.clear();
157
+ this.idf.clear();
158
+ this.tfVectors.clear();
159
+ this.embeddings.clear();
160
+ this.docLengths.clear();
161
+ this.avgDocLength = 0;
162
+ this.totalTokenCost = 0;
163
+
164
+ // 1. Populate tool map + search text
165
+ const allTokenSets: Map<string, Set<string>> = new Map();
166
+ let totalLength = 0;
167
+
168
+ for (const tool of tools) {
169
+ const docKey = this.getDocumentKey(tool);
170
+
171
+ if (!this.tools.has(tool.name)) {
172
+ this.tools.set(tool.name, []);
173
+ }
174
+ this.tools.get(tool.name)!.push(tool);
175
+ const estimatedTokens = ToolIndex.estimateTokens(tool);
176
+ this.toolSummaries.set(docKey, {
177
+ name: tool.name,
178
+ description: tool.description ?? '',
179
+ serverName: tool.serverName,
180
+ sessionId: tool.sessionId,
181
+ estimatedTokens,
182
+ });
183
+ this.totalTokenCost += estimatedTokens;
184
+
185
+ const text = this.buildSearchableText(tool).toLowerCase();
186
+ this.searchTexts.set(docKey, text);
187
+
188
+ const tokens = this.tokenize(text);
189
+ const tf = new Map<string, number>();
190
+ const uniqueTokens = new Set<string>();
191
+
192
+ for (const tok of tokens) {
193
+ tf.set(tok, (tf.get(tok) ?? 0) + 1);
194
+ uniqueTokens.add(tok);
195
+ }
196
+
197
+ // Normalize TF
198
+ const maxTf = Math.max(...tf.values(), 1);
199
+ for (const [k, v] of tf) {
200
+ tf.set(k, v / maxTf);
201
+ }
202
+
203
+ this.tfVectors.set(docKey, tf);
204
+ allTokenSets.set(docKey, uniqueTokens);
205
+
206
+ const length = tokens.length;
207
+ this.docLengths.set(docKey, length);
208
+ totalLength += length;
209
+ }
210
+
211
+ // Compute average document length
212
+ this.avgDocLength = totalLength / (tools.length || 1);
213
+
214
+ // 2. Compute IDF
215
+ const totalDocs = tools.length || 1;
216
+ const dfCounts = new Map<string, number>();
217
+
218
+ for (const tokenSet of allTokenSets.values()) {
219
+ for (const tok of tokenSet) {
220
+ dfCounts.set(tok, (dfCounts.get(tok) ?? 0) + 1);
221
+ }
222
+ }
223
+
224
+ for (const [tok, df] of dfCounts) {
225
+ this.idf.set(tok, Math.log(totalDocs / df) + 1);
226
+ }
227
+
228
+ // 3. Build embeddings if an embedFn was provided
229
+ if (this.options.embedFn) {
230
+ const names = [...this.searchTexts.keys()];
231
+ const texts = names.map((n) => this.searchTexts.get(n)!);
232
+
233
+ try {
234
+ const vectors = await this.options.embedFn(texts);
235
+ for (let i = 0; i < names.length; i++) {
236
+ if (vectors[i]) {
237
+ this.embeddings.set(names[i], vectors[i]);
238
+ }
239
+ }
240
+ } catch (err) {
241
+ console.warn('[ToolIndex] Embedding generation failed, falling back to keyword-only search:', err);
242
+ }
243
+ }
244
+ }
245
+
246
+ // -----------------------------------------------------------------------
247
+ // Search
248
+ // -----------------------------------------------------------------------
249
+
250
+ /**
251
+ * Search the index and return the top-K most relevant tools.
252
+ *
253
+ * When an `embedFn` is configured the final score is a weighted blend of
254
+ * keyword TF-IDF similarity and embedding cosine-similarity:
255
+ *
256
+ * `score = keywordWeight × keyword_score + (1 - keywordWeight) × cosine_score`
257
+ */
258
+ async search(query: string, topK = 5): Promise<ToolSummary[]> {
259
+ if (this.tools.size === 0) return [];
260
+
261
+ const queryLower = query.toLowerCase();
262
+ const queryTokens = this.tokenize(queryLower);
263
+
264
+ // 1. Keyword scores (BM25)
265
+ const keywordScores = new Map<string, number>();
266
+
267
+ const k1 = 1.2;
268
+ const b = 0.75;
269
+
270
+ for (const [docKey, docTf] of this.tfVectors) {
271
+ let score = 0;
272
+ const docLen = this.docLengths.get(docKey) ?? 0;
273
+
274
+ for (const tok of queryTokens) {
275
+ const tfVal = docTf.get(tok) ?? 0;
276
+ if (tfVal === 0) continue;
277
+
278
+ const idf = this.idf.get(tok) ?? 0;
279
+
280
+ // BM25 formula:
281
+ // score = idf * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (docLen / avgDocLength)))
282
+ const numerator = tfVal * (k1 + 1);
283
+ const denominator = tfVal + k1 * (1 - b + b * (docLen / this.avgDocLength));
284
+
285
+ score += idf * (numerator / denominator);
286
+ }
287
+
288
+ keywordScores.set(docKey, score);
289
+ }
290
+
291
+ // 2. Embedding scores (optional)
292
+ let embeddingScores: Map<string, number> | null = null;
293
+
294
+ if (this.options.embedFn && this.embeddings.size > 0) {
295
+ try {
296
+ const [queryEmbedding] = await this.options.embedFn([queryLower]);
297
+ if (queryEmbedding) {
298
+ embeddingScores = new Map();
299
+ for (const [docKey, vec] of this.embeddings) {
300
+ embeddingScores.set(docKey, this.cosineSimilarity(queryEmbedding, vec));
301
+ }
302
+ }
303
+ } catch {
304
+ // Silently fall back to keyword only for this query
305
+ }
306
+ }
307
+
308
+ // 3. Blend scores
309
+ const kw = this.options.keywordWeight;
310
+ const finalScores: Array<{ docKey: string; score: number }> = [];
311
+
312
+ for (const docKey of this.toolSummaries.keys()) {
313
+ const kwScore = keywordScores.get(docKey) ?? 0;
314
+ const embScore = embeddingScores?.get(docKey) ?? 0;
315
+
316
+ const score = embeddingScores ? kw * kwScore + (1 - kw) * embScore : kwScore;
317
+
318
+ if (score > 0) {
319
+ finalScores.push({ docKey, score });
320
+ }
321
+ }
322
+
323
+ // 4. Sort and return top-K
324
+ finalScores.sort((a, b) => b.score - a.score);
325
+
326
+ return finalScores.slice(0, topK).map(({ docKey }) => {
327
+ return this.toolSummaries.get(docKey)!;
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Search tools using a regex pattern.
333
+ * Matches against name, description, and parameter metadata.
334
+ */
335
+ searchRegex(pattern: string, topK = 5): ToolSummary[] {
336
+ if (this.tools.size === 0) return [];
337
+
338
+ try {
339
+ // Handle Anthropic-style (?i) case-insensitive flag which JS doesn't support natively in string
340
+ let flags = '';
341
+ let cleanPattern = pattern;
342
+ if (pattern.includes('(?i)')) {
343
+ flags = 'i';
344
+ cleanPattern = pattern.replace(/\(\?i\)/g, '');
345
+ }
346
+
347
+ const regex = new RegExp(cleanPattern, flags || undefined);
348
+ const matches: Array<{ docKey: string; score: number }> = [];
349
+
350
+ for (const [docKey, text] of this.searchTexts) {
351
+ const tool = this.toolSummaries.get(docKey);
352
+ if (!tool) continue;
353
+
354
+ if (regex.test(text) || regex.test(tool.name)) {
355
+ // Use a simple heuristic for ranking regex matches:
356
+ // 1. Exact name match (highest)
357
+ // 2. Name starts with pattern
358
+ // 3. Name contains pattern
359
+ // 4. Description contains pattern (lowest)
360
+ let score = 1;
361
+ if (tool.name === cleanPattern) score = 10;
362
+ else if (tool.name.startsWith(cleanPattern)) score = 5;
363
+ else if (tool.name.toLowerCase().includes(cleanPattern.toLowerCase())) score = 2;
364
+
365
+ matches.push({ docKey, score });
366
+ }
367
+ }
368
+
369
+ matches.sort((a, b) => b.score - a.score);
370
+
371
+ return matches.slice(0, topK).map(({ docKey }) => {
372
+ return this.toolSummaries.get(docKey)!;
373
+ });
374
+ } catch (err) {
375
+ console.warn('[ToolIndex] Regex search failed:', err);
376
+ return [];
377
+ }
378
+ }
379
+
380
+ // -----------------------------------------------------------------------
381
+ // Accessors
382
+ // -----------------------------------------------------------------------
383
+
384
+ /**
385
+ * Get tool definition(s) by name.
386
+ * If namespace is provided, it tries to match sessionId or serverName.
387
+ */
388
+ getTool(name: string, namespace?: string): IndexedTool[] {
389
+ const list = this.tools.get(name) ?? [];
390
+ if (!namespace) return list;
391
+
392
+ return list.filter((t) => t.sessionId === namespace || t.serverName === namespace);
393
+ }
394
+
395
+ /** All indexed tool names. */
396
+ getToolNames(): string[] {
397
+ return [...this.tools.keys()];
398
+ }
399
+
400
+ /** Number of indexed tools (including duplicates). */
401
+ get size(): number {
402
+ let count = 0;
403
+ for (const list of this.tools.values()) {
404
+ count += list.length;
405
+ }
406
+ return count;
407
+ }
408
+
409
+ /** Total estimated token cost of all indexed tool schemas. */
410
+ getTotalTokenCost(): number {
411
+ return this.totalTokenCost;
412
+ }
413
+
414
+ // -----------------------------------------------------------------------
415
+ // Static Helpers
416
+ // -----------------------------------------------------------------------
417
+
418
+ /**
419
+ * Estimate token count of a tool's full schema (name + description + inputSchema).
420
+ *
421
+ * Uses character-class weighted counting calibrated against cl100k_base.
422
+ * Accuracy is typically within ±10% for JSON Schema payloads.
423
+ */
424
+ static estimateTokens(tool: Tool): number {
425
+ const parts: string[] = [tool.name];
426
+ if (tool.description) parts.push(tool.description);
427
+ if (tool.inputSchema) parts.push(JSON.stringify(tool.inputSchema));
428
+
429
+ const text = parts.join(' ');
430
+ let weightedLen = 0;
431
+
432
+ for (let i = 0; i < text.length; i++) {
433
+ weightedLen += 1 / classifyChar(text[i]);
434
+ }
435
+
436
+ return Math.ceil(weightedLen / (1 / CALIBRATION_DIVISOR));
437
+ }
438
+
439
+ // -----------------------------------------------------------------------
440
+ // Internals
441
+ // -----------------------------------------------------------------------
442
+
443
+ /** Build a single searchable string from tool metadata. */
444
+ private buildSearchableText(tool: Tool): string {
445
+ const parts: string[] = [tool.name];
446
+ if (tool.description) parts.push(tool.description);
447
+
448
+ // Include property names and descriptions from schema
449
+ if (tool.inputSchema && typeof tool.inputSchema === 'object') {
450
+ const schema = tool.inputSchema as Record<string, unknown>;
451
+ const props = schema.properties as Record<string, { description?: string }> | undefined;
452
+ if (props) {
453
+ for (const [key, val] of Object.entries(props)) {
454
+ parts.push(key);
455
+ if (val && typeof val === 'object' && val.description) {
456
+ parts.push(val.description);
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ return parts.join(' ');
463
+ }
464
+
465
+ private getDocumentKey(tool: IndexedTool): string {
466
+ return `${tool.sessionId}::${tool.serverName}::${tool.name}`;
467
+ }
468
+
469
+ /** Simple whitespace + camelCase + snake_case tokenizer. */
470
+ private tokenize(text: string): string[] {
471
+ return text
472
+ // Split camelCase: "getWeather" → "get Weather"
473
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
474
+ // Split snake_case / kebab-case
475
+ .replace(/[_-]/g, ' ')
476
+ // Remove non-alphanumeric (except spaces)
477
+ .replace(/[^a-z0-9\s]/g, '')
478
+ // Split on whitespace
479
+ .split(/\s+/)
480
+ .filter((t) => t.length > 1); // drop single-char noise
481
+ }
482
+
483
+ /** Cosine similarity between two vectors. */
484
+ private cosineSimilarity(a: number[], b: number[]): number {
485
+ const len = Math.min(a.length, b.length);
486
+ let dot = 0;
487
+ let magA = 0;
488
+ let magB = 0;
489
+
490
+ for (let i = 0; i < len; i++) {
491
+ dot += a[i] * b[i];
492
+ magA += a[i] * a[i];
493
+ magB += b[i] * b[i];
494
+ }
495
+
496
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
497
+ return denom > 0 ? dot / denom : 0;
498
+ }
499
+ }