@mcp-ts/sdk 1.5.1 → 1.5.2
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/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-adapter.js +43 -9
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +43 -9
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- 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 +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/ai-adapter.js +42 -8
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +42 -8
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.js +42 -8
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +42 -8
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/client/react.d.mts +10 -0
- package/dist/client/react.d.ts +10 -0
- package/dist/client/react.js +23 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +23 -0
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +10 -0
- package/dist/client/vue.d.ts +10 -0
- package/dist/client/vue.js +17 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +17 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +115 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +115 -26
- package/dist/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +115 -26
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +115 -26
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-XnWVxPzv.d.mts → tool-router-DK0RJblO.d.mts} +3 -0
- package/dist/{tool-router-Bo8qZbsD.d.ts → tool-router-DsKhRmJm.d.ts} +3 -0
- package/package.json +1 -1
- package/src/adapters/agui-adapter.ts +7 -7
- package/src/adapters/ai-adapter.ts +5 -5
- package/src/adapters/langchain-adapter.ts +5 -5
- package/src/client/react/use-mcp.ts +48 -0
- package/src/client/vue/use-mcp.ts +42 -0
- package/src/shared/meta-tools.ts +62 -13
- package/src/shared/tool-index.ts +85 -12
- package/src/shared/tool-router.ts +8 -7
|
@@ -137,11 +137,11 @@ export class AIAdapter {
|
|
|
137
137
|
// @ts-ignore: ToolSet type inference can be tricky with dynamic imports
|
|
138
138
|
return Object.fromEntries(
|
|
139
139
|
filteredTools.map((tool) => {
|
|
140
|
-
const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
|
|
141
|
-
const namespace = routedTool.
|
|
140
|
+
const routedTool = tool as typeof tool & { sessionId?: string; serverId?: string; serverName?: string };
|
|
141
|
+
const namespace = routedTool.serverId ?? routedTool.sessionId;
|
|
142
142
|
const toolKey = isMetaTool(tool.name)
|
|
143
143
|
? tool.name
|
|
144
|
-
: this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.
|
|
144
|
+
: this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverId);
|
|
145
145
|
|
|
146
146
|
return [
|
|
147
147
|
toolKey,
|
|
@@ -172,8 +172,8 @@ export class AIAdapter {
|
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
private getRouterToolKey(toolName: string, sessionId?: string,
|
|
176
|
-
const namespace = sessionId ??
|
|
175
|
+
private getRouterToolKey(toolName: string, sessionId?: string, serverId?: string): string {
|
|
176
|
+
const namespace = sessionId ?? serverId ?? 'mcp';
|
|
177
177
|
const normalized = namespace
|
|
178
178
|
.toLowerCase()
|
|
179
179
|
.replace(/[^a-z0-9]+/g, '_')
|
|
@@ -144,14 +144,14 @@ export class LangChainAdapter {
|
|
|
144
144
|
const filteredTools = await router.getFilteredTools();
|
|
145
145
|
|
|
146
146
|
return filteredTools.map((tool) => {
|
|
147
|
-
const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
|
|
148
|
-
const namespace = routedTool.
|
|
147
|
+
const routedTool = tool as typeof tool & { sessionId?: string; serverId?: string; serverName?: string };
|
|
148
|
+
const namespace = routedTool.serverId ?? routedTool.sessionId;
|
|
149
149
|
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
150
150
|
|
|
151
151
|
return new this.DynamicStructuredTool!({
|
|
152
152
|
name: isMetaTool(tool.name)
|
|
153
153
|
? tool.name
|
|
154
|
-
: this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.
|
|
154
|
+
: this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverId),
|
|
155
155
|
description: tool.description || `Tool ${tool.name}`,
|
|
156
156
|
schema: schema,
|
|
157
157
|
func: async (args: any) => {
|
|
@@ -183,8 +183,8 @@ export class LangChainAdapter {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
private getRouterToolKey(toolName: string, sessionId?: string,
|
|
187
|
-
const namespace = sessionId ??
|
|
186
|
+
private getRouterToolKey(toolName: string, sessionId?: string, serverId?: string): string {
|
|
187
|
+
const namespace = sessionId ?? serverId ?? 'mcp';
|
|
188
188
|
const normalized = namespace
|
|
189
189
|
.toLowerCase()
|
|
190
190
|
.replace(/[^a-z0-9]+/g, '_')
|
|
@@ -118,6 +118,17 @@ export interface McpClient {
|
|
|
118
118
|
*/
|
|
119
119
|
disconnect: (sessionId: string) => Promise<void>;
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Reconnect to an MCP server (disconnects existing session first)
|
|
123
|
+
*/
|
|
124
|
+
reconnect: (params: {
|
|
125
|
+
serverId: string;
|
|
126
|
+
serverName: string;
|
|
127
|
+
serverUrl: string;
|
|
128
|
+
callbackUrl: string;
|
|
129
|
+
transportType?: 'sse' | 'streamable_http';
|
|
130
|
+
}) => Promise<string>;
|
|
131
|
+
|
|
121
132
|
/**
|
|
122
133
|
* Get connection by session ID
|
|
123
134
|
*/
|
|
@@ -499,6 +510,41 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
499
510
|
[]
|
|
500
511
|
);
|
|
501
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Reconnect to an MCP server (tears down existing session, then connects fresh)
|
|
515
|
+
*/
|
|
516
|
+
const reconnect = useCallback(
|
|
517
|
+
async (params: {
|
|
518
|
+
serverId: string;
|
|
519
|
+
serverName: string;
|
|
520
|
+
serverUrl: string;
|
|
521
|
+
callbackUrl: string;
|
|
522
|
+
transportType?: 'sse' | 'streamable_http';
|
|
523
|
+
}): Promise<string> => {
|
|
524
|
+
if (!clientRef.current) {
|
|
525
|
+
throw new Error('SSE client not initialized');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Find and disconnect existing session for the same server
|
|
529
|
+
const existing = connections.find(
|
|
530
|
+
(c: McpConnection) => c.serverId === params.serverId || c.serverUrl === params.serverUrl
|
|
531
|
+
);
|
|
532
|
+
if (existing) {
|
|
533
|
+
await clientRef.current.disconnectFromServer(existing.sessionId);
|
|
534
|
+
if (isMountedRef.current) {
|
|
535
|
+
setConnections((prev: McpConnection[]) =>
|
|
536
|
+
prev.filter((c: McpConnection) => c.sessionId !== existing.sessionId)
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Connect fresh
|
|
542
|
+
const result = await clientRef.current.connectToServer(params);
|
|
543
|
+
return result.sessionId;
|
|
544
|
+
},
|
|
545
|
+
[connections]
|
|
546
|
+
);
|
|
547
|
+
|
|
502
548
|
/**
|
|
503
549
|
* Disconnect from an MCP server
|
|
504
550
|
*/
|
|
@@ -668,6 +714,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
668
714
|
status,
|
|
669
715
|
isInitializing,
|
|
670
716
|
connect,
|
|
717
|
+
reconnect,
|
|
671
718
|
disconnect,
|
|
672
719
|
getConnection,
|
|
673
720
|
getConnectionByServerId,
|
|
@@ -691,6 +738,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
691
738
|
status,
|
|
692
739
|
isInitializing,
|
|
693
740
|
connect,
|
|
741
|
+
reconnect,
|
|
694
742
|
disconnect,
|
|
695
743
|
getConnection,
|
|
696
744
|
getConnectionByServerId,
|
|
@@ -117,6 +117,17 @@ export interface McpClient {
|
|
|
117
117
|
*/
|
|
118
118
|
disconnect: (sessionId: string) => Promise<void>;
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Reconnect to an MCP server (disconnects existing session first)
|
|
122
|
+
*/
|
|
123
|
+
reconnect: (params: {
|
|
124
|
+
serverId: string;
|
|
125
|
+
serverName: string;
|
|
126
|
+
serverUrl: string;
|
|
127
|
+
callbackUrl: string;
|
|
128
|
+
transportType?: 'sse' | 'streamable_http';
|
|
129
|
+
}) => Promise<string>;
|
|
130
|
+
|
|
120
131
|
/**
|
|
121
132
|
* Get connection by session ID
|
|
122
133
|
*/
|
|
@@ -492,6 +503,36 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
492
503
|
return result.sessionId;
|
|
493
504
|
};
|
|
494
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Reconnect to an MCP server (tears down existing session, then connects fresh)
|
|
508
|
+
*/
|
|
509
|
+
const reconnect = async (params: {
|
|
510
|
+
serverId: string;
|
|
511
|
+
serverName: string;
|
|
512
|
+
serverUrl: string;
|
|
513
|
+
callbackUrl: string;
|
|
514
|
+
transportType?: 'sse' | 'streamable_http';
|
|
515
|
+
}): Promise<string> => {
|
|
516
|
+
if (!clientRef.value) {
|
|
517
|
+
throw new Error('SSE client not initialized');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Find and disconnect existing session for the same server
|
|
521
|
+
const existing = connections.value.find(
|
|
522
|
+
(c) => c.serverId === params.serverId || c.serverUrl === params.serverUrl
|
|
523
|
+
);
|
|
524
|
+
if (existing) {
|
|
525
|
+
await clientRef.value.disconnectFromServer(existing.sessionId);
|
|
526
|
+
if (isMountedRef.value) {
|
|
527
|
+
connections.value = connections.value.filter((c) => c.sessionId !== existing.sessionId);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Connect fresh
|
|
532
|
+
const result = await clientRef.value.connectToServer(params);
|
|
533
|
+
return result.sessionId;
|
|
534
|
+
};
|
|
535
|
+
|
|
495
536
|
/**
|
|
496
537
|
* Disconnect from an MCP server
|
|
497
538
|
*/
|
|
@@ -642,6 +683,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
642
683
|
status: status as unknown as { value: 'connecting' | 'connected' | 'disconnected' | 'error' },
|
|
643
684
|
isInitializing: isInitializing as unknown as { value: boolean },
|
|
644
685
|
connect,
|
|
686
|
+
reconnect,
|
|
645
687
|
disconnect,
|
|
646
688
|
getConnection,
|
|
647
689
|
getConnectionByServerId,
|
package/src/shared/meta-tools.ts
CHANGED
|
@@ -33,16 +33,18 @@ export function createSearchToolDefinition(): Tool {
|
|
|
33
33
|
return {
|
|
34
34
|
name: 'mcp_search_tool_bm25',
|
|
35
35
|
description:
|
|
36
|
-
'Search the catalog of available tools
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
36
|
+
'Search the catalog of available tools. Returns tool names, descriptions, and server info. ' +
|
|
37
|
+
'Use this FIRST to find relevant tools before calling them.\n\n' +
|
|
38
|
+
'Query forms:\n' +
|
|
39
|
+
'- "select:Read,Edit,Grep" — fetch these exact tools by name\n' +
|
|
40
|
+
'- "notebook jupyter" — keyword search, up to limit best matches\n' +
|
|
41
|
+
'- "+slack send" — require "slack" in the name, rank by remaining terms',
|
|
40
42
|
inputSchema: {
|
|
41
43
|
type: 'object' as const,
|
|
42
44
|
properties: {
|
|
43
45
|
query: {
|
|
44
46
|
type: 'string',
|
|
45
|
-
description: '
|
|
47
|
+
description: 'Query to find tools. Use "select:<tool_name>" for direct selection, or keywords to search. Prefix keywords with + to require them.',
|
|
46
48
|
},
|
|
47
49
|
limit: {
|
|
48
50
|
type: 'number',
|
|
@@ -105,10 +107,10 @@ export function createGetSchemaToolDefinition(): Tool {
|
|
|
105
107
|
type: 'string',
|
|
106
108
|
description: 'The exact tool name returned by mcp_search_tool_bm25.',
|
|
107
109
|
},
|
|
108
|
-
|
|
110
|
+
serverId: {
|
|
109
111
|
type: 'string',
|
|
110
112
|
description:
|
|
111
|
-
'Optional: The server
|
|
113
|
+
'Optional: The server ID provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
|
|
112
114
|
},
|
|
113
115
|
},
|
|
114
116
|
required: ['toolName'],
|
|
@@ -141,10 +143,10 @@ export function createExecuteToolDefinition(): Tool {
|
|
|
141
143
|
type: 'string',
|
|
142
144
|
description: 'The exact tool name from mcp_search_tool_bm25 results.',
|
|
143
145
|
},
|
|
144
|
-
|
|
146
|
+
serverId: {
|
|
145
147
|
type: 'string',
|
|
146
148
|
description:
|
|
147
|
-
'Optional: The server
|
|
149
|
+
'Optional: The server ID provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
|
|
148
150
|
},
|
|
149
151
|
args: {
|
|
150
152
|
type: 'object',
|
|
@@ -206,6 +208,53 @@ export async function executeMetaTool(
|
|
|
206
208
|
const query = String(args.query ?? '');
|
|
207
209
|
const limit = Math.min(Number(args.limit) || 5, 20);
|
|
208
210
|
|
|
211
|
+
// Fast path: Check for select: prefix
|
|
212
|
+
const selectMatch = query.match(/^select:(.+)$/i);
|
|
213
|
+
if (selectMatch) {
|
|
214
|
+
const requested = selectMatch[1]!
|
|
215
|
+
.split(',')
|
|
216
|
+
.map((s) => s.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
|
|
219
|
+
const found: any[] = [];
|
|
220
|
+
const errors: string[] = [];
|
|
221
|
+
|
|
222
|
+
for (const requestedToolName of requested) {
|
|
223
|
+
const { tool, error } = resolveToolSchema(requestedToolName);
|
|
224
|
+
if (error) {
|
|
225
|
+
const errorMsg = error.content[0]?.type === 'text' ? error.content[0].text : 'Unknown error';
|
|
226
|
+
errors.push(`- **${requestedToolName}**: ${errorMsg}`);
|
|
227
|
+
} else if (tool) {
|
|
228
|
+
found.push(tool);
|
|
229
|
+
} else {
|
|
230
|
+
errors.push(`- **${requestedToolName}**: Tool not found. Try searching with mcp_search_tool_bm25.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const lines: string[] = [];
|
|
235
|
+
|
|
236
|
+
if (found.length > 0) {
|
|
237
|
+
lines.push(...found.map((t, i) =>
|
|
238
|
+
`${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n ${t.description}`
|
|
239
|
+
));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (errors.length > 0) {
|
|
243
|
+
if (lines.length > 0) lines.push(""); // Add empty line spacing
|
|
244
|
+
lines.push("Errors resolving some tools:");
|
|
245
|
+
lines.push(...errors);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const text = lines.length > 0
|
|
249
|
+
? lines.join('\n')
|
|
250
|
+
: `No tools found matching select query: ${requested.join(', ')}`;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: 'text', text }],
|
|
254
|
+
isError: found.length === 0,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
209
258
|
const results = await router.searchTools(query, limit);
|
|
210
259
|
|
|
211
260
|
const text = results.length === 0
|
|
@@ -213,7 +262,7 @@ export async function executeMetaTool(
|
|
|
213
262
|
: results
|
|
214
263
|
.map(
|
|
215
264
|
(t, i) =>
|
|
216
|
-
`${i + 1}. **${t.name}** (server: ${t.serverName})\n` +
|
|
265
|
+
`${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
|
|
217
266
|
` ${t.description}\n` +
|
|
218
267
|
` Estimated tokens: ${t.estimatedTokens}`
|
|
219
268
|
)
|
|
@@ -236,7 +285,7 @@ export async function executeMetaTool(
|
|
|
236
285
|
: results
|
|
237
286
|
.map(
|
|
238
287
|
(t, i) =>
|
|
239
|
-
`${i + 1}. **${t.name}** (server: ${t.serverName})\n` +
|
|
288
|
+
`${i + 1}. **${t.name}** (server: ${t.serverName}, serverId: ${t.serverId})\n` +
|
|
240
289
|
` ${t.description}\n` +
|
|
241
290
|
` Estimated tokens: ${t.estimatedTokens}`
|
|
242
291
|
)
|
|
@@ -250,7 +299,7 @@ export async function executeMetaTool(
|
|
|
250
299
|
|
|
251
300
|
case 'mcp_get_tool_schema': {
|
|
252
301
|
const name = String(args.toolName ?? '');
|
|
253
|
-
const namespace = String(args.
|
|
302
|
+
const namespace = String(args.serverId ?? '') || undefined;
|
|
254
303
|
const { tool, error } = resolveToolSchema(name, namespace);
|
|
255
304
|
|
|
256
305
|
if (error) {
|
|
@@ -283,7 +332,7 @@ export async function executeMetaTool(
|
|
|
283
332
|
|
|
284
333
|
case 'mcp_execute_tool': {
|
|
285
334
|
const targetToolName = String(args.toolName ?? '');
|
|
286
|
-
const namespace = String(args.
|
|
335
|
+
const namespace = String(args.serverId ?? '') || undefined;
|
|
287
336
|
const toolArgs = (args.args as Record<string, unknown>) ?? {};
|
|
288
337
|
|
|
289
338
|
if (!targetToolName) {
|
package/src/shared/tool-index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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([
|
|
366
|
+
const [queryEmbedding] = await this.options.embedFn([normalizedQueryText]);
|
|
297
367
|
if (queryEmbedding) {
|
|
298
368
|
embeddingScores = new Map();
|
|
299
|
-
for (const
|
|
300
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 "
|
|
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
|
|
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.
|
|
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 [
|
|
423
|
-
this.groupsMap.set(
|
|
423
|
+
for (const [serverId, tools] of serverTools) {
|
|
424
|
+
this.groupsMap.set(serverId, {
|
|
424
425
|
tools,
|
|
425
|
-
active: this.activeGroups.size === 0 || this.activeGroups.has(
|
|
426
|
+
active: this.activeGroups.size === 0 || this.activeGroups.has(serverId),
|
|
426
427
|
});
|
|
427
428
|
}
|
|
428
429
|
}
|