@mcp-ts/sdk 1.4.0 → 1.5.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.
- package/README.md +20 -27
- package/dist/adapters/agui-adapter.d.mts +16 -0
- package/dist/adapters/agui-adapter.d.ts +16 -0
- package/dist/adapters/agui-adapter.js +185 -0
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +185 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +2 -0
- package/dist/adapters/agui-middleware.d.ts +2 -0
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +21 -0
- package/dist/adapters/ai-adapter.d.ts +21 -0
- package/dist/adapters/ai-adapter.js +175 -0
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +175 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +16 -0
- package/dist/adapters/langchain-adapter.d.ts +16 -0
- package/dist/adapters/langchain-adapter.js +179 -0
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +179 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/react.d.mts +94 -8
- package/dist/client/react.d.ts +94 -8
- package/dist/client/react.js +364 -26
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +358 -27
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +4 -4
- package/dist/client/vue.d.ts +4 -4
- package/dist/client/vue.js +11 -2
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +11 -2
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-CQr9q0bF.d.mts → index-DcYfpY3H.d.mts} +1 -1
- package/dist/{index-nE_7Io0I.d.ts → index-GfC_eNEv.d.ts} +1 -1
- package/dist/index.d.mts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +938 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +923 -13
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +58 -12
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +58 -12
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +86 -4
- package/dist/shared/index.d.ts +86 -4
- package/dist/shared/index.js +874 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +865 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/tool-router-Bo8qZbsD.d.ts +325 -0
- package/dist/tool-router-XnWVxPzv.d.mts +325 -0
- package/dist/{types-CW6lghof.d.mts → types-CfCoIsWI.d.mts} +27 -1
- package/dist/{types-CW6lghof.d.ts → types-CfCoIsWI.d.ts} +27 -1
- package/package.json +3 -2
- package/src/adapters/agui-adapter.ts +79 -0
- package/src/adapters/ai-adapter.ts +75 -0
- package/src/adapters/langchain-adapter.ts +74 -0
- package/src/client/react/index.ts +16 -0
- package/src/client/react/oauth-popup.tsx +446 -0
- package/src/client/react/use-mcp-apps.tsx +50 -32
- package/src/client/react/use-mcp.ts +36 -3
- package/src/client/vue/use-mcp.ts +38 -3
- package/src/server/handlers/sse-handler.ts +39 -0
- package/src/server/index.ts +2 -0
- package/src/server/mcp/oauth-client.ts +35 -15
- package/src/shared/index.ts +36 -0
- package/src/shared/meta-tools.ts +387 -0
- package/src/shared/schema-compressor.ts +124 -0
- package/src/shared/tool-index.ts +499 -0
- package/src/shared/tool-router.ts +469 -0
- package/src/shared/types.ts +30 -0
- package/supabase/migrations/20260421010000_add_session_cleanup_cron.sql +32 -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
|
+
}
|