toolcraft 0.0.409 → 0.0.411

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.
@@ -1,1147 +1,7 @@
1
- import * as readline from "readline";
2
- import { compileJsonSchema, formatIssues, normalizeLegacyNullability } from "toolcraft-schema";
3
- import { JSON_RPC_ERROR_CODES, ToolError } from "./types.js";
4
- import { parseMessage, formatSuccessResponse, formatErrorResponse, isRequestId } from "./jsonrpc.js";
5
- import { parseUriTemplate } from "./uri-template.js";
6
- import { toContentBlocks } from "./content/convert.js";
7
- import { ToolCallAdmission } from "./tool-call-admission.js";
8
- import { StdioOutput } from "./stdio-output.js";
1
+ import * as readline from "node:readline";
2
+ import { createProtocolServer } from "./protocol-server.js";
9
3
  import { StdioInput } from "./stdio-input.js";
10
- import { isBase64 } from "./base64.js";
11
- import { selectRequestProtocol, decorateModernResult, MODERN_PROTOCOL_VERSION } from "./protocol.js";
12
- import { SubscriptionRegistry } from "./subscriptions.js";
13
- import { waitForRequest } from "./request-cancellation.js";
14
- import { isJsonValue } from "toolcraft-schema";
15
- import { isValidUri } from "./uri.js";
16
- import { getParameterHeaders, validateParameterHeaders } from "./headers.js";
17
- const PROTOCOL_VERSION = "2025-11-25";
18
- const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-03-26", "2025-06-18", PROTOCOL_VERSION, MODERN_PROTOCOL_VERSION]);
19
- function createLifecycleState(listener, supportNotifications, supportResourceSubscriptions) {
20
- const abortController = new AbortController();
21
- const admissions = new Set();
22
- const requests = new Map();
23
- abortController.signal.addEventListener("abort", () => {
24
- for (const admission of admissions)
25
- admission.abort(abortController.signal.reason);
26
- admissions.clear();
27
- for (const request of requests.values())
28
- request.abort(abortController.signal.reason);
29
- requests.clear();
30
- }, { once: true });
31
- return {
32
- initialized: false,
33
- protocolVersion: PROTOCOL_VERSION,
34
- initializeAccepted: false,
35
- notificationReady: false,
36
- resourceSubscriptions: new Set(),
37
- abortController,
38
- admissions,
39
- requests,
40
- subscriptions: new SubscriptionRegistry(listener ?? (() => { }), supportNotifications, supportResourceSubscriptions),
41
- listener
42
- };
43
- }
44
- function compileToolSchema(schema) {
45
- try {
46
- return compileJsonSchema(schema);
47
- }
48
- catch (error) {
49
- const message = error instanceof Error ? error.message : String(error);
50
- throw new Error(`schema is invalid: ${message}`, { cause: error });
51
- }
52
- }
4
+ import { StdioOutput } from "./stdio-output.js";
53
5
  export function createServer(options) {
54
- if (options.toolCallTimeoutMs !== undefined &&
55
- (!Number.isInteger(options.toolCallTimeoutMs) || options.toolCallTimeoutMs <= 0)) {
56
- throw new Error("toolCallTimeoutMs must be a positive integer.");
57
- }
58
- const maxConcurrentToolCalls = options.maxConcurrentToolCalls ?? 4;
59
- const maxQueuedToolCalls = options.maxQueuedToolCalls ?? 64;
60
- const maxStdioOutputBytes = options.maxStdioOutputBytes ?? 1024 * 1024;
61
- const maxPendingStdioMessages = options.maxPendingStdioMessages ?? 128;
62
- const maxActiveRequests = options.maxActiveRequests ?? 128;
63
- const maxStdioLineBytes = options.maxStdioLineBytes ?? 1024 * 1024;
64
- for (const [name, value, minimum] of [
65
- ["maxConcurrentToolCalls", maxConcurrentToolCalls, 1],
66
- ["maxQueuedToolCalls", maxQueuedToolCalls, 0],
67
- ["maxStdioOutputBytes", maxStdioOutputBytes, 1],
68
- ["maxPendingStdioMessages", maxPendingStdioMessages, 1],
69
- ["maxActiveRequests", maxActiveRequests, 1],
70
- ["maxStdioLineBytes", maxStdioLineBytes, 1]
71
- ]) {
72
- if (!Number.isSafeInteger(value) || value < minimum) {
73
- throw new Error(`${name} must be a safe integer greater than or equal to ${minimum}.`);
74
- }
75
- }
76
- const toolAdmission = new ToolCallAdmission(maxConcurrentToolCalls, maxQueuedToolCalls);
77
- const supportNotifications = options.supportNotifications !== false;
78
- const supportResourceSubscriptions = options.supportResourceSubscriptions !== false;
79
- const tools = new Map();
80
- const prompts = new Map();
81
- const resources = new Map();
82
- const resourceTemplates = new Map();
83
- const methods = new Map();
84
- const notificationListeners = new Set();
85
- const connectionNotificationListeners = new Map();
86
- const defaultLifecycle = createLifecycleState((notification) => {
87
- for (const listener of notificationListeners)
88
- listener(notification);
89
- }, supportNotifications, supportResourceSubscriptions);
90
- const messageLifecycles = new Set([defaultLifecycle]);
91
- let activeRequests = 0;
92
- const describeCapabilities = () => ({
93
- tools: {
94
- ...(supportNotifications ? { listChanged: true } : {})
95
- },
96
- prompts: {
97
- ...(supportNotifications ? { listChanged: true } : {})
98
- },
99
- resources: {
100
- ...(supportNotifications ? { listChanged: true } : {}),
101
- ...(supportResourceSubscriptions ? { subscribe: true } : {})
102
- }
103
- });
104
- const dispatchMessage = async (method, lifecycle, params, modern = false, requestSignal = lifecycle.abortController.signal, parameterHeaders) => {
105
- const handlerContext = {
106
- signal: requestSignal,
107
- ...(modern && params?.requestState !== undefined
108
- ? { requestState: params.requestState }
109
- : {}),
110
- ...(modern && params?.inputResponses !== undefined
111
- ? { inputResponses: params.inputResponses }
112
- : {}),
113
- clientCapabilities: modern
114
- ? (params?._meta)["io.modelcontextprotocol/clientCapabilities"]
115
- : {}
116
- };
117
- if (method === "server/discover") {
118
- const result = {
119
- resultType: "complete",
120
- supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS].reverse(),
121
- capabilities: describeCapabilities(),
122
- _meta: {
123
- "io.modelcontextprotocol/serverInfo": { name: options.name, version: options.version }
124
- },
125
- ttlMs: 0,
126
- cacheScope: "private"
127
- };
128
- return { result };
129
- }
130
- // Allow ping and initialize before initialization
131
- if (method === "ping") {
132
- return { result: {} };
133
- }
134
- if (method === "initialize") {
135
- // Re-initialize on the same connection is idempotent: real MCP clients
136
- // (e.g. kimi-cli via fastmcp) re-send `initialize` on a persistent
137
- // connection per tool call, and the official MCP SDK server re-responds
138
- // with InitializeResult instead of erroring. Per-connection isolation is
139
- // still enforced by the separate lifecycle object given to each connection.
140
- lifecycle.initializeAccepted = true;
141
- lifecycle.initialized = true;
142
- lifecycle.notificationReady = false;
143
- const requestedProtocol = typeof params?.protocolVersion === "string" ? params.protocolVersion : undefined;
144
- const result = {
145
- protocolVersion: requestedProtocol !== undefined && requestedProtocol !== MODERN_PROTOCOL_VERSION &&
146
- SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol)
147
- ? requestedProtocol
148
- : PROTOCOL_VERSION,
149
- capabilities: describeCapabilities(),
150
- serverInfo: {
151
- name: options.name,
152
- version: options.version
153
- }
154
- };
155
- lifecycle.protocolVersion = result.protocolVersion;
156
- return { result };
157
- }
158
- if (method === "notifications/initialized") {
159
- if (!lifecycle.initializeAccepted) {
160
- return {
161
- error: {
162
- code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
163
- message: "Server not initialized"
164
- }
165
- };
166
- }
167
- lifecycle.notificationReady = true;
168
- return { result: undefined };
169
- }
170
- // All other methods require initialization
171
- if (!modern && !lifecycle.initialized) {
172
- return {
173
- error: {
174
- code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
175
- message: "Server not initialized"
176
- }
177
- };
178
- }
179
- if (method === "tools/list") {
180
- const toolList = [];
181
- for (const tool of tools.values()) {
182
- const descriptor = { ...tool };
183
- delete descriptor.handler;
184
- delete descriptor.inputValidator;
185
- delete descriptor.outputValidator;
186
- delete descriptor.parameterHeaders;
187
- if (!modern && descriptor.outputSchema?.type !== "object")
188
- delete descriptor.outputSchema;
189
- toolList.push(structuredClone(descriptor));
190
- }
191
- return { result: { tools: toolList } };
192
- }
193
- if (method === "tools/call") {
194
- const toolName = params?.name;
195
- if (!toolName) {
196
- return {
197
- error: {
198
- code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
199
- message: "Tool name required"
200
- }
201
- };
202
- }
203
- const tool = tools.get(toolName);
204
- if (!tool) {
205
- const availableTools = [...tools.keys()].slice(0, 20);
206
- return {
207
- error: {
208
- code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
209
- message: `Tool not found: ${toolName}${availableTools.length === 0 ? "" : `. Available: ${availableTools.join(", ")}`}`
210
- }
211
- };
212
- }
213
- if (params?.arguments !== undefined && !isJsonObject(params.arguments))
214
- return invalidParams("Tool arguments must be an object");
215
- const toolArgs = (params?.arguments ?? {});
216
- if (modern && parameterHeaders !== undefined) {
217
- const headerError = validateParameterHeaders(tool.parameterHeaders, toolArgs, parameterHeaders);
218
- if (headerError !== undefined)
219
- return { error: { code: -32020, message: headerError } };
220
- }
221
- const inputValidation = tool.inputValidator.validate(toolArgs);
222
- if (options.validateToolArguments !== false && !inputValidation.ok) {
223
- return {
224
- error: {
225
- code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
226
- message: `Invalid tool arguments: ${formatIssues(inputValidation.issues)}`,
227
- data: inputValidation.issues
228
- }
229
- };
230
- }
231
- try {
232
- let handlerResult;
233
- const admissionController = new AbortController();
234
- const sessionSignal = requestSignal;
235
- const admissionSignal = admissionController.signal;
236
- const cancelAdmission = () => admissionController.abort(sessionSignal.reason);
237
- if (sessionSignal.aborted)
238
- admissionController.abort(sessionSignal.reason);
239
- else {
240
- lifecycle.admissions.add(admissionController);
241
- if (sessionSignal !== lifecycle.abortController.signal)
242
- sessionSignal.addEventListener("abort", cancelAdmission, { once: true });
243
- }
244
- const handlerPromise = (async () => {
245
- const release = await toolAdmission.acquire(admissionSignal);
246
- try {
247
- admissionSignal.throwIfAborted();
248
- return await tool.handler(toolArgs, { ...handlerContext, signal: admissionSignal });
249
- }
250
- finally {
251
- release();
252
- }
253
- })();
254
- try {
255
- if (options.toolCallTimeoutMs === undefined) {
256
- handlerResult = await handlerPromise;
257
- }
258
- else {
259
- let timeout;
260
- handlerResult = await Promise.race([
261
- handlerPromise,
262
- new Promise((_resolve, reject) => {
263
- timeout = setTimeout(() => {
264
- const error = new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Tool call timed out: ${toolName}`);
265
- admissionController.abort(error);
266
- reject(error);
267
- }, options.toolCallTimeoutMs);
268
- })
269
- ]).finally(() => {
270
- if (timeout !== undefined) {
271
- clearTimeout(timeout);
272
- }
273
- });
274
- }
275
- }
276
- finally {
277
- lifecycle.admissions.delete(admissionController);
278
- sessionSignal.removeEventListener("abort", cancelAdmission);
279
- }
280
- if (modern && isInputRequiredResult(handlerResult))
281
- return { result: handlerResult };
282
- const outputSchema = modern || tool.outputSchema?.type === "object" ? tool.outputSchema : undefined;
283
- const result = normalizeToolResult(handlerResult, tool.outputSchema, modern);
284
- const outputValidation = outputSchema === undefined ? undefined : tool.outputValidator?.validate(result.structuredContent);
285
- if (result.isError !== true && outputValidation !== undefined && !outputValidation.ok) {
286
- throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Invalid structured tool result: ${formatIssues(outputValidation.issues)}`, outputValidation.issues);
287
- }
288
- return { result };
289
- }
290
- catch (err) {
291
- if (err instanceof ToolError) {
292
- return {
293
- error: {
294
- code: err.code,
295
- message: err.message,
296
- ...(err.data === undefined ? {} : { data: err.data })
297
- }
298
- };
299
- }
300
- const errorMessage = err instanceof Error ? err.message : String(err);
301
- const result = {
302
- content: [{ type: "text", text: `Error: ${errorMessage}` }],
303
- isError: true
304
- };
305
- return { result };
306
- }
307
- }
308
- if (method === "prompts/list") {
309
- return {
310
- result: {
311
- prompts: [...prompts.values()].map(({ handler: _handler, ...prompt }) => structuredClone(prompt))
312
- }
313
- };
314
- }
315
- if (method === "prompts/get") {
316
- const promptName = typeof params?.name === "string" ? params.name : undefined;
317
- if (promptName === undefined) {
318
- return invalidParams("Prompt name required");
319
- }
320
- const prompt = prompts.get(promptName);
321
- if (prompt === undefined) {
322
- return invalidParams(`Prompt not found: ${promptName}`);
323
- }
324
- const args = toStringArguments(params?.arguments);
325
- if (args === undefined || !hasRequiredPromptArguments(prompt, args)) {
326
- return invalidParams("Invalid prompt arguments");
327
- }
328
- try {
329
- const result = await prompt.handler(args, handlerContext);
330
- if (modern && isInputRequiredResult(result))
331
- return { result };
332
- if (!isGetPromptResult(result, modern || lifecycle.protocolVersion === PROTOCOL_VERSION)) {
333
- return internalError("Invalid prompt result");
334
- }
335
- return { result };
336
- }
337
- catch (error) {
338
- return internalError(toErrorMessage(error));
339
- }
340
- }
341
- if (method === "resources/list") {
342
- return {
343
- result: {
344
- resources: [...resources.values()].map(({ handler: _handler, ...resource }) => structuredClone(resource))
345
- }
346
- };
347
- }
348
- if (method === "resources/templates/list") {
349
- return {
350
- result: {
351
- resourceTemplates: [...resourceTemplates.values()].map(({ handler: _handler, template: _template, ...resourceTemplate }) => structuredClone(resourceTemplate))
352
- }
353
- };
354
- }
355
- if (method === "resources/read") {
356
- const uri = typeof params?.uri === "string" ? params.uri : undefined;
357
- if (uri === undefined || !isValidUri(uri)) {
358
- return invalidParams("Resource URI required");
359
- }
360
- const resource = findReadableResource(uri, resources, resourceTemplates);
361
- if (resource === undefined) {
362
- return resourceNotFound(uri);
363
- }
364
- try {
365
- const result = await resource.handler(uri, handlerContext);
366
- if (modern && isInputRequiredResult(result))
367
- return { result };
368
- if (!isReadResourceResult(result)) {
369
- return internalError("Invalid resource result");
370
- }
371
- return { result };
372
- }
373
- catch (error) {
374
- return internalError(toErrorMessage(error));
375
- }
376
- }
377
- if (method === "resources/subscribe" || method === "resources/unsubscribe") {
378
- if (!supportResourceSubscriptions) {
379
- return {
380
- error: {
381
- code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
382
- message: "Method not found"
383
- }
384
- };
385
- }
386
- const uri = typeof params?.uri === "string" ? params.uri : undefined;
387
- if (uri === undefined || !isValidUri(uri)) {
388
- return invalidParams("Resource URI required");
389
- }
390
- if (method === "resources/subscribe" &&
391
- findReadableResource(uri, resources, resourceTemplates) === undefined) {
392
- return resourceNotFound(uri);
393
- }
394
- if (method === "resources/subscribe") {
395
- lifecycle.resourceSubscriptions.add(uri);
396
- }
397
- else {
398
- lifecycle.resourceSubscriptions.delete(uri);
399
- }
400
- return { result: {} };
401
- }
402
- const customMethod = methods.get(method);
403
- if (customMethod !== undefined) {
404
- try {
405
- const result = await customMethod(params, {
406
- signal: requestSignal,
407
- async notify(notificationMethod, notificationParams) {
408
- if (requestSignal.aborted ||
409
- (!modern && !lifecycle.notificationReady) ||
410
- lifecycle.listener === undefined) {
411
- return;
412
- }
413
- await lifecycle.listener({
414
- jsonrpc: "2.0",
415
- method: notificationMethod,
416
- ...(notificationParams === undefined ? {} : { params: notificationParams })
417
- });
418
- }
419
- });
420
- return { result };
421
- }
422
- catch (error) {
423
- if (error instanceof ToolError) {
424
- return {
425
- error: {
426
- code: error.code,
427
- message: error.message,
428
- ...(error.data === undefined ? {} : { data: error.data })
429
- }
430
- };
431
- }
432
- return internalError(toErrorMessage(error));
433
- }
434
- }
435
- return {
436
- error: {
437
- code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
438
- message: "Method not found"
439
- }
440
- };
441
- };
442
- const handleMessageWithLifecycle = async (method, lifecycle, params, context) => {
443
- if (method === "notifications/cancelled") {
444
- const id = params?.requestId;
445
- if (typeof id === "string" || typeof id === "number") {
446
- lifecycle.requests.get(id)?.abort(new Error("Request cancelled"));
447
- }
448
- return { result: undefined };
449
- }
450
- const protocol = selectRequestProtocol(method, params, SUPPORTED_PROTOCOL_VERSIONS);
451
- if (protocol.error !== undefined)
452
- return { error: protocol.error };
453
- const directLegacy = !protocol.modern && context === undefined;
454
- if (protocol.modern &&
455
- context !== undefined &&
456
- context.requestId !== undefined &&
457
- !isRequestId(context.requestId)) {
458
- return {
459
- error: { code: JSON_RPC_ERROR_CODES.INVALID_REQUEST, message: "Invalid Request ID" }
460
- };
461
- }
462
- if (activeRequests >= maxActiveRequests) {
463
- return { error: { code: -32000, message: "Too many active requests" } };
464
- }
465
- const key = context?.requestId ?? Symbol("request");
466
- if (lifecycle.requests.has(key)) {
467
- return {
468
- error: {
469
- code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
470
- message: "Request ID is already active"
471
- }
472
- };
473
- }
474
- const controller = new AbortController();
475
- const parents = context?.signal === undefined ? [] : [context.signal];
476
- const signal = controller.signal;
477
- if (lifecycle.abortController.signal.aborted)
478
- controller.abort(lifecycle.abortController.signal.reason);
479
- const abort = (event) => controller.abort(event.target.reason);
480
- for (const parent of parents) {
481
- if (parent.aborted)
482
- controller.abort(parent.reason);
483
- else
484
- parent.addEventListener("abort", abort, { once: true });
485
- }
486
- const cleanup = () => {
487
- for (const parent of parents)
488
- parent.removeEventListener("abort", abort);
489
- };
490
- if (signal.aborted) {
491
- cleanup();
492
- return { result: undefined };
493
- }
494
- lifecycle.requests.set(key, controller);
495
- activeRequests += 1;
496
- const operation = protocol.modern && method === "subscriptions/listen"
497
- ? lifecycle.subscriptions.listen(context?.requestId, params?.notifications, signal)
498
- : dispatchMessage(method, lifecycle, params, protocol.modern, directLegacy ? lifecycle.abortController.signal : signal, context?.parameterHeaders);
499
- const release = () => {
500
- activeRequests -= 1;
501
- if (lifecycle.requests.get(key) === controller)
502
- lifecycle.requests.delete(key);
503
- };
504
- void operation.then(release, release);
505
- try {
506
- const handled = directLegacy ? await operation : await waitForRequest(operation, signal);
507
- if (handled === undefined || (!directLegacy && signal.aborted))
508
- return { result: undefined };
509
- return protocol.modern
510
- ? decorateModernResult(method, handled, { name: options.name, version: options.version }, (params?._meta)["io.modelcontextprotocol/clientCapabilities"])
511
- : handled;
512
- }
513
- finally {
514
- cleanup();
515
- controller.abort();
516
- }
517
- };
518
- const createMessageSession = (listener) => {
519
- const lifecycle = createLifecycleState(listener, supportNotifications, supportResourceSubscriptions);
520
- messageLifecycles.add(lifecycle);
521
- if (listener !== undefined) {
522
- connectionNotificationListeners.set(listener, lifecycle);
523
- }
524
- return {
525
- handleMessage: (method, params, context) => handleMessageWithLifecycle(method, lifecycle, params, context),
526
- close: () => {
527
- lifecycle.abortController.abort();
528
- if (listener !== undefined) {
529
- connectionNotificationListeners.delete(listener);
530
- }
531
- messageLifecycles.delete(lifecycle);
532
- }
533
- };
534
- };
535
- const handleMessage = (method, params, context) => handleMessageWithLifecycle(method, defaultLifecycle, params, context);
536
- const processLine = async (line, write, messageHandler, subscriptionSignal) => {
537
- const parsed = parseMessage(line);
538
- if (!parsed.success) {
539
- await write(formatErrorResponse(parsed.id, parsed.error) + "\n");
540
- return;
541
- }
542
- const { request, isNotification } = parsed;
543
- if (isNotification &&
544
- !request.method.startsWith("notifications/") &&
545
- selectRequestProtocol(request.method, request.params, SUPPORTED_PROTOCOL_VERSIONS).modern)
546
- return;
547
- if (isNotification && request.method === "initialize") {
548
- return;
549
- }
550
- if (!isNotification && request.method === "notifications/initialized") {
551
- const requestWithId = request;
552
- await write(formatErrorResponse(requestWithId.id, {
553
- code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
554
- message: "Invalid Request"
555
- }) + "\n");
556
- return;
557
- }
558
- let handled;
559
- try {
560
- handled = await messageHandler(request.method, request.params, !isNotification && request.id !== null
561
- ? {
562
- requestId: request.id,
563
- ...(request.method === "subscriptions/listen" ? { signal: subscriptionSignal } : {})
564
- }
565
- : undefined);
566
- }
567
- catch {
568
- if (!isNotification) {
569
- const requestWithId = request;
570
- await write(formatErrorResponse(requestWithId.id, {
571
- code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
572
- message: "Internal error"
573
- }) + "\n");
574
- }
575
- return;
576
- }
577
- const { result, error } = handled;
578
- if (isNotification) {
579
- return;
580
- }
581
- const requestWithId = request;
582
- if (error) {
583
- await write(formatErrorResponse(requestWithId.id, error) + "\n");
584
- }
585
- else if (result !== undefined) {
586
- await write(formatSuccessResponse(requestWithId.id, result) + "\n");
587
- }
588
- };
589
- const broadcastNotification = async (method, params, canSend = () => true) => {
590
- const notification = {
591
- jsonrpc: "2.0",
592
- method,
593
- ...(params === undefined ? {} : { params })
594
- };
595
- for (const listener of notificationListeners) {
596
- listener(notification);
597
- }
598
- await Promise.all([...messageLifecycles].map((lifecycle) => lifecycle.subscriptions.emit(method, params)));
599
- await Promise.all([...connectionNotificationListeners].map(async ([listener, lifecycle]) => {
600
- if (lifecycle.notificationReady && canSend(lifecycle)) {
601
- await listener(notification);
602
- }
603
- }));
604
- };
605
- const server = {
606
- tool(name, description, inputSchema, handler, outputSchema) {
607
- assertNonEmptyName(name, "Tool name required");
608
- if (tools.has(name)) {
609
- throw new Error(`Tool already registered: ${name}`);
610
- }
611
- const inputSchemaSnapshot = normalizeLegacyNullability(inputSchema);
612
- const outputSchemaSnapshot = outputSchema === undefined ? undefined : normalizeLegacyNullability(outputSchema);
613
- const inputValidator = compileToolSchema(inputSchemaSnapshot);
614
- assertObjectRootSchema(inputSchemaSnapshot, "inputSchema");
615
- let outputValidator;
616
- if (outputSchemaSnapshot !== undefined) {
617
- assertOutputSchema(outputSchemaSnapshot);
618
- outputValidator = compileToolSchema(outputSchemaSnapshot);
619
- }
620
- tools.set(name, {
621
- name,
622
- description,
623
- inputSchema: inputSchemaSnapshot,
624
- ...(outputSchemaSnapshot === undefined ? {} : { outputSchema: outputSchemaSnapshot }),
625
- handler: handler,
626
- inputValidator,
627
- parameterHeaders: getParameterHeaders(inputSchemaSnapshot),
628
- ...(outputValidator === undefined ? {} : { outputValidator })
629
- });
630
- return server;
631
- },
632
- registerTool(definition, handler) {
633
- assertNonEmptyName(definition.name, "Tool name required");
634
- if (tools.has(definition.name)) {
635
- throw new Error(`Tool already registered: ${definition.name}`);
636
- }
637
- const descriptor = structuredClone(definition);
638
- const inputSchema = normalizeLegacyNullability(descriptor.inputSchema);
639
- const inputValidator = compileToolSchema(inputSchema);
640
- assertObjectRootSchema(inputSchema, "inputSchema");
641
- let outputValidator;
642
- if (descriptor.outputSchema !== undefined) {
643
- descriptor.outputSchema = normalizeLegacyNullability(descriptor.outputSchema);
644
- assertOutputSchema(descriptor.outputSchema);
645
- outputValidator = compileToolSchema(descriptor.outputSchema);
646
- }
647
- tools.set(definition.name, {
648
- ...descriptor,
649
- inputSchema,
650
- handler: handler,
651
- inputValidator,
652
- parameterHeaders: getParameterHeaders(inputSchema),
653
- ...(outputValidator === undefined ? {} : { outputValidator })
654
- });
655
- return server;
656
- },
657
- prompt(definition, handler) {
658
- assertNonEmptyName(definition.name, "Prompt name required");
659
- if (prompts.has(definition.name)) {
660
- throw new Error(`Prompt already registered: ${definition.name}`);
661
- }
662
- prompts.set(definition.name, { ...structuredClone(definition), handler });
663
- return server;
664
- },
665
- resource(definition, handler) {
666
- if (!isValidUri(definition.uri)) {
667
- throw new Error(`Invalid resource URI: ${definition.uri}`);
668
- }
669
- if (resources.has(definition.uri)) {
670
- throw new Error(`Resource already registered: ${definition.uri}`);
671
- }
672
- resources.set(definition.uri, { ...structuredClone(definition), handler });
673
- return server;
674
- },
675
- resourceTemplate(definition, handler) {
676
- const template = parseReadableUriTemplate(definition.uriTemplate);
677
- if (resourceTemplates.has(definition.uriTemplate)) {
678
- throw new Error(`Resource template already registered: ${definition.uriTemplate}`);
679
- }
680
- resourceTemplates.set(definition.uriTemplate, { ...structuredClone(definition), handler, template });
681
- return server;
682
- },
683
- method(name, handler) {
684
- assertNonEmptyName(name, "Method name required");
685
- methods.set(name, handler);
686
- return server;
687
- },
688
- onNotification(listener) {
689
- notificationListeners.add(listener);
690
- return () => {
691
- notificationListeners.delete(listener);
692
- };
693
- },
694
- removeTool(name) {
695
- return tools.delete(name);
696
- },
697
- removePrompt(name) {
698
- return prompts.delete(name);
699
- },
700
- removeResource(uri) {
701
- return resources.delete(uri);
702
- },
703
- removeResourceTemplate(uriTemplate) {
704
- return resourceTemplates.delete(uriTemplate);
705
- },
706
- async notifyToolsChanged() {
707
- if (supportNotifications &&
708
- [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady || lifecycle.subscriptions.size > 0)) {
709
- await broadcastNotification("notifications/tools/list_changed");
710
- }
711
- },
712
- async notifyPromptsChanged() {
713
- if (supportNotifications &&
714
- [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady || lifecycle.subscriptions.size > 0)) {
715
- await broadcastNotification("notifications/prompts/list_changed");
716
- }
717
- },
718
- async notifyResourcesChanged() {
719
- if (supportNotifications &&
720
- [...messageLifecycles].some((lifecycle) => lifecycle.notificationReady || lifecycle.subscriptions.size > 0)) {
721
- await broadcastNotification("notifications/resources/list_changed");
722
- }
723
- },
724
- async notifyResourceUpdated(uri) {
725
- if (!supportResourceSubscriptions) {
726
- return;
727
- }
728
- await broadcastNotification("notifications/resources/updated", { uri }, (lifecycle) => lifecycle.resourceSubscriptions.has(uri));
729
- },
730
- createMessageSession,
731
- handleMessage,
732
- async listen() {
733
- return server.connect({
734
- readable: process.stdin,
735
- writable: process.stdout
736
- });
737
- },
738
- async connect(transport) {
739
- return new Promise((resolve, reject) => {
740
- let inputClosed = false;
741
- let settled = false;
742
- const pendingMessages = new Set();
743
- const subscriptions = new AbortController();
744
- const output = new StdioOutput(transport.writable, maxStdioOutputBytes, fail, finish);
745
- const listener = (notification) => output.write(`${JSON.stringify(notification)}\n`);
746
- const session = server.createMessageSession(listener);
747
- const input = new StdioInput(maxStdioLineBytes);
748
- const rl = readline.createInterface({
749
- input,
750
- crlfDelay: Infinity
751
- });
752
- function fail(error) {
753
- if (settled)
754
- return;
755
- settled = true;
756
- session.close();
757
- rl.close();
758
- transport.readable.unpipe(input);
759
- input.destroy();
760
- transport.readable.pause();
761
- transport.readable.off("error", fail);
762
- output.abort(error);
763
- pendingMessages.clear();
764
- reject(error);
765
- }
766
- function finish() {
767
- if (settled || !inputClosed || pendingMessages.size > 0 || output.pending > 0)
768
- return;
769
- settled = true;
770
- session.close();
771
- transport.readable.unpipe(input);
772
- input.destroy();
773
- transport.readable.off("error", fail);
774
- output.close();
775
- resolve();
776
- }
777
- transport.readable.on("error", fail);
778
- input.on("error", fail);
779
- rl.on("error", fail);
780
- rl.on("line", (line) => {
781
- if (settled)
782
- return;
783
- if (pendingMessages.size >= maxPendingStdioMessages) {
784
- fail(new Error("Stdio pending message limit exceeded"));
785
- return;
786
- }
787
- const message = processLine(line, (data) => output.write(data), session.handleMessage, subscriptions.signal);
788
- if (!settled)
789
- pendingMessages.add(message);
790
- void message.then(() => {
791
- pendingMessages.delete(message);
792
- finish();
793
- }, fail);
794
- });
795
- rl.on("close", () => {
796
- inputClosed = true;
797
- subscriptions.abort();
798
- finish();
799
- });
800
- transport.readable.pipe(input);
801
- });
802
- },
803
- async connectSDK(transport) {
804
- return new Promise((resolve, reject) => {
805
- const listener = (notification) => transport.send(notification);
806
- const session = server.createMessageSession(listener);
807
- transport.onmessage = async (message) => {
808
- // Ignore responses (we only handle requests/notifications)
809
- if (!("method" in message)) {
810
- return;
811
- }
812
- // Handle notifications (no id) - don't respond
813
- if (!("id" in message) || message.id === undefined) {
814
- if (!message.method.startsWith("notifications/") &&
815
- selectRequestProtocol(message.method, message.params, SUPPORTED_PROTOCOL_VERSIONS)
816
- .modern)
817
- return;
818
- if (message.method === "initialize") {
819
- return;
820
- }
821
- try {
822
- await session.handleMessage(message.method, message.params);
823
- }
824
- catch {
825
- return;
826
- }
827
- return;
828
- }
829
- if (message.method === "notifications/initialized") {
830
- await transport.send({
831
- jsonrpc: "2.0",
832
- id: message.id,
833
- error: {
834
- code: JSON_RPC_ERROR_CODES.INVALID_REQUEST,
835
- message: "Invalid Request"
836
- }
837
- });
838
- return;
839
- }
840
- const request = message;
841
- if (selectRequestProtocol(request.method, request.params, SUPPORTED_PROTOCOL_VERSIONS)
842
- .modern &&
843
- !isRequestId(request.id)) {
844
- await transport.send({
845
- jsonrpc: "2.0",
846
- id: null,
847
- error: { code: JSON_RPC_ERROR_CODES.INVALID_REQUEST, message: "Invalid Request ID" }
848
- });
849
- return;
850
- }
851
- let handled;
852
- try {
853
- handled = await session.handleMessage(request.method, request.params, request.id === null ? undefined : { requestId: request.id });
854
- }
855
- catch {
856
- await transport.send({
857
- jsonrpc: "2.0",
858
- id: request.id,
859
- error: {
860
- code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
861
- message: "Internal error"
862
- }
863
- });
864
- return;
865
- }
866
- const { result, error } = handled;
867
- if (error) {
868
- const response = {
869
- jsonrpc: "2.0",
870
- id: request.id,
871
- error
872
- };
873
- await transport.send(response);
874
- }
875
- else if (result !== undefined) {
876
- const response = {
877
- jsonrpc: "2.0",
878
- id: request.id,
879
- result
880
- };
881
- await transport.send(response);
882
- }
883
- };
884
- transport.onclose = () => {
885
- session.close();
886
- resolve();
887
- };
888
- void transport.start().catch((error) => {
889
- session.close();
890
- reject(error);
891
- });
892
- });
893
- }
894
- };
895
- return server;
896
- }
897
- function invalidParams(message) {
898
- return {
899
- error: {
900
- code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
901
- message
902
- }
903
- };
904
- }
905
- function internalError(message) {
906
- return {
907
- error: {
908
- code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
909
- message
910
- }
911
- };
912
- }
913
- function resourceNotFound(uri) {
914
- return {
915
- error: {
916
- code: JSON_RPC_ERROR_CODES.RESOURCE_NOT_FOUND,
917
- message: `Resource not found: ${uri}`
918
- }
919
- };
920
- }
921
- function toErrorMessage(error) {
922
- return error instanceof Error ? error.message : String(error);
923
- }
924
- function assertNonEmptyName(name, message) {
925
- if (name.length === 0) {
926
- throw new Error(message);
927
- }
928
- }
929
- function parseReadableUriTemplate(uriTemplate) {
930
- const template = parseUriTemplate(uriTemplate);
931
- if (!isValidUri(template.expand({}))) {
932
- throw new Error(`Invalid resource URI template: ${uriTemplate}`);
933
- }
934
- return template;
935
- }
936
- function toStringArguments(value) {
937
- if (value === undefined) {
938
- return {};
939
- }
940
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
941
- return undefined;
942
- }
943
- const args = {};
944
- for (const [name, argument] of Object.entries(value)) {
945
- if (typeof argument !== "string") {
946
- return undefined;
947
- }
948
- args[name] = argument;
949
- }
950
- return args;
951
- }
952
- function hasRequiredPromptArguments(prompt, args) {
953
- return (prompt.arguments ?? []).every((argument) => argument.required !== true || args[argument.name] !== undefined);
954
- }
955
- function findReadableResource(uri, resources, resourceTemplates) {
956
- const resource = resources.get(uri);
957
- if (resource !== undefined) {
958
- return resource;
959
- }
960
- return [...resourceTemplates.values()].find((template) => template.template.match(uri) !== null);
961
- }
962
- function isInputRequiredResult(value) {
963
- return (typeof value === "object" &&
964
- value !== null &&
965
- !Array.isArray(value) &&
966
- value.resultType === "input_required");
967
- }
968
- function isCallToolResult(value, modern) {
969
- if (!hasContentArray(value) || !value.content.every(isContentItem)) {
970
- return false;
971
- }
972
- if (hasOwnProperty(value, "structuredContent") &&
973
- value.structuredContent !== undefined &&
974
- !(isJsonValue(value.structuredContent) && (modern || isJsonObject(value.structuredContent)))) {
975
- return false;
976
- }
977
- return !(hasOwnProperty(value, "isError") &&
978
- value.isError !== undefined &&
979
- typeof value.isError !== "boolean");
980
- }
981
- function normalizeToolResult(handlerResult, outputSchema, modern) {
982
- if (!modern && outputSchema !== undefined && outputSchema.type !== "object") {
983
- const result = normalizeToolResult(handlerResult, outputSchema, true);
984
- const { structuredContent, ...legacyResult } = result;
985
- return {
986
- ...legacyResult,
987
- content: typeof handlerResult === "string"
988
- ? [{ type: "text", text: handlerResult }]
989
- : legacyResult.content.length > 0 || structuredContent === undefined
990
- ? legacyResult.content
991
- : [{ type: "text", text: JSON.stringify(structuredContent) }]
992
- };
993
- }
994
- if (hasContentArray(handlerResult) && !isCallToolResult(handlerResult, modern)) {
995
- if (outputSchema !== undefined)
996
- throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, "Invalid tool result");
997
- throw new Error("Invalid tool result");
998
- }
999
- if (outputSchema === undefined) {
1000
- const result = isCallToolResult(handlerResult, modern)
1001
- ? handlerResult
1002
- : { content: toContentBlocks(handlerResult) };
1003
- if (!isCallToolResult(result, modern)) {
1004
- throw new Error("Invalid tool result");
1005
- }
1006
- return result;
1007
- }
1008
- if (isCallToolResult(handlerResult, modern) && handlerResult.isError === true) {
1009
- return handlerResult;
1010
- }
1011
- const callToolResult = isCallToolResult(handlerResult, modern) ? handlerResult : undefined;
1012
- const structuredContent = callToolResult ? callToolResult.structuredContent : handlerResult;
1013
- if (!(isJsonValue(structuredContent) && (modern || isJsonObject(structuredContent)))) {
1014
- throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, modern ? "Structured tool result must be JSON" : "Structured tool result must be a JSON object");
1015
- }
1016
- return {
1017
- ...callToolResult,
1018
- content: callToolResult !== undefined && callToolResult.content.length > 0
1019
- ? callToolResult.content
1020
- : [{ type: "text", text: JSON.stringify(structuredContent) }],
1021
- structuredContent
1022
- };
1023
- }
1024
- function assertObjectRootSchema(schema, path) {
1025
- if (typeof schema !== "object" || schema === null || !("type" in schema) || schema.type !== "object") {
1026
- throw new Error(`${path} root type must be "object"`);
1027
- }
1028
- }
1029
- function assertOutputSchema(schema) {
1030
- if (typeof schema !== "object" || schema === null || Array.isArray(schema))
1031
- throw new Error("outputSchema must be a JSON Schema object");
1032
- }
1033
- function isJsonObject(value) {
1034
- return typeof value === "object" && value !== null && !Array.isArray(value);
1035
- }
1036
- function isGetPromptResult(value, modern = false) {
1037
- if (typeof value !== "object" || value === null || !hasOwnProperty(value, "messages")) {
1038
- return false;
1039
- }
1040
- return ((!hasOwnProperty(value, "description") ||
1041
- value.description === undefined ||
1042
- typeof value.description === "string") &&
1043
- Array.isArray(value.messages) &&
1044
- value.messages.every((message) => typeof message === "object" &&
1045
- message !== null &&
1046
- hasOwnProperty(message, "role") &&
1047
- (message.role === "user" || message.role === "assistant") &&
1048
- hasOwnProperty(message, "content") &&
1049
- (modern ? isContentItem(message.content) : isPromptContentItem(message.content))));
1050
- }
1051
- function isReadResourceResult(value) {
1052
- if (typeof value !== "object" || value === null || !hasOwnProperty(value, "contents")) {
1053
- return false;
1054
- }
1055
- return Array.isArray(value.contents) && value.contents.every(isResourceContents);
1056
- }
1057
- function hasContentArray(value) {
1058
- return (typeof value === "object" &&
1059
- value !== null &&
1060
- hasOwnProperty(value, "content") &&
1061
- Array.isArray(value.content));
1062
- }
1063
- function isContentItem(value) {
1064
- if (typeof value !== "object" || value === null || !hasOwnProperty(value, "type")) {
1065
- return false;
1066
- }
1067
- const block = value;
1068
- if (!hasValidContentAnnotations(block)) {
1069
- return false;
1070
- }
1071
- if (block.type === "text") {
1072
- return hasOwnProperty(block, "text") && typeof block.text === "string";
1073
- }
1074
- if (block.type === "image" || block.type === "audio") {
1075
- return (hasOwnProperty(block, "data") &&
1076
- typeof block.data === "string" &&
1077
- isBase64(block.data) &&
1078
- hasOwnProperty(block, "mimeType") &&
1079
- typeof block.mimeType === "string");
1080
- }
1081
- if (block.type === "resource_link") {
1082
- return (hasOwnProperty(block, "uri") &&
1083
- typeof block.uri === "string" &&
1084
- isValidUri(block.uri) &&
1085
- hasOwnProperty(block, "name") &&
1086
- typeof block.name === "string" &&
1087
- (!hasOwnProperty(block, "title") ||
1088
- block.title === undefined ||
1089
- typeof block.title === "string") &&
1090
- (!hasOwnProperty(block, "description") ||
1091
- block.description === undefined ||
1092
- typeof block.description === "string") &&
1093
- (!hasOwnProperty(block, "mimeType") ||
1094
- block.mimeType === undefined ||
1095
- typeof block.mimeType === "string") &&
1096
- (!hasOwnProperty(block, "size") || block.size === undefined || typeof block.size === "number"));
1097
- }
1098
- if (block.type !== "resource" ||
1099
- !hasOwnProperty(block, "resource") ||
1100
- typeof block.resource !== "object" ||
1101
- block.resource === null) {
1102
- return false;
1103
- }
1104
- return isResourceContents(block.resource);
1105
- }
1106
- function isResourceContents(value) {
1107
- if (typeof value !== "object" ||
1108
- value === null ||
1109
- !hasOwnProperty(value, "uri") ||
1110
- typeof value.uri !== "string" ||
1111
- !isValidUri(value.uri)) {
1112
- return false;
1113
- }
1114
- if (hasOwnProperty(value, "mimeType") &&
1115
- value.mimeType !== undefined &&
1116
- typeof value.mimeType !== "string") {
1117
- return false;
1118
- }
1119
- return ((hasOwnProperty(value, "text") && typeof value.text === "string") ||
1120
- (hasOwnProperty(value, "blob") && typeof value.blob === "string" && isBase64(value.blob)));
1121
- }
1122
- function hasValidContentAnnotations(value) {
1123
- if (!hasOwnProperty(value, "annotations") || value.annotations === undefined) {
1124
- return true;
1125
- }
1126
- if (!isJsonObject(value.annotations)) {
1127
- return false;
1128
- }
1129
- const { audience, priority, lastModified } = value.annotations;
1130
- return ((audience === undefined ||
1131
- (Array.isArray(audience) &&
1132
- audience.every((item) => item === "user" || item === "assistant"))) &&
1133
- (priority === undefined || typeof priority === "number") &&
1134
- (lastModified === undefined || typeof lastModified === "string"));
1135
- }
1136
- function isPromptContentItem(value) {
1137
- if (!isContentItem(value)) {
1138
- return false;
1139
- }
1140
- return !(typeof value === "object" &&
1141
- value !== null &&
1142
- hasOwnProperty(value, "type") &&
1143
- value.type === "resource_link");
1144
- }
1145
- function hasOwnProperty(value, name) {
1146
- return Object.prototype.hasOwnProperty.call(value, name);
6
+ return createProtocolServer(options, { readline, StdioInput, StdioOutput });
1147
7
  }