@zmdb/mcp 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +70 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +141 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +153 -0
- package/dist/server.js.map +1 -0
- package/package.json +49 -0
- package/src/client.ts +183 -0
- package/src/index.ts +10 -0
- package/src/server.ts +246 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Transport-neutral MCP client owned by @zmdb/mcp.
|
|
2
|
+
import { MCP_PROTOCOL_VERSION } from './server.js';
|
|
3
|
+
|
|
4
|
+
const PROTOCOL_VERSION_KEY = 'io.modelcontextprotocol/protocolVersion';
|
|
5
|
+
const CLIENT_INFO_KEY = 'io.modelcontextprotocol/clientInfo';
|
|
6
|
+
const CLIENT_CAPABILITIES_KEY = 'io.modelcontextprotocol/clientCapabilities';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_CALLS = 64;
|
|
9
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
type RequestId = string | number;
|
|
12
|
+
|
|
13
|
+
export interface RemoteTool {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly description?: string;
|
|
16
|
+
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RemoteToolResult {
|
|
20
|
+
readonly content: readonly { readonly type: string; readonly text?: string }[];
|
|
21
|
+
readonly isError: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface McpClient {
|
|
25
|
+
listTools(): Promise<readonly RemoteTool[]>;
|
|
26
|
+
callTool(name: string, args: unknown): Promise<RemoteToolResult>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface McpClientOptions {
|
|
30
|
+
readonly maxCalls?: number;
|
|
31
|
+
readonly maxResponseBytes?: number;
|
|
32
|
+
readonly clientInfo?: { readonly name: string; readonly version: string };
|
|
33
|
+
readonly clientCapabilities?: Readonly<Record<string, unknown>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class McpProtocolError extends Error {
|
|
37
|
+
readonly code: number;
|
|
38
|
+
readonly data: unknown;
|
|
39
|
+
|
|
40
|
+
constructor(code: number, message: string, data?: unknown) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'McpProtocolError';
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.data = data;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
49
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
|
+
|
|
51
|
+
const field = (value: Readonly<Record<string, unknown>>, key: string): unknown => Reflect.get(value, key);
|
|
52
|
+
|
|
53
|
+
const positiveSafeInteger = (value: number, name: string): number => {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const boundedResponse = (value: unknown, maxResponseBytes: number): unknown => {
|
|
59
|
+
const serialised = typeof value === 'string' ? value : JSON.stringify(value);
|
|
60
|
+
if (serialised === undefined) throw new TypeError('MCP transport returned a non-JSON value');
|
|
61
|
+
if (new TextEncoder().encode(serialised).byteLength > maxResponseBytes) {
|
|
62
|
+
throw new RangeError(`MCP response exceeds maxResponseBytes (${String(maxResponseBytes)})`);
|
|
63
|
+
}
|
|
64
|
+
if (typeof value !== 'string') return value;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(value);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new TypeError('MCP transport returned invalid JSON');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const responseResult = (
|
|
73
|
+
response: unknown,
|
|
74
|
+
id: RequestId,
|
|
75
|
+
maxResponseBytes: number,
|
|
76
|
+
): Readonly<Record<string, unknown>> => {
|
|
77
|
+
const bounded = boundedResponse(response, maxResponseBytes);
|
|
78
|
+
if (!isRecord(bounded) || field(bounded, 'jsonrpc') !== '2.0' || field(bounded, 'id') !== id) {
|
|
79
|
+
throw new TypeError('invalid MCP response envelope');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rawError = field(bounded, 'error');
|
|
83
|
+
if (rawError !== undefined) {
|
|
84
|
+
if (!isRecord(rawError)) throw new TypeError('invalid MCP error response');
|
|
85
|
+
const code = field(rawError, 'code');
|
|
86
|
+
const message = field(rawError, 'message');
|
|
87
|
+
if (typeof code !== 'number' || !Number.isInteger(code) || typeof message !== 'string') {
|
|
88
|
+
throw new TypeError('invalid MCP error response');
|
|
89
|
+
}
|
|
90
|
+
throw new McpProtocolError(code, message, field(rawError, 'data'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = field(bounded, 'result');
|
|
94
|
+
if (!isRecord(result)) throw new TypeError('invalid MCP result response');
|
|
95
|
+
const resultType = field(result, 'resultType');
|
|
96
|
+
if (resultType !== undefined && resultType !== 'complete') {
|
|
97
|
+
throw new TypeError(`unsupported MCP resultType ${String(resultType)}`);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const remoteToolOf = (value: unknown): RemoteTool => {
|
|
103
|
+
if (!isRecord(value)) throw new TypeError('invalid remote MCP tool');
|
|
104
|
+
const name = field(value, 'name');
|
|
105
|
+
const description = field(value, 'description');
|
|
106
|
+
const inputSchema = field(value, 'inputSchema');
|
|
107
|
+
if (
|
|
108
|
+
typeof name !== 'string' ||
|
|
109
|
+
(description !== undefined && typeof description !== 'string') ||
|
|
110
|
+
!isRecord(inputSchema)
|
|
111
|
+
) {
|
|
112
|
+
throw new TypeError('invalid remote MCP tool');
|
|
113
|
+
}
|
|
114
|
+
return description === undefined ? { name, inputSchema } : { name, description, inputSchema };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const contentBlockOf = (value: unknown): { readonly type: string; readonly text?: string } => {
|
|
118
|
+
if (!isRecord(value)) throw new TypeError('invalid remote MCP content block');
|
|
119
|
+
const type = field(value, 'type');
|
|
120
|
+
const text = field(value, 'text');
|
|
121
|
+
if (typeof type !== 'string' || (text !== undefined && typeof text !== 'string')) {
|
|
122
|
+
throw new TypeError('invalid remote MCP content block');
|
|
123
|
+
}
|
|
124
|
+
return text === undefined ? { ...value, type } : { ...value, type, text };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export function createMcpClient(send: (message: unknown) => Promise<unknown>, opts: McpClientOptions = {}): McpClient {
|
|
128
|
+
const maxCalls = positiveSafeInteger(opts.maxCalls ?? DEFAULT_MAX_CALLS, 'maxCalls');
|
|
129
|
+
const maxResponseBytes = positiveSafeInteger(opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES, 'maxResponseBytes');
|
|
130
|
+
const clientCapabilities = opts.clientCapabilities ?? {};
|
|
131
|
+
let nextId = 1;
|
|
132
|
+
let calls = 0;
|
|
133
|
+
|
|
134
|
+
const request = async (
|
|
135
|
+
method: string,
|
|
136
|
+
params: Readonly<Record<string, unknown>>,
|
|
137
|
+
): Promise<Readonly<Record<string, unknown>>> => {
|
|
138
|
+
if (calls >= maxCalls) throw new RangeError(`MCP client call budget exhausted (${String(maxCalls)})`);
|
|
139
|
+
calls += 1;
|
|
140
|
+
const id = nextId;
|
|
141
|
+
nextId += 1;
|
|
142
|
+
const meta =
|
|
143
|
+
opts.clientInfo === undefined
|
|
144
|
+
? {
|
|
145
|
+
[PROTOCOL_VERSION_KEY]: MCP_PROTOCOL_VERSION,
|
|
146
|
+
[CLIENT_CAPABILITIES_KEY]: clientCapabilities,
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
[PROTOCOL_VERSION_KEY]: MCP_PROTOCOL_VERSION,
|
|
150
|
+
[CLIENT_INFO_KEY]: opts.clientInfo,
|
|
151
|
+
[CLIENT_CAPABILITIES_KEY]: clientCapabilities,
|
|
152
|
+
};
|
|
153
|
+
const response = await send({
|
|
154
|
+
jsonrpc: '2.0',
|
|
155
|
+
id,
|
|
156
|
+
method,
|
|
157
|
+
params: { ...params, _meta: meta },
|
|
158
|
+
});
|
|
159
|
+
return responseResult(response, id, maxResponseBytes);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
async listTools(): Promise<readonly RemoteTool[]> {
|
|
164
|
+
const result = await request('tools/list', {});
|
|
165
|
+
const tools = field(result, 'tools');
|
|
166
|
+
if (!Array.isArray(tools)) throw new TypeError('invalid MCP tools/list result');
|
|
167
|
+
return tools.map(remoteToolOf);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
async callTool(name: string, args: unknown): Promise<RemoteToolResult> {
|
|
171
|
+
const result = await request('tools/call', { name, arguments: args });
|
|
172
|
+
const content = field(result, 'content');
|
|
173
|
+
const isError = field(result, 'isError');
|
|
174
|
+
if (!Array.isArray(content) || (isError !== undefined && typeof isError !== 'boolean')) {
|
|
175
|
+
throw new TypeError('invalid MCP tools/call result');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
content: content.map(contentBlockOf),
|
|
179
|
+
isError: isError ?? false,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The complete public surface of @zmdb/mcp.
|
|
2
|
+
export { MCP_PROTOCOL_VERSION, createMcpServer, type McpServer, type McpServerOptions } from './server.js';
|
|
3
|
+
export {
|
|
4
|
+
McpProtocolError,
|
|
5
|
+
createMcpClient,
|
|
6
|
+
type McpClient,
|
|
7
|
+
type McpClientOptions,
|
|
8
|
+
type RemoteTool,
|
|
9
|
+
type RemoteToolResult,
|
|
10
|
+
} from './client.js';
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { ToolRegistry } from '@zmdb/ai/chat';
|
|
2
|
+
import { invokeTool } from '@zmdb/ai/tool-runtime';
|
|
3
|
+
|
|
4
|
+
export const MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
5
|
+
|
|
6
|
+
const PARSE_ERROR = -32_700;
|
|
7
|
+
const INVALID_REQUEST = -32_600;
|
|
8
|
+
const METHOD_NOT_FOUND = -32_601;
|
|
9
|
+
const INVALID_PARAMS = -32_602;
|
|
10
|
+
const UNSUPPORTED_PROTOCOL_VERSION = -32_022;
|
|
11
|
+
|
|
12
|
+
const PROTOCOL_VERSION_KEY = 'io.modelcontextprotocol/protocolVersion';
|
|
13
|
+
const CLIENT_CAPABILITIES_KEY = 'io.modelcontextprotocol/clientCapabilities';
|
|
14
|
+
const SERVER_INFO_KEY = 'io.modelcontextprotocol/serverInfo';
|
|
15
|
+
|
|
16
|
+
type RequestId = string | number;
|
|
17
|
+
|
|
18
|
+
const toolErrorId = (): string =>
|
|
19
|
+
[...globalThis.crypto.getRandomValues(new Uint8Array(4))].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
20
|
+
|
|
21
|
+
interface ParsedRequest {
|
|
22
|
+
readonly kind: 'request';
|
|
23
|
+
readonly id: RequestId;
|
|
24
|
+
readonly method: string;
|
|
25
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ParsedNotification {
|
|
29
|
+
readonly kind: 'notification';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface InvalidMessage {
|
|
33
|
+
readonly kind: 'invalid';
|
|
34
|
+
readonly id: RequestId | null;
|
|
35
|
+
readonly code: number;
|
|
36
|
+
readonly message: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ParsedMessage = ParsedRequest | ParsedNotification | InvalidMessage;
|
|
40
|
+
|
|
41
|
+
export interface McpServer {
|
|
42
|
+
handle(message: unknown, transport: unknown): Promise<unknown | undefined>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface McpServerOptions {
|
|
46
|
+
readonly serverInfo: { readonly name: string; readonly version: string };
|
|
47
|
+
readonly identify: (transport: unknown) => Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type McpToolHandler = {
|
|
51
|
+
bivarianceHack(input: unknown, identity?: unknown): unknown | PromiseLike<unknown>;
|
|
52
|
+
}['bivarianceHack'];
|
|
53
|
+
|
|
54
|
+
interface McpToolEntry {
|
|
55
|
+
readonly spec: {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly description?: string;
|
|
58
|
+
readonly parameters: unknown;
|
|
59
|
+
};
|
|
60
|
+
readonly validate: (args: unknown) => unknown;
|
|
61
|
+
readonly handler: McpToolHandler;
|
|
62
|
+
readonly effectful?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type McpToolRegistry = Readonly<Record<string, McpToolEntry>>;
|
|
66
|
+
|
|
67
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
68
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
|
+
|
|
70
|
+
const field = (value: Readonly<Record<string, unknown>>, key: string): unknown => Reflect.get(value, key);
|
|
71
|
+
|
|
72
|
+
const requestIdOf = (value: unknown): RequestId | null => {
|
|
73
|
+
if (typeof value === 'string') return value;
|
|
74
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const parseMessage = (message: unknown): ParsedMessage => {
|
|
78
|
+
let value: unknown = message;
|
|
79
|
+
if (typeof message === 'string') {
|
|
80
|
+
try {
|
|
81
|
+
value = JSON.parse(message);
|
|
82
|
+
} catch {
|
|
83
|
+
return { kind: 'invalid', id: null, code: PARSE_ERROR, message: 'Parse error' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isRecord(value)) {
|
|
88
|
+
return { kind: 'invalid', id: null, code: INVALID_REQUEST, message: 'Invalid Request' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rawId = field(value, 'id');
|
|
92
|
+
const id = requestIdOf(rawId);
|
|
93
|
+
if (field(value, 'jsonrpc') !== '2.0' || typeof field(value, 'method') !== 'string') {
|
|
94
|
+
return { kind: 'invalid', id, code: INVALID_REQUEST, message: 'Invalid Request' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!Object.hasOwn(value, 'id')) return { kind: 'notification' };
|
|
98
|
+
if (id === null) return { kind: 'invalid', id: null, code: INVALID_REQUEST, message: 'Invalid Request' };
|
|
99
|
+
|
|
100
|
+
const method = field(value, 'method');
|
|
101
|
+
const rawParams = field(value, 'params');
|
|
102
|
+
if (rawParams !== undefined && !isRecord(rawParams)) {
|
|
103
|
+
return { kind: 'invalid', id, code: INVALID_PARAMS, message: 'Invalid params' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
kind: 'request',
|
|
108
|
+
id,
|
|
109
|
+
method: typeof method === 'string' ? method : '',
|
|
110
|
+
params: rawParams ?? {},
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const errorResponse = (
|
|
115
|
+
id: RequestId | null,
|
|
116
|
+
code: number,
|
|
117
|
+
message: string,
|
|
118
|
+
data?: unknown,
|
|
119
|
+
): Readonly<Record<string, unknown>> => ({
|
|
120
|
+
jsonrpc: '2.0',
|
|
121
|
+
id,
|
|
122
|
+
error: data === undefined ? { code, message } : { code, message, data },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const responseMeta = (
|
|
126
|
+
serverInfo: McpServerOptions['serverInfo'],
|
|
127
|
+
): Readonly<Record<string, Readonly<Record<string, string>>>> => ({
|
|
128
|
+
[SERVER_INFO_KEY]: serverInfo,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const resultResponse = (
|
|
132
|
+
id: RequestId,
|
|
133
|
+
serverInfo: McpServerOptions['serverInfo'],
|
|
134
|
+
result: Readonly<Record<string, unknown>>,
|
|
135
|
+
): Readonly<Record<string, unknown>> => ({
|
|
136
|
+
jsonrpc: '2.0',
|
|
137
|
+
id,
|
|
138
|
+
result: {
|
|
139
|
+
...result,
|
|
140
|
+
resultType: 'complete',
|
|
141
|
+
_meta: responseMeta(serverInfo),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const protocolVersionOf = (params: Readonly<Record<string, unknown>>): string | undefined => {
|
|
146
|
+
const meta = field(params, '_meta');
|
|
147
|
+
if (!isRecord(meta)) return undefined;
|
|
148
|
+
const version = field(meta, PROTOCOL_VERSION_KEY);
|
|
149
|
+
const capabilities = field(meta, CLIENT_CAPABILITIES_KEY);
|
|
150
|
+
return typeof version === 'string' && isRecord(capabilities) ? version : undefined;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const toolCallOf = (
|
|
154
|
+
params: Readonly<Record<string, unknown>>,
|
|
155
|
+
): { readonly name: string; readonly args: unknown } | undefined => {
|
|
156
|
+
const name = field(params, 'name');
|
|
157
|
+
if (typeof name !== 'string') return undefined;
|
|
158
|
+
return { name, args: field(params, 'arguments') ?? {} };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const contentResult = (
|
|
162
|
+
id: RequestId,
|
|
163
|
+
serverInfo: McpServerOptions['serverInfo'],
|
|
164
|
+
text: string,
|
|
165
|
+
isError: boolean,
|
|
166
|
+
): Readonly<Record<string, unknown>> =>
|
|
167
|
+
resultResponse(id, serverInfo, {
|
|
168
|
+
content: [{ type: 'text', text }],
|
|
169
|
+
isError,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// The public declaration uses the structural subset MCP consumes so the
|
|
173
|
+
// provider-neutral @zmdb/ai/chat implementation cannot leak a provider SDK type.
|
|
174
|
+
// The implementation signature still proves compatibility with AI's ToolRegistry.
|
|
175
|
+
export function createMcpServer(tools: McpToolRegistry, opts: McpServerOptions): McpServer;
|
|
176
|
+
export function createMcpServer(tools: McpToolRegistry | ToolRegistry, opts: McpServerOptions): McpServer {
|
|
177
|
+
const entries = Object.entries(tools);
|
|
178
|
+
for (const [key, entry] of entries) {
|
|
179
|
+
if (entry.spec.name !== key) {
|
|
180
|
+
throw new Error(`tool registry key ${key} does not match spec name ${entry.spec.name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const entriesByName = new Map(entries);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
async handle(message: unknown, transport: unknown): Promise<unknown | undefined> {
|
|
187
|
+
const identity = await opts.identify(transport);
|
|
188
|
+
const parsed = parseMessage(message);
|
|
189
|
+
if (parsed.kind === 'notification') return undefined;
|
|
190
|
+
if (parsed.kind === 'invalid') {
|
|
191
|
+
return errorResponse(parsed.id, parsed.code, parsed.message);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const protocolVersion = protocolVersionOf(parsed.params);
|
|
195
|
+
if (protocolVersion === undefined) {
|
|
196
|
+
return errorResponse(parsed.id, INVALID_PARAMS, 'Missing required MCP request metadata');
|
|
197
|
+
}
|
|
198
|
+
if (protocolVersion !== MCP_PROTOCOL_VERSION) {
|
|
199
|
+
return errorResponse(parsed.id, UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', {
|
|
200
|
+
supported: [MCP_PROTOCOL_VERSION],
|
|
201
|
+
requested: protocolVersion,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (parsed.method === 'server/discover') {
|
|
206
|
+
return resultResponse(parsed.id, opts.serverInfo, {
|
|
207
|
+
supportedVersions: [MCP_PROTOCOL_VERSION],
|
|
208
|
+
capabilities: { tools: {} },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (parsed.method === 'tools/list') {
|
|
213
|
+
return resultResponse(parsed.id, opts.serverInfo, {
|
|
214
|
+
tools: entries.map(([, entry]) =>
|
|
215
|
+
entry.spec.description === undefined
|
|
216
|
+
? { name: entry.spec.name, inputSchema: entry.spec.parameters }
|
|
217
|
+
: {
|
|
218
|
+
name: entry.spec.name,
|
|
219
|
+
description: entry.spec.description,
|
|
220
|
+
inputSchema: entry.spec.parameters,
|
|
221
|
+
},
|
|
222
|
+
),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (parsed.method !== 'tools/call') {
|
|
227
|
+
return errorResponse(parsed.id, METHOD_NOT_FOUND, 'Method not found');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const call = toolCallOf(parsed.params);
|
|
231
|
+
if (call === undefined) return errorResponse(parsed.id, INVALID_PARAMS, 'Invalid tool call');
|
|
232
|
+
const entry = entriesByName.get(call.name);
|
|
233
|
+
if (entry === undefined) return errorResponse(parsed.id, INVALID_PARAMS, `unknown tool ${call.name}`);
|
|
234
|
+
|
|
235
|
+
const invocation = await invokeTool(entry, call.args, identity);
|
|
236
|
+
if (invocation.kind === 'success') {
|
|
237
|
+
return contentResult(parsed.id, opts.serverInfo, invocation.content, false);
|
|
238
|
+
}
|
|
239
|
+
if (invocation.kind === 'validation-error' && invocation.content !== undefined) {
|
|
240
|
+
return contentResult(parsed.id, opts.serverInfo, invocation.content, true);
|
|
241
|
+
}
|
|
242
|
+
const id = toolErrorId();
|
|
243
|
+
return contentResult(parsed.id, opts.serverInfo, `tool ${call.name} failed (${id})`, true);
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|