@mcp-ts/sdk 1.3.10 → 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.
- 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 +4 -190
- package/dist/client/index.d.ts +4 -190
- package/dist/client/index.js +218 -54
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +215 -55
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +31 -17
- package/dist/client/react.d.ts +31 -17
- package/dist/client/react.js +447 -103
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +443 -105
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -4
- package/dist/client/vue.d.ts +5 -4
- package/dist/client/vue.js +239 -63
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +236 -64
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index-DcYfpY3H.d.mts +295 -0
- package/dist/index-GfC_eNEv.d.ts +295 -0
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1120 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1097 -60
- 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 +18 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -5
- 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 +15 -12
- package/src/adapters/agui-adapter.ts +79 -0
- package/src/adapters/ai-adapter.ts +75 -0
- package/src/adapters/langchain-adapter.ts +75 -1
- package/src/client/core/app-host.ts +252 -65
- package/src/client/core/constants.ts +30 -0
- package/src/client/index.ts +6 -1
- package/src/client/react/index.ts +3 -0
- package/src/client/react/use-app-host.ts +8 -15
- package/src/client/react/use-mcp-apps.tsx +262 -49
- package/src/client/react/use-mcp.ts +23 -12
- package/src/client/utils/app-host-utils.ts +62 -0
- package/src/client/vue/use-mcp.ts +23 -12
- package/src/server/index.ts +2 -0
- package/src/server/mcp/oauth-client.ts +34 -9
- 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
|
@@ -285,22 +285,33 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
case 'auth_required': {
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
onLog?.('
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
288
|
+
const url = (event.authUrl || '').trim();
|
|
289
|
+
if (!url) {
|
|
290
|
+
onLog?.('error', 'OAuth required but authorization URL is missing', { sessionId: event.sessionId });
|
|
291
|
+
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
292
|
+
if (index !== -1) {
|
|
293
|
+
connections.value[index] = {
|
|
294
|
+
...connections.value[index],
|
|
295
|
+
state: 'FAILED',
|
|
296
|
+
error: 'OAuth authorization URL not available',
|
|
297
|
+
authUrl: undefined,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
onLog?.('info', `OAuth required - redirecting to ${url}`, { authUrl: url });
|
|
303
|
+
|
|
304
|
+
// Suppress redirects/popups for background auto-restore on page load.
|
|
305
|
+
if (!suppressAuthRedirectSessions.value.has(event.sessionId)) {
|
|
306
|
+
if (onRedirect) {
|
|
307
|
+
onRedirect(url);
|
|
308
|
+
} else if (typeof window !== 'undefined') {
|
|
309
|
+
window.location.href = url;
|
|
299
310
|
}
|
|
300
311
|
}
|
|
301
312
|
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
302
313
|
if (index !== -1) {
|
|
303
|
-
connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl:
|
|
314
|
+
connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl: url };
|
|
304
315
|
}
|
|
305
316
|
break;
|
|
306
317
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -51,12 +51,12 @@ export type TransportType = 'sse' | 'streamable_http';
|
|
|
51
51
|
*/
|
|
52
52
|
import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
|
|
53
53
|
|
|
54
|
-
interface McpAppClientCapabilities extends ClientCapabilities {
|
|
54
|
+
interface McpAppClientCapabilities extends Omit<ClientCapabilities, 'extensions'> {
|
|
55
55
|
extensions?: {
|
|
56
56
|
'io.modelcontextprotocol/ui'?: {
|
|
57
57
|
mimeTypes: string[];
|
|
58
58
|
};
|
|
59
|
-
[key: string]:
|
|
59
|
+
[key: string]: any;
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -516,17 +516,40 @@ export class MCPClient {
|
|
|
516
516
|
error instanceof SDKUnauthorizedError ||
|
|
517
517
|
(error instanceof Error && error.message.toLowerCase().includes('unauthorized'))
|
|
518
518
|
) {
|
|
519
|
+
/** Set when the SDK calls redirectToAuthorization on the OAuth provider */
|
|
520
|
+
let authUrl = '';
|
|
521
|
+
if (this.oauthProvider) {
|
|
522
|
+
authUrl = (this.oauthProvider.authUrl || '').trim();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 401 without a usable URL means metadata/DCR failed or the server never started
|
|
527
|
+
* an interactive OAuth flow — not recoverable as "pending OAuth".
|
|
528
|
+
*/
|
|
529
|
+
if (!authUrl) {
|
|
530
|
+
const detail =
|
|
531
|
+
error instanceof Error && error.message.trim().length > 0
|
|
532
|
+
? error.message.trim()
|
|
533
|
+
: 'Unauthorized';
|
|
534
|
+
const message =
|
|
535
|
+
detail.toLowerCase() === 'unauthorized'
|
|
536
|
+
? 'OAuth authorization URL not available'
|
|
537
|
+
: `OAuth authorization URL not available: ${detail}`;
|
|
538
|
+
this.emitError(message, 'auth');
|
|
539
|
+
this.emitStateChange('FAILED');
|
|
540
|
+
try {
|
|
541
|
+
await storage.removeSession(this.identity, this.sessionId);
|
|
542
|
+
} catch {
|
|
543
|
+
// best-effort cleanup
|
|
544
|
+
}
|
|
545
|
+
throw new Error(message);
|
|
546
|
+
}
|
|
547
|
+
|
|
519
548
|
this.emitStateChange('AUTHENTICATING');
|
|
520
549
|
// Save session with 10min TTL for OAuth pending state
|
|
521
550
|
console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
|
|
522
551
|
await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
|
|
523
552
|
|
|
524
|
-
/** Get OAuth authorization URL if available */
|
|
525
|
-
let authUrl = '';
|
|
526
|
-
if (this.oauthProvider) {
|
|
527
|
-
authUrl = this.oauthProvider.authUrl || '';
|
|
528
|
-
}
|
|
529
|
-
|
|
530
553
|
if (this.serverId) {
|
|
531
554
|
this._onConnectionEvent.fire({
|
|
532
555
|
type: 'auth_required',
|
|
@@ -1085,7 +1108,9 @@ export class MCPClient {
|
|
|
1085
1108
|
* @returns Server name or undefined
|
|
1086
1109
|
*/
|
|
1087
1110
|
getServerName(): string | undefined {
|
|
1088
|
-
|
|
1111
|
+
const info = (this.client as any)?.getServerVersion();
|
|
1112
|
+
console.log('server info ->', info);
|
|
1113
|
+
return info?.title ?? info?.name ?? this.serverName;
|
|
1089
1114
|
}
|
|
1090
1115
|
|
|
1091
1116
|
/**
|
package/src/shared/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ export * from './errors';
|
|
|
22
22
|
|
|
23
23
|
// Types
|
|
24
24
|
export type {
|
|
25
|
+
ToolClient,
|
|
26
|
+
ToolClientProvider,
|
|
25
27
|
ToolInfo,
|
|
26
28
|
McpRpcRequest,
|
|
27
29
|
McpRpcResponse,
|
|
@@ -73,3 +75,37 @@ export {
|
|
|
73
75
|
type ToolUiConfig,
|
|
74
76
|
} from './tool-utils.js';
|
|
75
77
|
|
|
78
|
+
// Tool Router — Context window optimization
|
|
79
|
+
export {
|
|
80
|
+
ToolRouter,
|
|
81
|
+
type ToolRouterOptions,
|
|
82
|
+
type ToolRouterStrategy,
|
|
83
|
+
type ToolRouterClientInput,
|
|
84
|
+
type ToolGroupInfo,
|
|
85
|
+
} from './tool-router.js';
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
ToolIndex,
|
|
89
|
+
type ToolSummary,
|
|
90
|
+
type IndexedTool,
|
|
91
|
+
type ToolIndexOptions,
|
|
92
|
+
type EmbedFn,
|
|
93
|
+
} from './tool-index.js';
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
SchemaCompressor,
|
|
97
|
+
type CompactTool,
|
|
98
|
+
type CompressionStats,
|
|
99
|
+
} from './schema-compressor.js';
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
createSearchToolDefinition,
|
|
103
|
+
createRegexSearchToolDefinition,
|
|
104
|
+
createGetSchemaToolDefinition,
|
|
105
|
+
createExecuteToolDefinition,
|
|
106
|
+
executeMetaTool,
|
|
107
|
+
isMetaTool,
|
|
108
|
+
resolveMetaToolProxy,
|
|
109
|
+
type CallToolFn,
|
|
110
|
+
} from './meta-tools.js';
|
|
111
|
+
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta-tools — Injectable tool definitions that let the LLM discover and
|
|
3
|
+
* load MCP tools on-demand, following Anthropic's Tool Search pattern.
|
|
4
|
+
*
|
|
5
|
+
* Instead of injecting 50+ full tool schemas into the context window, you
|
|
6
|
+
* inject just these 4 meta-tools. The LLM calls them to find and load
|
|
7
|
+
* only the tools it actually needs.
|
|
8
|
+
*
|
|
9
|
+
* Meta-tools:
|
|
10
|
+
* • `mcp_search_tool_bm25` — BM25 natural language search
|
|
11
|
+
* • `mcp_search_tool_regex` — Regex pattern search
|
|
12
|
+
* • `mcp_get_tool_schema` — Get full inputSchema for a discovered tool
|
|
13
|
+
* • `mcp_execute_tool` — Execute a discovered tool
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
+
import type { ToolRouter } from './tool-router.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Tool Definitions
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates the `mcp_search_tool_bm25` tool definition.
|
|
27
|
+
*
|
|
28
|
+
* This tool lets the LLM search the full catalog of available MCP tools
|
|
29
|
+
* using a BM25 natural-language query. Returns tool names and descriptions
|
|
30
|
+
* without the full inputSchema to save context space.
|
|
31
|
+
*/
|
|
32
|
+
export function createSearchToolDefinition(): Tool {
|
|
33
|
+
return {
|
|
34
|
+
name: 'mcp_search_tool_bm25',
|
|
35
|
+
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".',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object' as const,
|
|
42
|
+
properties: {
|
|
43
|
+
query: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Natural language description of the capability you need.',
|
|
46
|
+
},
|
|
47
|
+
limit: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
description: 'Maximum number of results to return (default: 5, max: 20).',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['query'],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates the `mcp_search_tool_regex` tool definition.
|
|
59
|
+
*
|
|
60
|
+
* Matches Anthropic's tool_search_tool_regex exactly (takes a 'query' regex pattern).
|
|
61
|
+
*/
|
|
62
|
+
export function createRegexSearchToolDefinition(): Tool {
|
|
63
|
+
return {
|
|
64
|
+
name: 'mcp_search_tool_regex',
|
|
65
|
+
description:
|
|
66
|
+
'Search the catalog of available tools using a Python-style regex pattern. ' +
|
|
67
|
+
'Matches against tool names, descriptions, and parameter descriptions. ' +
|
|
68
|
+
'Example patterns: "^github_", "weather", "(?i)slack".',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object' as const,
|
|
71
|
+
properties: {
|
|
72
|
+
query: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Regex pattern to search for (e.g., "^get_.*_data", "database").',
|
|
75
|
+
},
|
|
76
|
+
limit: {
|
|
77
|
+
type: 'number',
|
|
78
|
+
description: 'Maximum number of results to return (default: 5, max: 20).',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['query'],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates the `mcp_get_tool_schema` tool definition.
|
|
88
|
+
*
|
|
89
|
+
* After discovering tools via `mcp_search_tool_bm25` or
|
|
90
|
+
* `mcp_search_tool_regex`, the LLM calls this to load the full
|
|
91
|
+
* inputSchema for a specific tool so it can construct the correct
|
|
92
|
+
* arguments.
|
|
93
|
+
*/
|
|
94
|
+
export function createGetSchemaToolDefinition(): Tool {
|
|
95
|
+
return {
|
|
96
|
+
name: 'mcp_get_tool_schema',
|
|
97
|
+
description:
|
|
98
|
+
'Get the full input schema (parameters) for a specific tool. ' +
|
|
99
|
+
'Call this after mcp_search_tool_bm25 to get the parameter details ' +
|
|
100
|
+
'needed to call a tool correctly.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object' as const,
|
|
103
|
+
properties: {
|
|
104
|
+
toolName: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'The exact tool name returned by mcp_search_tool_bm25.',
|
|
107
|
+
},
|
|
108
|
+
serverName: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description:
|
|
111
|
+
'Optional: The server name provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['toolName'],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates the `mcp_execute_tool` tool definition.
|
|
121
|
+
*
|
|
122
|
+
* This is the execution meta-tool — the LLM calls this to execute any
|
|
123
|
+
* tool discovered via `mcp_search_tool_bm25` or `mcp_search_tool_regex`.
|
|
124
|
+
* The LLM should first call `mcp_get_tool_schema` to know the correct
|
|
125
|
+
* arguments.
|
|
126
|
+
*
|
|
127
|
+
* Instead of registering every real tool with the framework, we proxy
|
|
128
|
+
* all execution through a single meta-tool.
|
|
129
|
+
*/
|
|
130
|
+
export function createExecuteToolDefinition(): Tool {
|
|
131
|
+
return {
|
|
132
|
+
name: 'mcp_execute_tool',
|
|
133
|
+
description:
|
|
134
|
+
'Execute a tool that was discovered via mcp_search_tool_bm25. ' +
|
|
135
|
+
'You MUST call mcp_get_tool_schema first to know the correct parameters. ' +
|
|
136
|
+
'Pass the exact tool name and its arguments.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object' as const,
|
|
139
|
+
properties: {
|
|
140
|
+
toolName: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'The exact tool name from mcp_search_tool_bm25 results.',
|
|
143
|
+
},
|
|
144
|
+
serverName: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description:
|
|
147
|
+
'Optional: The server name provided in mcp_search_tool_bm25. Required if multiple tools have the same name.',
|
|
148
|
+
},
|
|
149
|
+
args: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
description:
|
|
152
|
+
"Arguments matching the tool's inputSchema. Omit or pass {} if the tool takes no parameters.",
|
|
153
|
+
additionalProperties: true,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ['toolName'],
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Meta-tool Executors
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Callback for executing a real MCP tool via the correct client.
|
|
167
|
+
* Provided by adapters that wire up client routing.
|
|
168
|
+
*/
|
|
169
|
+
export type CallToolFn = (
|
|
170
|
+
toolName: string,
|
|
171
|
+
args: Record<string, unknown>,
|
|
172
|
+
namespace?: string
|
|
173
|
+
) => Promise<any>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Execute a meta-tool call and return the result in MCP CallToolResult format.
|
|
177
|
+
*
|
|
178
|
+
* @param toolName - One of the meta-tool names (mcp_search_tool_bm25, mcp_search_tool_regex, etc.)
|
|
179
|
+
* @param args - The arguments from the LLM's tool call
|
|
180
|
+
* @param router - The ToolRouter to query
|
|
181
|
+
* @param callToolFn - Optional callback for executing real tools (required for mcp_execute_tool)
|
|
182
|
+
* @returns MCP-compatible CallToolResult, or null if this isn't a meta-tool
|
|
183
|
+
*/
|
|
184
|
+
export async function executeMetaTool(
|
|
185
|
+
toolName: string,
|
|
186
|
+
args: Record<string, unknown>,
|
|
187
|
+
router: ToolRouter,
|
|
188
|
+
callToolFn?: CallToolFn
|
|
189
|
+
): Promise<CallToolResult | null> {
|
|
190
|
+
const resolveToolSchema = (name: string, namespace?: string): { tool?: Tool; error?: CallToolResult } => {
|
|
191
|
+
try {
|
|
192
|
+
return { tool: router.getToolSchema(name, namespace) };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
195
|
+
return {
|
|
196
|
+
error: {
|
|
197
|
+
content: [{ type: 'text', text: errorMessage }],
|
|
198
|
+
isError: true,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
switch (toolName) {
|
|
205
|
+
case 'mcp_search_tool_bm25': {
|
|
206
|
+
const query = String(args.query ?? '');
|
|
207
|
+
const limit = Math.min(Number(args.limit) || 5, 20);
|
|
208
|
+
|
|
209
|
+
const results = await router.searchTools(query, limit);
|
|
210
|
+
|
|
211
|
+
const text = results.length === 0
|
|
212
|
+
? 'No tools found matching your query. Try different keywords.'
|
|
213
|
+
: results
|
|
214
|
+
.map(
|
|
215
|
+
(t, i) =>
|
|
216
|
+
`${i + 1}. **${t.name}** (server: ${t.serverName})\n` +
|
|
217
|
+
` ${t.description}\n` +
|
|
218
|
+
` Estimated tokens: ${t.estimatedTokens}`
|
|
219
|
+
)
|
|
220
|
+
.join('\n');
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: 'text', text }],
|
|
224
|
+
isError: false,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'mcp_search_tool_regex': {
|
|
229
|
+
const pattern = String(args.query ?? '');
|
|
230
|
+
const limit = Math.min(Number(args.limit) || 5, 20);
|
|
231
|
+
|
|
232
|
+
const results = await router.searchToolsRegex(pattern, limit);
|
|
233
|
+
|
|
234
|
+
const text = results.length === 0
|
|
235
|
+
? 'No tools matched your regex pattern. Try a broader pattern.'
|
|
236
|
+
: results
|
|
237
|
+
.map(
|
|
238
|
+
(t, i) =>
|
|
239
|
+
`${i + 1}. **${t.name}** (server: ${t.serverName})\n` +
|
|
240
|
+
` ${t.description}\n` +
|
|
241
|
+
` Estimated tokens: ${t.estimatedTokens}`
|
|
242
|
+
)
|
|
243
|
+
.join('\n');
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: 'text', text }],
|
|
247
|
+
isError: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case 'mcp_get_tool_schema': {
|
|
252
|
+
const name = String(args.toolName ?? '');
|
|
253
|
+
const namespace = String(args.serverName ?? '') || undefined;
|
|
254
|
+
const { tool, error } = resolveToolSchema(name, namespace);
|
|
255
|
+
|
|
256
|
+
if (error) {
|
|
257
|
+
return error;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!tool) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: `Tool "${name}" not found. Use mcp_search_tool_bm25 to find available tools first.`,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
isError: true,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const schema = {
|
|
273
|
+
name: tool.name,
|
|
274
|
+
description: tool.description,
|
|
275
|
+
inputSchema: tool.inputSchema,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: 'text', text: JSON.stringify(schema, null, 2) }],
|
|
280
|
+
isError: false,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'mcp_execute_tool': {
|
|
285
|
+
const targetToolName = String(args.toolName ?? '');
|
|
286
|
+
const namespace = String(args.serverName ?? '') || undefined;
|
|
287
|
+
const toolArgs = (args.args as Record<string, unknown>) ?? {};
|
|
288
|
+
|
|
289
|
+
if (!targetToolName) {
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: 'text', text: 'Missing required parameter "toolName". Specify which tool to execute.' }],
|
|
292
|
+
isError: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Verify the tool exists in our index
|
|
297
|
+
const { tool, error } = resolveToolSchema(targetToolName, namespace);
|
|
298
|
+
if (error) {
|
|
299
|
+
return error;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!tool) {
|
|
303
|
+
return {
|
|
304
|
+
content: [
|
|
305
|
+
{
|
|
306
|
+
type: 'text',
|
|
307
|
+
text: `Tool "${targetToolName}" not found. Use mcp_search_tool_bm25 to discover available tools first.`,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
isError: true,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!callToolFn) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: 'text', text: 'Tool execution is not available. No callToolFn was configured.' }],
|
|
317
|
+
isError: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const result = await callToolFn(targetToolName, toolArgs, namespace);
|
|
323
|
+
|
|
324
|
+
// Normalize result to text
|
|
325
|
+
if (result && typeof result === 'object' && 'content' in result) {
|
|
326
|
+
// Already MCP CallToolResult format
|
|
327
|
+
return result as CallToolResult;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: 'text', text }],
|
|
333
|
+
isError: false,
|
|
334
|
+
};
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: 'text', text: `Tool execution failed: ${errorMessage}` }],
|
|
339
|
+
isError: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
default:
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Check if a tool name is one of the meta-tools. */
|
|
350
|
+
export function isMetaTool(toolName: string): boolean {
|
|
351
|
+
return (
|
|
352
|
+
toolName === 'mcp_search_tool_bm25' ||
|
|
353
|
+
toolName === 'mcp_search_tool_regex' ||
|
|
354
|
+
toolName === 'mcp_get_tool_schema' ||
|
|
355
|
+
toolName === 'mcp_execute_tool'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Unwraps a meta-tool proxy call (like mcp_execute_tool) to find the real target tool name and arguments.
|
|
361
|
+
* Also automatically strips routing prefixes like tool_{serverId}_.
|
|
362
|
+
*
|
|
363
|
+
* Useful for frontend components that need to determine the actual tool being executed by an AI agent.
|
|
364
|
+
*/
|
|
365
|
+
export function resolveMetaToolProxy(
|
|
366
|
+
toolName: string,
|
|
367
|
+
args: Record<string, unknown> | null | undefined
|
|
368
|
+
): { toolName: string; args: Record<string, unknown> } {
|
|
369
|
+
// Unwrap mcp_execute_tool proxy arguments
|
|
370
|
+
if (toolName === 'mcp_execute_tool') {
|
|
371
|
+
const innerName = args?.toolName;
|
|
372
|
+
const innerArgs = args?.args;
|
|
373
|
+
return {
|
|
374
|
+
toolName: typeof innerName === 'string' && innerName ? innerName : toolName,
|
|
375
|
+
args: innerArgs && typeof innerArgs === 'object' && !Array.isArray(innerArgs)
|
|
376
|
+
? (innerArgs as Record<string, unknown>)
|
|
377
|
+
: {},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Strip tool_<serverId>_ prefix used by AIAdapter
|
|
382
|
+
const match = toolName.match(/(?:tool_[^_]+_)?(.+)$/);
|
|
383
|
+
const resolvedName = match?.[1] ?? toolName;
|
|
384
|
+
|
|
385
|
+
return { toolName: resolvedName, args: args ?? {} };
|
|
386
|
+
}
|
|
387
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SchemaCompressor — Utilities for reducing tool schema token overhead.
|
|
3
|
+
*
|
|
4
|
+
* Provides compact representations of tools (name + description only,
|
|
5
|
+
* no inputSchema) and token savings estimation.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { ToolIndex } from './tool-index.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A minimal tool representation containing only what an LLM needs to
|
|
19
|
+
* *decide whether* to use a tool. The full `inputSchema` is deferred.
|
|
20
|
+
*/
|
|
21
|
+
export interface CompactTool {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Human-readable hint about the expected parameters.
|
|
26
|
+
* e.g. "(location: string, unit?: 'celsius' | 'fahrenheit')"
|
|
27
|
+
*/
|
|
28
|
+
parameterHint?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CompressionStats {
|
|
32
|
+
/** Estimated tokens for the *full* tool list. */
|
|
33
|
+
fullTokens: number;
|
|
34
|
+
/** Estimated tokens for the *compact* tool list. */
|
|
35
|
+
compactTokens: number;
|
|
36
|
+
/** Absolute token savings. */
|
|
37
|
+
savedTokens: number;
|
|
38
|
+
/** Percentage savings as a human-readable string, e.g. "82.3%". */
|
|
39
|
+
savingsPercent: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// SchemaCompressor
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export class SchemaCompressor {
|
|
47
|
+
/**
|
|
48
|
+
* Convert a full MCP Tool definition to a compact summary.
|
|
49
|
+
*
|
|
50
|
+
* The compact form omits `inputSchema` entirely and optionally generates
|
|
51
|
+
* a short `parameterHint` from the schema's top-level properties.
|
|
52
|
+
*/
|
|
53
|
+
static toCompact(tool: Tool): CompactTool {
|
|
54
|
+
const compact: CompactTool = {
|
|
55
|
+
name: tool.name,
|
|
56
|
+
description: tool.description,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Build parameter hint from schema
|
|
60
|
+
if (tool.inputSchema && typeof tool.inputSchema === 'object') {
|
|
61
|
+
const schema = tool.inputSchema as {
|
|
62
|
+
properties?: Record<string, { type?: string; enum?: unknown[] }>;
|
|
63
|
+
required?: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (schema.properties) {
|
|
67
|
+
const required = new Set(schema.required ?? []);
|
|
68
|
+
const parts: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (const [key, val] of Object.entries(schema.properties)) {
|
|
71
|
+
const type = val?.type ?? 'any';
|
|
72
|
+
const enumSuffix =
|
|
73
|
+
val?.enum && Array.isArray(val.enum)
|
|
74
|
+
? `: ${val.enum.map((e) => `'${e}'`).join(' | ')}`
|
|
75
|
+
: `: ${type}`;
|
|
76
|
+
|
|
77
|
+
parts.push(required.has(key) ? `${key}${enumSuffix}` : `${key}?${enumSuffix}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (parts.length > 0) {
|
|
81
|
+
compact.parameterHint = `(${parts.join(', ')})`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return compact;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert an array of tools to compact form, optionally limiting the count.
|
|
91
|
+
*/
|
|
92
|
+
static compactAll(tools: Tool[], options?: { maxTools?: number }): CompactTool[] {
|
|
93
|
+
const limited = options?.maxTools ? tools.slice(0, options.maxTools) : tools;
|
|
94
|
+
return limited.map((t) => SchemaCompressor.toCompact(t));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Estimate token savings from using compact vs full tool schemas.
|
|
99
|
+
*/
|
|
100
|
+
static estimateSavings(tools: Tool[]): CompressionStats {
|
|
101
|
+
let fullTokens = 0;
|
|
102
|
+
let compactTokens = 0;
|
|
103
|
+
|
|
104
|
+
for (const tool of tools) {
|
|
105
|
+
fullTokens += ToolIndex.estimateTokens(tool);
|
|
106
|
+
|
|
107
|
+
// Compact form: name + description + parameterHint
|
|
108
|
+
const compact = SchemaCompressor.toCompact(tool);
|
|
109
|
+
const text = [compact.name, compact.description ?? '', compact.parameterHint ?? ''].join(' ');
|
|
110
|
+
// Simple estimation for compact: ~4 chars per token for plain text
|
|
111
|
+
compactTokens += Math.ceil(text.length / 4);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const saved = fullTokens - compactTokens;
|
|
115
|
+
const pct = fullTokens > 0 ? ((saved / fullTokens) * 100).toFixed(1) : '0.0';
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
fullTokens,
|
|
119
|
+
compactTokens,
|
|
120
|
+
savedTokens: saved,
|
|
121
|
+
savingsPercent: `${pct}%`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|