toolcraft 0.0.103 → 0.0.105
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/composition.json +7 -2
- package/dist/cli.js +53 -0
- package/dist/composition.json +7 -2
- package/dist/index.d.ts +25 -3
- package/dist/index.js +18 -0
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.js +121 -1
- package/dist/sdk.d.ts +2 -1
- package/dist/sdk.js +56 -0
- package/dist/stream.d.ts +19 -0
- package/dist/stream.js +92 -0
- package/dist/testing/harness.d.ts +11 -0
- package/dist/testing/harness.js +70 -0
- package/dist/testing/index.d.ts +1 -1
- package/node_modules/tiny-stdio-mcp-server/LICENSE +21 -0
- package/node_modules/tiny-stdio-mcp-server/README.md +231 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/audio.d.ts +15 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/audio.js +84 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/convert.d.ts +16 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/convert.js +61 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file-type.d.ts +11 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file-type.js +93 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file.d.ts +28 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/file.js +110 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/image.d.ts +15 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/image.js +72 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/index.d.ts +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/index.js +9 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/mime.d.ts +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/mime.js +52 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/remote.d.ts +5 -0
- package/node_modules/tiny-stdio-mcp-server/dist/content/remote.js +69 -0
- package/node_modules/tiny-stdio-mcp-server/dist/index.d.ts +9 -0
- package/node_modules/tiny-stdio-mcp-server/dist/index.js +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.d.ts +14 -0
- package/node_modules/tiny-stdio-mcp-server/dist/jsonrpc.js +118 -0
- package/node_modules/tiny-stdio-mcp-server/dist/schema.d.ts +20 -0
- package/node_modules/tiny-stdio-mcp-server/dist/schema.js +26 -0
- package/node_modules/tiny-stdio-mcp-server/dist/server.d.ts +35 -0
- package/node_modules/tiny-stdio-mcp-server/dist/server.js +865 -0
- package/node_modules/tiny-stdio-mcp-server/dist/testing.d.ts +7 -0
- package/node_modules/tiny-stdio-mcp-server/dist/testing.js +20 -0
- package/node_modules/tiny-stdio-mcp-server/dist/types.d.ts +247 -0
- package/node_modules/tiny-stdio-mcp-server/dist/types.js +22 -0
- package/node_modules/tiny-stdio-mcp-server/package.json +56 -0
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +9 -5
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import AjvModule from "ajv";
|
|
3
|
+
import uriTemplateParser from "uri-template";
|
|
4
|
+
import UriTemplate from "uri-template-lite";
|
|
5
|
+
import { JSON_RPC_ERROR_CODES, ToolError } from "./types.js";
|
|
6
|
+
import { parseMessage, formatSuccessResponse, formatErrorResponse } from "./jsonrpc.js";
|
|
7
|
+
import { toContentBlocks } from "./content/convert.js";
|
|
8
|
+
const PROTOCOL_VERSION = "2025-11-25";
|
|
9
|
+
const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-03-26", "2025-06-18", PROTOCOL_VERSION]);
|
|
10
|
+
export function createServer(options) {
|
|
11
|
+
if (options.toolCallTimeoutMs !== undefined &&
|
|
12
|
+
(!Number.isInteger(options.toolCallTimeoutMs) || options.toolCallTimeoutMs <= 0)) {
|
|
13
|
+
throw new Error("toolCallTimeoutMs must be a positive integer.");
|
|
14
|
+
}
|
|
15
|
+
const Ajv = "default" in AjvModule ? AjvModule.default : AjvModule;
|
|
16
|
+
const jsonSchemaValidator = new Ajv({ strict: false });
|
|
17
|
+
const supportNotifications = options.supportNotifications !== false;
|
|
18
|
+
const supportResourceSubscriptions = options.supportResourceSubscriptions !== false;
|
|
19
|
+
const tools = new Map();
|
|
20
|
+
const prompts = new Map();
|
|
21
|
+
const resources = new Map();
|
|
22
|
+
const resourceTemplates = new Map();
|
|
23
|
+
const methods = new Map();
|
|
24
|
+
const notificationListeners = new Set();
|
|
25
|
+
const connectionNotificationListeners = new Map();
|
|
26
|
+
const defaultLifecycle = {
|
|
27
|
+
initialized: false,
|
|
28
|
+
initializeAccepted: false,
|
|
29
|
+
notificationReady: false,
|
|
30
|
+
resourceSubscriptions: new Set(),
|
|
31
|
+
abortController: new AbortController()
|
|
32
|
+
};
|
|
33
|
+
const messageLifecycles = new Set([defaultLifecycle]);
|
|
34
|
+
const handleMessageWithLifecycle = async (method, lifecycle, params) => {
|
|
35
|
+
// Allow ping and initialize before initialization
|
|
36
|
+
if (method === "ping") {
|
|
37
|
+
return { result: {} };
|
|
38
|
+
}
|
|
39
|
+
if (method === "initialize") {
|
|
40
|
+
// Re-initialize on the same connection is idempotent: real MCP clients
|
|
41
|
+
// (e.g. kimi-cli via fastmcp) re-send `initialize` on a persistent
|
|
42
|
+
// connection per tool call, and the official MCP SDK server re-responds
|
|
43
|
+
// with InitializeResult instead of erroring. Per-connection isolation is
|
|
44
|
+
// still enforced by the separate lifecycle object given to each connection.
|
|
45
|
+
lifecycle.initializeAccepted = true;
|
|
46
|
+
lifecycle.initialized = true;
|
|
47
|
+
lifecycle.notificationReady = false;
|
|
48
|
+
const requestedProtocol = typeof params?.protocolVersion === "string" ? params.protocolVersion : undefined;
|
|
49
|
+
const result = {
|
|
50
|
+
protocolVersion: requestedProtocol !== undefined && SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol)
|
|
51
|
+
? requestedProtocol
|
|
52
|
+
: PROTOCOL_VERSION,
|
|
53
|
+
capabilities: {
|
|
54
|
+
tools: {
|
|
55
|
+
...(supportNotifications ? { listChanged: true } : {})
|
|
56
|
+
},
|
|
57
|
+
prompts: {
|
|
58
|
+
...(supportNotifications ? { listChanged: true } : {})
|
|
59
|
+
},
|
|
60
|
+
resources: {
|
|
61
|
+
...(supportNotifications ? { listChanged: true } : {}),
|
|
62
|
+
...(supportResourceSubscriptions ? { subscribe: true } : {})
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
serverInfo: {
|
|
66
|
+
name: options.name,
|
|
67
|
+
version: options.version
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return { result };
|
|
71
|
+
}
|
|
72
|
+
if (method === "notifications/initialized") {
|
|
73
|
+
if (!lifecycle.initializeAccepted) {
|
|
74
|
+
return {
|
|
75
|
+
error: {
|
|
76
|
+
code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
|
|
77
|
+
message: "Server not initialized"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
lifecycle.notificationReady = true;
|
|
82
|
+
return { result: undefined };
|
|
83
|
+
}
|
|
84
|
+
// All other methods require initialization
|
|
85
|
+
if (!lifecycle.initialized) {
|
|
86
|
+
return {
|
|
87
|
+
error: {
|
|
88
|
+
code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
|
|
89
|
+
message: "Server not initialized"
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (method === "tools/list") {
|
|
94
|
+
const toolList = [];
|
|
95
|
+
for (const tool of tools.values()) {
|
|
96
|
+
const descriptor = { ...tool };
|
|
97
|
+
delete descriptor.handler;
|
|
98
|
+
delete descriptor.inputValidator;
|
|
99
|
+
delete descriptor.outputValidator;
|
|
100
|
+
toolList.push({
|
|
101
|
+
...descriptor
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { result: { tools: toolList } };
|
|
105
|
+
}
|
|
106
|
+
if (method === "tools/call") {
|
|
107
|
+
const toolName = params?.name;
|
|
108
|
+
if (!toolName) {
|
|
109
|
+
return {
|
|
110
|
+
error: {
|
|
111
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
112
|
+
message: "Tool name required"
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const tool = tools.get(toolName);
|
|
117
|
+
if (!tool) {
|
|
118
|
+
return {
|
|
119
|
+
error: {
|
|
120
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
121
|
+
message: `Tool not found: ${toolName}`
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const toolArgs = (params?.arguments ?? {});
|
|
126
|
+
if (options.validateToolArguments !== false && !tool.inputValidator(toolArgs)) {
|
|
127
|
+
const errors = tool.inputValidator.errors ?? [];
|
|
128
|
+
return {
|
|
129
|
+
error: {
|
|
130
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
131
|
+
message: `Invalid tool arguments: ${jsonSchemaValidator.errorsText(errors)}`,
|
|
132
|
+
data: errors
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
let handlerResult;
|
|
138
|
+
if (options.toolCallTimeoutMs === undefined) {
|
|
139
|
+
handlerResult = await tool.handler(toolArgs);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
let timeout;
|
|
143
|
+
const handlerPromise = Promise.resolve().then(() => tool.handler(toolArgs));
|
|
144
|
+
handlerResult = await Promise.race([
|
|
145
|
+
handlerPromise,
|
|
146
|
+
new Promise((_resolve, reject) => {
|
|
147
|
+
timeout = setTimeout(() => {
|
|
148
|
+
reject(new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Tool call timed out: ${toolName}`));
|
|
149
|
+
}, options.toolCallTimeoutMs);
|
|
150
|
+
})
|
|
151
|
+
]).finally(() => {
|
|
152
|
+
if (timeout !== undefined) {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const result = normalizeToolResult(handlerResult, tool.outputSchema);
|
|
158
|
+
if (result.isError !== true &&
|
|
159
|
+
tool.outputValidator !== undefined &&
|
|
160
|
+
!tool.outputValidator(result.structuredContent)) {
|
|
161
|
+
const errors = tool.outputValidator.errors ?? [];
|
|
162
|
+
throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Invalid structured tool result: ${jsonSchemaValidator.errorsText(errors)}`, errors);
|
|
163
|
+
}
|
|
164
|
+
return { result };
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
if (err instanceof ToolError) {
|
|
168
|
+
return {
|
|
169
|
+
error: {
|
|
170
|
+
code: err.code,
|
|
171
|
+
message: err.message,
|
|
172
|
+
...(err.data === undefined ? {} : { data: err.data })
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
177
|
+
const result = {
|
|
178
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
179
|
+
isError: true
|
|
180
|
+
};
|
|
181
|
+
return { result };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (method === "prompts/list") {
|
|
185
|
+
return {
|
|
186
|
+
result: {
|
|
187
|
+
prompts: [...prompts.values()].map(({ handler: _handler, ...prompt }) => prompt)
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (method === "prompts/get") {
|
|
192
|
+
const promptName = typeof params?.name === "string" ? params.name : undefined;
|
|
193
|
+
if (promptName === undefined) {
|
|
194
|
+
return invalidParams("Prompt name required");
|
|
195
|
+
}
|
|
196
|
+
const prompt = prompts.get(promptName);
|
|
197
|
+
if (prompt === undefined) {
|
|
198
|
+
return invalidParams(`Prompt not found: ${promptName}`);
|
|
199
|
+
}
|
|
200
|
+
const args = toStringArguments(params?.arguments);
|
|
201
|
+
if (args === undefined || !hasRequiredPromptArguments(prompt, args)) {
|
|
202
|
+
return invalidParams("Invalid prompt arguments");
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const result = await prompt.handler(args);
|
|
206
|
+
if (!isGetPromptResult(result)) {
|
|
207
|
+
return internalError("Invalid prompt result");
|
|
208
|
+
}
|
|
209
|
+
return { result };
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return internalError(toErrorMessage(error));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (method === "resources/list") {
|
|
216
|
+
return {
|
|
217
|
+
result: {
|
|
218
|
+
resources: [...resources.values()].map(({ handler: _handler, ...resource }) => resource)
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (method === "resources/templates/list") {
|
|
223
|
+
return {
|
|
224
|
+
result: {
|
|
225
|
+
resourceTemplates: [...resourceTemplates.values()].map(({ handler: _handler, ...resourceTemplate }) => resourceTemplate)
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (method === "resources/read") {
|
|
230
|
+
const uri = typeof params?.uri === "string" ? params.uri : undefined;
|
|
231
|
+
if (uri === undefined || !isValidUri(uri)) {
|
|
232
|
+
return invalidParams("Resource URI required");
|
|
233
|
+
}
|
|
234
|
+
const resource = findReadableResource(uri, resources, resourceTemplates);
|
|
235
|
+
if (resource === undefined) {
|
|
236
|
+
return resourceNotFound(uri);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const result = await resource.handler(uri);
|
|
240
|
+
if (!isReadResourceResult(result)) {
|
|
241
|
+
return internalError("Invalid resource result");
|
|
242
|
+
}
|
|
243
|
+
return { result };
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
return internalError(toErrorMessage(error));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (method === "resources/subscribe" || method === "resources/unsubscribe") {
|
|
250
|
+
if (!supportResourceSubscriptions) {
|
|
251
|
+
return {
|
|
252
|
+
error: {
|
|
253
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
254
|
+
message: "Method not found"
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const uri = typeof params?.uri === "string" ? params.uri : undefined;
|
|
259
|
+
if (uri === undefined || !isValidUri(uri)) {
|
|
260
|
+
return invalidParams("Resource URI required");
|
|
261
|
+
}
|
|
262
|
+
if (method === "resources/subscribe" &&
|
|
263
|
+
findReadableResource(uri, resources, resourceTemplates) === undefined) {
|
|
264
|
+
return resourceNotFound(uri);
|
|
265
|
+
}
|
|
266
|
+
if (method === "resources/subscribe") {
|
|
267
|
+
lifecycle.resourceSubscriptions.add(uri);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
lifecycle.resourceSubscriptions.delete(uri);
|
|
271
|
+
}
|
|
272
|
+
return { result: {} };
|
|
273
|
+
}
|
|
274
|
+
const customMethod = methods.get(method);
|
|
275
|
+
if (customMethod !== undefined) {
|
|
276
|
+
try {
|
|
277
|
+
const result = await customMethod(params, {
|
|
278
|
+
signal: lifecycle.abortController.signal,
|
|
279
|
+
async notify(notificationMethod, notificationParams) {
|
|
280
|
+
if (!lifecycle.notificationReady || lifecycle.listener === undefined) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await lifecycle.listener({
|
|
284
|
+
jsonrpc: "2.0",
|
|
285
|
+
method: notificationMethod,
|
|
286
|
+
...(notificationParams === undefined ? {} : { params: notificationParams })
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return { result };
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return internalError(toErrorMessage(error));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
error: {
|
|
298
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
299
|
+
message: "Method not found"
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const createMessageSession = (listener) => {
|
|
304
|
+
const lifecycle = {
|
|
305
|
+
initialized: false,
|
|
306
|
+
initializeAccepted: false,
|
|
307
|
+
notificationReady: false,
|
|
308
|
+
resourceSubscriptions: new Set(),
|
|
309
|
+
abortController: new AbortController(),
|
|
310
|
+
listener
|
|
311
|
+
};
|
|
312
|
+
messageLifecycles.add(lifecycle);
|
|
313
|
+
if (listener !== undefined) {
|
|
314
|
+
connectionNotificationListeners.set(listener, lifecycle);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
handleMessage: (method, params) => handleMessageWithLifecycle(method, lifecycle, params),
|
|
318
|
+
close: () => {
|
|
319
|
+
lifecycle.abortController.abort();
|
|
320
|
+
if (listener !== undefined) {
|
|
321
|
+
connectionNotificationListeners.delete(listener);
|
|
322
|
+
}
|
|
323
|
+
messageLifecycles.delete(lifecycle);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
const handleMessage = (method, params) => handleMessageWithLifecycle(method, defaultLifecycle, params);
|
|
328
|
+
const processLine = async (line, write, messageHandler) => {
|
|
329
|
+
const parsed = parseMessage(line);
|
|
330
|
+
if (!parsed.success) {
|
|
331
|
+
write(formatErrorResponse(parsed.id, parsed.error) + "\n");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const { request, isNotification } = parsed;
|
|
335
|
+
if (isNotification && request.method === "initialize") {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!isNotification && request.method === "notifications/initialized") {
|
|
339
|
+
const requestWithId = request;
|
|
340
|
+
write(formatErrorResponse(requestWithId.id, {
|
|
341
|
+
code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
|
|
342
|
+
message: "Invalid Request"
|
|
343
|
+
}) + "\n");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
let handled;
|
|
347
|
+
try {
|
|
348
|
+
handled = await messageHandler(request.method, request.params);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
if (!isNotification) {
|
|
352
|
+
const requestWithId = request;
|
|
353
|
+
write(formatErrorResponse(requestWithId.id, {
|
|
354
|
+
code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
355
|
+
message: "Internal error"
|
|
356
|
+
}) + "\n");
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const { result, error } = handled;
|
|
361
|
+
if (isNotification) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const requestWithId = request;
|
|
365
|
+
if (error) {
|
|
366
|
+
write(formatErrorResponse(requestWithId.id, error) + "\n");
|
|
367
|
+
}
|
|
368
|
+
else if (result !== undefined) {
|
|
369
|
+
write(formatSuccessResponse(requestWithId.id, result) + "\n");
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const broadcastNotification = async (method, params, canSend = () => true) => {
|
|
373
|
+
const notification = {
|
|
374
|
+
jsonrpc: "2.0",
|
|
375
|
+
method,
|
|
376
|
+
...(params === undefined ? {} : { params })
|
|
377
|
+
};
|
|
378
|
+
for (const listener of notificationListeners) {
|
|
379
|
+
listener(notification);
|
|
380
|
+
}
|
|
381
|
+
await Promise.all([...connectionNotificationListeners].map(async ([listener, lifecycle]) => {
|
|
382
|
+
if (lifecycle.notificationReady && canSend(lifecycle)) {
|
|
383
|
+
await listener(notification);
|
|
384
|
+
}
|
|
385
|
+
}));
|
|
386
|
+
};
|
|
387
|
+
const server = {
|
|
388
|
+
tool(name, description, inputSchema, handler, outputSchema) {
|
|
389
|
+
assertNonEmptyName(name, "Tool name required");
|
|
390
|
+
const inputValidator = jsonSchemaValidator.compile(inputSchema);
|
|
391
|
+
let outputValidator;
|
|
392
|
+
if (outputSchema !== undefined) {
|
|
393
|
+
assertObjectRootSchema(outputSchema, "outputSchema");
|
|
394
|
+
outputValidator = jsonSchemaValidator.compile(outputSchema);
|
|
395
|
+
}
|
|
396
|
+
tools.set(name, {
|
|
397
|
+
name,
|
|
398
|
+
description,
|
|
399
|
+
inputSchema: inputSchema,
|
|
400
|
+
...(outputSchema === undefined ? {} : { outputSchema: outputSchema }),
|
|
401
|
+
handler: handler,
|
|
402
|
+
inputValidator,
|
|
403
|
+
...(outputValidator === undefined ? {} : { outputValidator })
|
|
404
|
+
});
|
|
405
|
+
return server;
|
|
406
|
+
},
|
|
407
|
+
registerTool(definition, handler) {
|
|
408
|
+
assertNonEmptyName(definition.name, "Tool name required");
|
|
409
|
+
const inputValidator = jsonSchemaValidator.compile(definition.inputSchema);
|
|
410
|
+
let outputValidator;
|
|
411
|
+
if (definition.outputSchema !== undefined) {
|
|
412
|
+
assertObjectRootSchema(definition.outputSchema, "outputSchema");
|
|
413
|
+
outputValidator = jsonSchemaValidator.compile(definition.outputSchema);
|
|
414
|
+
}
|
|
415
|
+
tools.set(definition.name, {
|
|
416
|
+
...definition,
|
|
417
|
+
handler: handler,
|
|
418
|
+
inputValidator,
|
|
419
|
+
...(outputValidator === undefined ? {} : { outputValidator })
|
|
420
|
+
});
|
|
421
|
+
return server;
|
|
422
|
+
},
|
|
423
|
+
prompt(definition, handler) {
|
|
424
|
+
assertNonEmptyName(definition.name, "Prompt name required");
|
|
425
|
+
prompts.set(definition.name, { ...definition, handler });
|
|
426
|
+
return server;
|
|
427
|
+
},
|
|
428
|
+
resource(definition, handler) {
|
|
429
|
+
if (!isValidUri(definition.uri)) {
|
|
430
|
+
throw new Error(`Invalid resource URI: ${definition.uri}`);
|
|
431
|
+
}
|
|
432
|
+
resources.set(definition.uri, { ...definition, handler });
|
|
433
|
+
return server;
|
|
434
|
+
},
|
|
435
|
+
resourceTemplate(definition, handler) {
|
|
436
|
+
assertReadableUriTemplate(definition.uriTemplate);
|
|
437
|
+
new UriTemplate(definition.uriTemplate);
|
|
438
|
+
resourceTemplates.set(definition.uriTemplate, { ...definition, handler });
|
|
439
|
+
return server;
|
|
440
|
+
},
|
|
441
|
+
method(name, handler) {
|
|
442
|
+
assertNonEmptyName(name, "Method name required");
|
|
443
|
+
methods.set(name, handler);
|
|
444
|
+
return server;
|
|
445
|
+
},
|
|
446
|
+
onNotification(listener) {
|
|
447
|
+
notificationListeners.add(listener);
|
|
448
|
+
return () => {
|
|
449
|
+
notificationListeners.delete(listener);
|
|
450
|
+
};
|
|
451
|
+
},
|
|
452
|
+
removeTool(name) {
|
|
453
|
+
return tools.delete(name);
|
|
454
|
+
},
|
|
455
|
+
removePrompt(name) {
|
|
456
|
+
return prompts.delete(name);
|
|
457
|
+
},
|
|
458
|
+
removeResource(uri) {
|
|
459
|
+
return resources.delete(uri);
|
|
460
|
+
},
|
|
461
|
+
removeResourceTemplate(uriTemplate) {
|
|
462
|
+
return resourceTemplates.delete(uriTemplate);
|
|
463
|
+
},
|
|
464
|
+
async notifyToolsChanged() {
|
|
465
|
+
if (supportNotifications &&
|
|
466
|
+
[...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
|
|
467
|
+
await broadcastNotification("notifications/tools/list_changed");
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
async notifyPromptsChanged() {
|
|
471
|
+
if (supportNotifications &&
|
|
472
|
+
[...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
|
|
473
|
+
await broadcastNotification("notifications/prompts/list_changed");
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
async notifyResourcesChanged() {
|
|
477
|
+
if (supportNotifications &&
|
|
478
|
+
[...messageLifecycles].some((lifecycle) => lifecycle.notificationReady)) {
|
|
479
|
+
await broadcastNotification("notifications/resources/list_changed");
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
async notifyResourceUpdated(uri) {
|
|
483
|
+
if (!supportResourceSubscriptions) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await broadcastNotification("notifications/resources/updated", { uri }, (lifecycle) => lifecycle.resourceSubscriptions.has(uri));
|
|
487
|
+
},
|
|
488
|
+
createMessageSession,
|
|
489
|
+
handleMessage,
|
|
490
|
+
async listen() {
|
|
491
|
+
return server.connect({
|
|
492
|
+
readable: process.stdin,
|
|
493
|
+
writable: process.stdout
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
async connect(transport) {
|
|
497
|
+
return new Promise((resolve) => {
|
|
498
|
+
const listener = (notification) => {
|
|
499
|
+
transport.writable.write(`${JSON.stringify(notification)}\n`);
|
|
500
|
+
};
|
|
501
|
+
const session = server.createMessageSession(listener);
|
|
502
|
+
const rl = readline.createInterface({
|
|
503
|
+
input: transport.readable,
|
|
504
|
+
crlfDelay: Infinity
|
|
505
|
+
});
|
|
506
|
+
const pendingMessages = new Set();
|
|
507
|
+
rl.on("line", (line) => {
|
|
508
|
+
const message = processLine(line, (data) => transport.writable.write(data), session.handleMessage);
|
|
509
|
+
pendingMessages.add(message);
|
|
510
|
+
void message.finally(() => {
|
|
511
|
+
pendingMessages.delete(message);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
rl.on("close", async () => {
|
|
515
|
+
await Promise.all([...pendingMessages]);
|
|
516
|
+
session.close();
|
|
517
|
+
resolve();
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
async connectSDK(transport) {
|
|
522
|
+
return new Promise((resolve, reject) => {
|
|
523
|
+
const listener = (notification) => transport.send(notification);
|
|
524
|
+
const session = server.createMessageSession(listener);
|
|
525
|
+
transport.onmessage = async (message) => {
|
|
526
|
+
// Ignore responses (we only handle requests/notifications)
|
|
527
|
+
if (!("method" in message)) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Handle notifications (no id) - don't respond
|
|
531
|
+
if (!("id" in message) || message.id === undefined) {
|
|
532
|
+
if (message.method === "initialize") {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
await session.handleMessage(message.method, message.params);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (message.method === "notifications/initialized") {
|
|
544
|
+
await transport.send({
|
|
545
|
+
jsonrpc: "2.0",
|
|
546
|
+
id: message.id,
|
|
547
|
+
error: {
|
|
548
|
+
code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
|
|
549
|
+
message: "Invalid Request"
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const request = message;
|
|
555
|
+
let handled;
|
|
556
|
+
try {
|
|
557
|
+
handled = await session.handleMessage(request.method, request.params);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
await transport.send({
|
|
561
|
+
jsonrpc: "2.0",
|
|
562
|
+
id: request.id,
|
|
563
|
+
error: {
|
|
564
|
+
code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
565
|
+
message: "Internal error"
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const { result, error } = handled;
|
|
571
|
+
if (error) {
|
|
572
|
+
const response = {
|
|
573
|
+
jsonrpc: "2.0",
|
|
574
|
+
id: request.id,
|
|
575
|
+
error
|
|
576
|
+
};
|
|
577
|
+
await transport.send(response);
|
|
578
|
+
}
|
|
579
|
+
else if (result !== undefined) {
|
|
580
|
+
const response = {
|
|
581
|
+
jsonrpc: "2.0",
|
|
582
|
+
id: request.id,
|
|
583
|
+
result
|
|
584
|
+
};
|
|
585
|
+
await transport.send(response);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
transport.onclose = () => {
|
|
589
|
+
session.close();
|
|
590
|
+
resolve();
|
|
591
|
+
};
|
|
592
|
+
void transport.start().catch((error) => {
|
|
593
|
+
session.close();
|
|
594
|
+
reject(error);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
return server;
|
|
600
|
+
}
|
|
601
|
+
function invalidParams(message) {
|
|
602
|
+
return {
|
|
603
|
+
error: {
|
|
604
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
605
|
+
message
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function internalError(message) {
|
|
610
|
+
return {
|
|
611
|
+
error: {
|
|
612
|
+
code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
613
|
+
message
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function resourceNotFound(uri) {
|
|
618
|
+
return {
|
|
619
|
+
error: {
|
|
620
|
+
code: JSON_RPC_ERROR_CODES.RESOURCE_NOT_FOUND,
|
|
621
|
+
message: `Resource not found: ${uri}`
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function toErrorMessage(error) {
|
|
626
|
+
return error instanceof Error ? error.message : String(error);
|
|
627
|
+
}
|
|
628
|
+
function isValidUri(uri) {
|
|
629
|
+
try {
|
|
630
|
+
new URL(uri);
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function assertNonEmptyName(name, message) {
|
|
638
|
+
if (name.length === 0) {
|
|
639
|
+
throw new Error(message);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function assertReadableUriTemplate(uriTemplate) {
|
|
643
|
+
const parsed = uriTemplateParser.parse(uriTemplate);
|
|
644
|
+
const expanded = parsed.expand(new Proxy({}, {
|
|
645
|
+
get: (_target, property) => (typeof property === "string" ? "value" : undefined)
|
|
646
|
+
}));
|
|
647
|
+
if (typeof expanded !== "string" || !isValidUri(expanded)) {
|
|
648
|
+
throw new Error(`Invalid resource URI template: ${uriTemplate}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function toStringArguments(value) {
|
|
652
|
+
if (value === undefined) {
|
|
653
|
+
return {};
|
|
654
|
+
}
|
|
655
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
const args = {};
|
|
659
|
+
for (const [name, argument] of Object.entries(value)) {
|
|
660
|
+
if (typeof argument !== "string") {
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
args[name] = argument;
|
|
664
|
+
}
|
|
665
|
+
return args;
|
|
666
|
+
}
|
|
667
|
+
function hasRequiredPromptArguments(prompt, args) {
|
|
668
|
+
return (prompt.arguments ?? []).every((argument) => argument.required !== true || args[argument.name] !== undefined);
|
|
669
|
+
}
|
|
670
|
+
function findReadableResource(uri, resources, resourceTemplates) {
|
|
671
|
+
const resource = resources.get(uri);
|
|
672
|
+
if (resource !== undefined) {
|
|
673
|
+
return resource;
|
|
674
|
+
}
|
|
675
|
+
return [...resourceTemplates.values()].find((template) => matchesUriTemplate(template.uriTemplate, uri));
|
|
676
|
+
}
|
|
677
|
+
function matchesUriTemplate(template, uri) {
|
|
678
|
+
try {
|
|
679
|
+
return new UriTemplate(template).match(uri) !== null;
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function isCallToolResult(value) {
|
|
686
|
+
if (!hasContentArray(value) || !value.content.every(isContentItem)) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
if (hasOwnProperty(value, "structuredContent") &&
|
|
690
|
+
value.structuredContent !== undefined &&
|
|
691
|
+
!isJsonObject(value.structuredContent)) {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
return !(hasOwnProperty(value, "isError") &&
|
|
695
|
+
value.isError !== undefined &&
|
|
696
|
+
typeof value.isError !== "boolean");
|
|
697
|
+
}
|
|
698
|
+
function normalizeToolResult(handlerResult, outputSchema) {
|
|
699
|
+
if (hasContentArray(handlerResult) && !isCallToolResult(handlerResult)) {
|
|
700
|
+
throw new Error("Invalid tool result");
|
|
701
|
+
}
|
|
702
|
+
if (outputSchema === undefined) {
|
|
703
|
+
const result = isCallToolResult(handlerResult)
|
|
704
|
+
? handlerResult
|
|
705
|
+
: { content: toContentBlocks(handlerResult) };
|
|
706
|
+
if (!isCallToolResult(result)) {
|
|
707
|
+
throw new Error("Invalid tool result");
|
|
708
|
+
}
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
if (isCallToolResult(handlerResult) && handlerResult.isError === true) {
|
|
712
|
+
return handlerResult;
|
|
713
|
+
}
|
|
714
|
+
const callToolResult = isCallToolResult(handlerResult) ? handlerResult : undefined;
|
|
715
|
+
const structuredContent = callToolResult ? callToolResult.structuredContent : handlerResult;
|
|
716
|
+
if (!isJsonObject(structuredContent)) {
|
|
717
|
+
throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, "Structured tool result must be an object");
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
content: callToolResult !== undefined && callToolResult.content.length > 0
|
|
721
|
+
? callToolResult.content
|
|
722
|
+
: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
723
|
+
...(callToolResult?.isError !== undefined ? { isError: callToolResult.isError } : {}),
|
|
724
|
+
structuredContent
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function assertObjectRootSchema(schema, path) {
|
|
728
|
+
if (schema.type !== "object") {
|
|
729
|
+
throw new Error(`${path} root type must be "object"`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function isJsonObject(value) {
|
|
733
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
734
|
+
}
|
|
735
|
+
function isGetPromptResult(value) {
|
|
736
|
+
if (typeof value !== "object" || value === null || !hasOwnProperty(value, "messages")) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
return ((!hasOwnProperty(value, "description") ||
|
|
740
|
+
value.description === undefined ||
|
|
741
|
+
typeof value.description === "string") &&
|
|
742
|
+
Array.isArray(value.messages) &&
|
|
743
|
+
value.messages.every((message) => typeof message === "object" &&
|
|
744
|
+
message !== null &&
|
|
745
|
+
hasOwnProperty(message, "role") &&
|
|
746
|
+
(message.role === "user" || message.role === "assistant") &&
|
|
747
|
+
hasOwnProperty(message, "content") &&
|
|
748
|
+
isPromptContentItem(message.content)));
|
|
749
|
+
}
|
|
750
|
+
function isReadResourceResult(value) {
|
|
751
|
+
if (typeof value !== "object" || value === null || !hasOwnProperty(value, "contents")) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return Array.isArray(value.contents) && value.contents.every(isResourceContents);
|
|
755
|
+
}
|
|
756
|
+
function hasContentArray(value) {
|
|
757
|
+
return (typeof value === "object" &&
|
|
758
|
+
value !== null &&
|
|
759
|
+
hasOwnProperty(value, "content") &&
|
|
760
|
+
Array.isArray(value.content));
|
|
761
|
+
}
|
|
762
|
+
function isContentItem(value) {
|
|
763
|
+
if (typeof value !== "object" || value === null || !hasOwnProperty(value, "type")) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
const block = value;
|
|
767
|
+
if (!hasValidContentAnnotations(block)) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
if (block.type === "text") {
|
|
771
|
+
return hasOwnProperty(block, "text") && typeof block.text === "string";
|
|
772
|
+
}
|
|
773
|
+
if (block.type === "image" || block.type === "audio") {
|
|
774
|
+
return (hasOwnProperty(block, "data") &&
|
|
775
|
+
typeof block.data === "string" &&
|
|
776
|
+
isBase64(block.data) &&
|
|
777
|
+
hasOwnProperty(block, "mimeType") &&
|
|
778
|
+
typeof block.mimeType === "string");
|
|
779
|
+
}
|
|
780
|
+
if (block.type === "resource_link") {
|
|
781
|
+
return (hasOwnProperty(block, "uri") &&
|
|
782
|
+
typeof block.uri === "string" &&
|
|
783
|
+
isValidUri(block.uri) &&
|
|
784
|
+
hasOwnProperty(block, "name") &&
|
|
785
|
+
typeof block.name === "string" &&
|
|
786
|
+
(!hasOwnProperty(block, "title") ||
|
|
787
|
+
block.title === undefined ||
|
|
788
|
+
typeof block.title === "string") &&
|
|
789
|
+
(!hasOwnProperty(block, "description") ||
|
|
790
|
+
block.description === undefined ||
|
|
791
|
+
typeof block.description === "string") &&
|
|
792
|
+
(!hasOwnProperty(block, "mimeType") ||
|
|
793
|
+
block.mimeType === undefined ||
|
|
794
|
+
typeof block.mimeType === "string") &&
|
|
795
|
+
(!hasOwnProperty(block, "size") || block.size === undefined || typeof block.size === "number"));
|
|
796
|
+
}
|
|
797
|
+
if (block.type !== "resource" ||
|
|
798
|
+
!hasOwnProperty(block, "resource") ||
|
|
799
|
+
typeof block.resource !== "object" ||
|
|
800
|
+
block.resource === null) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
return isResourceContents(block.resource);
|
|
804
|
+
}
|
|
805
|
+
function isResourceContents(value) {
|
|
806
|
+
if (typeof value !== "object" ||
|
|
807
|
+
value === null ||
|
|
808
|
+
!hasOwnProperty(value, "uri") ||
|
|
809
|
+
typeof value.uri !== "string" ||
|
|
810
|
+
!isValidUri(value.uri)) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (hasOwnProperty(value, "mimeType") &&
|
|
814
|
+
value.mimeType !== undefined &&
|
|
815
|
+
typeof value.mimeType !== "string") {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
return ((hasOwnProperty(value, "text") && typeof value.text === "string") ||
|
|
819
|
+
(hasOwnProperty(value, "blob") && typeof value.blob === "string" && isBase64(value.blob)));
|
|
820
|
+
}
|
|
821
|
+
function hasValidContentAnnotations(value) {
|
|
822
|
+
if (!hasOwnProperty(value, "annotations") || value.annotations === undefined) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
if (!isJsonObject(value.annotations)) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
const { audience, priority, lastModified } = value.annotations;
|
|
829
|
+
return ((audience === undefined ||
|
|
830
|
+
(Array.isArray(audience) &&
|
|
831
|
+
audience.every((item) => item === "user" || item === "assistant"))) &&
|
|
832
|
+
(priority === undefined || typeof priority === "number") &&
|
|
833
|
+
(lastModified === undefined || typeof lastModified === "string"));
|
|
834
|
+
}
|
|
835
|
+
function isBase64(value) {
|
|
836
|
+
if (value.length === 0) {
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
if (value.length % 4 !== 0) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
843
|
+
const paddingStart = value.indexOf("=");
|
|
844
|
+
const encoded = paddingStart === -1 ? value : value.slice(0, paddingStart);
|
|
845
|
+
const padding = paddingStart === -1 ? "" : value.slice(paddingStart);
|
|
846
|
+
if (padding.length > 2 || [...padding].some((character) => character !== "=")) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
if ([...encoded].some((character) => !alphabet.includes(character))) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
return Buffer.from(value, "base64").toString("base64") === value;
|
|
853
|
+
}
|
|
854
|
+
function isPromptContentItem(value) {
|
|
855
|
+
if (!isContentItem(value)) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
return !(typeof value === "object" &&
|
|
859
|
+
value !== null &&
|
|
860
|
+
hasOwnProperty(value, "type") &&
|
|
861
|
+
value.type === "resource_link");
|
|
862
|
+
}
|
|
863
|
+
function hasOwnProperty(value, name) {
|
|
864
|
+
return Object.prototype.hasOwnProperty.call(value, name);
|
|
865
|
+
}
|