From 6db0e68e45009902f9654dc17493a3d840a17360 Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Mon, 11 Aug 2025 17:29:34 -0700 Subject: [PATCH 1/7] chore: initial upgrade, broken tests --- package-lock.json | 8 +- package.json | 2 +- src/client/index.test.ts | 2 +- .../server/jsonResponseStreamableHttp.ts | 2 +- src/examples/server/mcpServerOutputSchema.ts | 2 +- src/examples/server/simpleSseServer.ts | 6 +- .../server/simpleStatelessStreamableHttp.ts | 6 +- src/examples/server/simpleStreamableHttp.ts | 6 +- .../sseAndStreamableHttpCompatibleServer.ts | 6 +- src/examples/server/toolWithSampleServer.ts | 2 +- .../stateManagementStreamableHttp.test.ts | 4 +- .../taskResumability.test.ts | 8 +- src/server/auth/handlers/authorize.ts | 8 +- src/server/auth/handlers/token.ts | 6 +- src/server/auth/middleware/clientAuth.ts | 2 +- src/server/completable.test.ts | 2 +- src/server/completable.ts | 95 +- src/server/index.test.ts | 2 +- src/server/mcp.test.ts | 931 ++++++++------- src/server/mcp.ts | 456 +++++--- src/server/sse.test.ts | 2 +- src/server/streamableHttp.test.ts | 2 +- src/server/title.test.ts | 2 +- src/shared/auth.ts | 48 +- .../protocol-transport-handling.test.ts | 2 +- src/shared/protocol.test.ts | 2 +- src/shared/protocol.ts | 2 +- src/types.ts | 1001 ++++++++--------- 28 files changed, 1381 insertions(+), 1236 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fdf89b2e..b8ac05e03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", + "zod": "^4.0.17", "zod-to-json-schema": "^3.24.1" }, "devDependencies": { @@ -6639,9 +6639,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 2f5a030bb..146fdaafd 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", + "zod": "^4.0.17", "zod-to-json-schema": "^3.24.1" }, "devDependencies": { diff --git a/src/client/index.test.ts b/src/client/index.test.ts index abd0c34e4..41fb4c1aa 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -2,7 +2,7 @@ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Client } from "./index.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { RequestSchema, NotificationSchema, diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index d6501d275..1dfed4f65 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -2,7 +2,7 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import cors from 'cors'; diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 75bfe6900..1a43d24e2 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -6,7 +6,7 @@ import { McpServer } from "../../server/mcp.js"; import { StdioServerTransport } from "../../server/stdio.js"; -import { z } from "zod"; +import { z } from "zod/v4"; const server = new McpServer( { diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index f8bdd4662..bae583f7a 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { SSEServerTransport } from '../../server/sse.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult } from '../../types.js'; /** @@ -25,8 +25,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications', { - interval: z.number().describe('Interval in milliseconds between notifications').default(1000), - count: z.number().describe('Number of notifications to send').default(10), + interval: z.number().describe('Interval in milliseconds between notifications').prefault(1000), + count: z.number().describe('Number of notifications to send').prefault(10), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index b5a1e291e..365700933 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -1,7 +1,7 @@ import express, { Request, Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js'; import cors from 'cors'; @@ -39,8 +39,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(10), + interval: z.number().describe('Interval in milliseconds between notifications').prefault(100), + count: z.number().describe('Number of notifications to send (0 for 100)').prefault(10), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 98f9d351c..ca19adb39 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -1,6 +1,6 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; @@ -270,8 +270,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + interval: z.number().describe('Interval in milliseconds between notifications').prefault(100), + count: z.number().describe('Number of notifications to send (0 for 100)').prefault(50), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index e097ca70e..89107a009 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { SSEServerTransport } from '../../server/sse.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; import cors from 'cors'; @@ -30,8 +30,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').default(100), - count: z.number().describe('Number of notifications to send (0 for 100)').default(50), + interval: z.number().describe('Interval in milliseconds between notifications').prefault(100), + count: z.number().describe('Number of notifications to send (0 for 100)').prefault(50), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/toolWithSampleServer.ts b/src/examples/server/toolWithSampleServer.ts index 44e5cecbb..bf480a858 100644 --- a/src/examples/server/toolWithSampleServer.ts +++ b/src/examples/server/toolWithSampleServer.ts @@ -3,7 +3,7 @@ import { McpServer } from "../../server/mcp.js"; import { StdioServerTransport } from "../../server/stdio.js"; -import { z } from "zod"; +import { z } from "zod/v4"; const mcpServer = new McpServer({ name: "tools-with-sample-server", diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index 4a191134b..d2e3d56b0 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -6,7 +6,7 @@ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js'; import { McpServer } from '../server/mcp.js'; import { StreamableHTTPServerTransport } from '../server/streamableHttp.js'; import { CallToolResultSchema, ListToolsResultSchema, ListResourcesResultSchema, ListPromptsResultSchema, LATEST_PROTOCOL_VERSION } from '../types.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; describe('Streamable HTTP Transport Session Management', () => { // Function to set up the server with optional session management @@ -55,7 +55,7 @@ describe('Streamable HTTP Transport Session Management', () => { 'greet', 'A simple greeting tool', { - name: z.string().describe('Name to greet').default('World'), + name: z.string().describe('Name to greet').prefault('World'), }, async ({ name }) => { return { diff --git a/src/integration-tests/taskResumability.test.ts b/src/integration-tests/taskResumability.test.ts index efd2611f8..9802e0582 100644 --- a/src/integration-tests/taskResumability.test.ts +++ b/src/integration-tests/taskResumability.test.ts @@ -6,7 +6,7 @@ import { StreamableHTTPClientTransport } from '../client/streamableHttp.js'; import { McpServer } from '../server/mcp.js'; import { StreamableHTTPServerTransport } from '../server/streamableHttp.js'; import { CallToolResultSchema, LoggingMessageNotificationSchema } from '../types.js'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { InMemoryEventStore } from '../examples/shared/inMemoryEventStore.js'; @@ -33,7 +33,7 @@ describe('Transport resumability', () => { 'send-notification', 'Sends a single notification', { - message: z.string().describe('Message to send').default('Test notification') + message: z.string().describe('Message to send').prefault('Test notification') }, async ({ message }, { sendNotification }) => { // Send notification immediately @@ -56,8 +56,8 @@ describe('Transport resumability', () => { 'run-notifications', 'Sends multiple notifications over time', { - count: z.number().describe('Number of notifications to send').default(10), - interval: z.number().describe('Interval between notifications in ms').default(50) + count: z.number().describe('Number of notifications to send').prefault(10), + interval: z.number().describe('Interval between notifications in ms').prefault(50) }, async ({ count, interval }, { sendNotification }) => { // Send notifications at specified intervals diff --git a/src/server/auth/handlers/authorize.ts b/src/server/auth/handlers/authorize.ts index 126ce006b..81078e0de 100644 --- a/src/server/auth/handlers/authorize.ts +++ b/src/server/auth/handlers/authorize.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { z } from "zod"; +import { z } from "zod/v4"; import express from "express"; import { OAuthServerProvider } from "../provider.js"; import { rateLimit, Options as RateLimitOptions } from "express-rate-limit"; @@ -25,7 +25,9 @@ export type AuthorizationHandlerOptions = { // Parameters that must be validated in order to issue redirects. const ClientAuthorizationParamsSchema = z.object({ client_id: z.string(), - redirect_uri: z.string().optional().refine((value) => value === undefined || URL.canParse(value), { message: "redirect_uri must be a valid URL" }), + redirect_uri: z.string().optional().refine((value) => value === undefined || URL.canParse(value), { + error: "redirect_uri must be a valid URL" +}), }); // Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI. @@ -35,7 +37,7 @@ const RequestAuthorizationParamsSchema = z.object({ code_challenge_method: z.literal("S256"), scope: z.string().optional(), state: z.string().optional(), - resource: z.string().url().optional(), + resource: z.url().optional(), }); export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): RequestHandler { diff --git a/src/server/auth/handlers/token.ts b/src/server/auth/handlers/token.ts index b2ab74391..caf36d0d1 100644 --- a/src/server/auth/handlers/token.ts +++ b/src/server/auth/handlers/token.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import express, { RequestHandler } from "express"; import { OAuthServerProvider } from "../provider.js"; import cors from "cors"; @@ -32,13 +32,13 @@ const AuthorizationCodeGrantSchema = z.object({ code: z.string(), code_verifier: z.string(), redirect_uri: z.string().optional(), - resource: z.string().url().optional(), + resource: z.url().optional(), }); const RefreshTokenGrantSchema = z.object({ refresh_token: z.string(), scope: z.string().optional(), - resource: z.string().url().optional(), + resource: z.url().optional(), }); export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): RequestHandler { diff --git a/src/server/auth/middleware/clientAuth.ts b/src/server/auth/middleware/clientAuth.ts index ecd9a7b65..682cb269f 100644 --- a/src/server/auth/middleware/clientAuth.ts +++ b/src/server/auth/middleware/clientAuth.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { RequestHandler } from "express"; import { OAuthRegisteredClientsStore } from "../clients.js"; import { OAuthClientInformationFull } from "../../../shared/auth.js"; diff --git a/src/server/completable.test.ts b/src/server/completable.test.ts index 6040ff3f6..66184128d 100644 --- a/src/server/completable.test.ts +++ b/src/server/completable.test.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { completable } from "./completable.js"; describe("completable", () => { diff --git a/src/server/completable.ts b/src/server/completable.ts index 652eaf72e..d83c0c888 100644 --- a/src/server/completable.ts +++ b/src/server/completable.ts @@ -1,13 +1,4 @@ -import { - ZodTypeAny, - ZodTypeDef, - ZodType, - ParseInput, - ParseReturnType, - RawCreateParams, - ZodErrorMap, - ProcessedCreateParams, -} from "zod"; +import { ZodTypeAny } from "zod/v4"; export enum McpZodTypeKind { Completable = "McpCompletable", @@ -17,82 +8,38 @@ export type CompleteCallback = ( value: T["_input"], context?: { arguments?: Record; - }, + } ) => T["_input"][] | Promise; -export interface CompletableDef - extends ZodTypeDef { +export interface CompletableDef { type: T; complete: CompleteCallback; typeName: McpZodTypeKind.Completable; } -export class Completable extends ZodType< - T["_output"], - CompletableDef, - T["_input"] -> { - _parse(input: ParseInput): ParseReturnType { - const { ctx } = this._processInputParams(input); - const data = ctx.data; - return this._def.type._parse({ - data, - path: ctx.path, - parent: ctx, - }); - } - - unwrap() { - return this._def.type; - } - - static create = ( - type: T, - params: RawCreateParams & { - complete: CompleteCallback; - }, - ): Completable => { - return new Completable({ - type, - typeName: McpZodTypeKind.Completable, - complete: params.complete, - ...processCreateParams(params), - }); - }; -} - /** * Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP. */ export function completable( schema: T, - complete: CompleteCallback, -): Completable { - return Completable.create(schema, { ...schema._def, complete }); -} - -// Not sure why this isn't exported from Zod: -// https://github.com/colinhacks/zod/blob/f7ad26147ba291cb3fb257545972a8e00e767470/src/types.ts#L130 -function processCreateParams(params: RawCreateParams): ProcessedCreateParams { - if (!params) return {}; - const { errorMap, invalid_type_error, required_error, description } = params; - if (errorMap && (invalid_type_error || required_error)) { - throw new Error( - `Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`, - ); + complete: CompleteCallback +): T & { + _def: (T extends { _def: infer D } ? D : unknown) & CompletableDef; +} { + const target = schema as unknown as { _def?: Record }; + const originalDef = (target._def ?? {}) as Record; + // Only mutate the existing _def object to respect read-only property semantics + if ( + (originalDef as { typeName?: unknown }).typeName !== + McpZodTypeKind.Completable + ) { + (originalDef as { typeName?: McpZodTypeKind; type?: ZodTypeAny }).typeName = + McpZodTypeKind.Completable; + (originalDef as { typeName?: McpZodTypeKind; type?: ZodTypeAny }).type = + schema; } - if (errorMap) return { errorMap: errorMap, description }; - const customMap: ZodErrorMap = (iss, ctx) => { - const { message } = params; - - if (iss.code === "invalid_enum_value") { - return { message: message ?? ctx.defaultError }; - } - if (typeof ctx.data === "undefined") { - return { message: message ?? required_error ?? ctx.defaultError }; - } - if (iss.code !== "invalid_type") return { message: ctx.defaultError }; - return { message: message ?? invalid_type_error ?? ctx.defaultError }; + (originalDef as { complete?: CompleteCallback }).complete = complete; + return schema as unknown as T & { + _def: (T extends { _def: infer D } ? D : unknown) & CompletableDef; }; - return { errorMap: customMap, description }; } diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 46205d726..ac5449ae6 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -2,7 +2,7 @@ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Server } from "./index.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { RequestSchema, NotificationSchema, diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 10e550df4..4df5062de 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1,7 +1,7 @@ import { McpServer } from "./mcp.js"; import { Client } from "../client/index.js"; import { InMemoryTransport } from "../inMemory.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { ListToolsResultSchema, CallToolResultSchema, @@ -14,7 +14,7 @@ import { LoggingMessageNotificationSchema, Notification, TextContent, - ElicitRequestSchema + ElicitRequestSchema, } from "../types.js"; import { ResourceTemplate } from "./mcp.js"; import { completable } from "./completable.js"; @@ -43,17 +43,17 @@ describe("McpServer", () => { name: "test server", version: "1.0", }, - { capabilities: { logging: {} } }, + { capabilities: { logging: {} } } ); - const notifications: Notification[] = [] + const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification) - } + notifications.push(notification); + }; const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -68,30 +68,28 @@ describe("McpServer", () => { mcpServer.server.sendLoggingMessage({ level: "info", data: "Test log message", - }), + }) ).resolves.not.toThrow(); expect(notifications).toMatchObject([ { - "method": "notifications/message", + method: "notifications/message", params: { level: "info", data: "Test log message", - } - } - ]) + }, + }, + ]); }); /*** * Test: Progress Notification with Message Field */ test("should send progress notifications with message field", async () => { - const mcpServer = new McpServer( - { - name: "test server", - version: "1.0", - } - ); + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); // Create a tool that sends progress updates mcpServer.tool( @@ -118,18 +116,30 @@ describe("McpServer", () => { } } - return { content: [{ type: "text" as const, text: `Operation completed with ${steps} steps` }] }; + return { + content: [ + { + type: "text" as const, + text: `Operation completed with ${steps} steps`, + }, + ], + }; } ); - const progressUpdates: Array<{ progress: number, total?: number, message?: string }> = []; + const progressUpdates: Array<{ + progress: number; + total?: number; + message?: string; + }> = []; const client = new Client({ name: "test client", version: "1.0", }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -144,15 +154,15 @@ describe("McpServer", () => { name: "long-operation", arguments: { steps: 3 }, _meta: { - progressToken: "progress-test-1" - } - } + progressToken: "progress-test-1", + }, + }, }, CallToolResultSchema, { onprogress: (progress) => { progressUpdates.push(progress); - } + }, } ); @@ -212,9 +222,13 @@ describe("ResourceTemplate", () => { const abortController = new AbortController(); const result = await template.listCallback?.({ signal: abortController.signal, - requestId: 'not-implemented', - sendRequest: () => { throw new Error("Not implemented") }, - sendNotification: () => { throw new Error("Not implemented") } + requestId: "not-implemented", + sendRequest: () => { + throw new Error("Not implemented"); + }, + sendNotification: () => { + throw new Error("Not implemented"); + }, }); expect(result?.resources).toHaveLength(1); expect(list).toHaveBeenCalled(); @@ -230,14 +244,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = [] + const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification) - } + notifications.push(notification); + }; mcpServer.tool("test", async () => ({ content: [ @@ -260,7 +274,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(1); @@ -271,7 +285,7 @@ describe("tool()", () => { }); // Adding the tool before the connection was established means no notification was sent - expect(notifications).toHaveLength(0) + expect(notifications).toHaveLength(0); // Adding another tool triggers the update notification mcpServer.tool("test2", async () => ({ @@ -284,13 +298,13 @@ describe("tool()", () => { })); // Yield event loop to let the notification fly - await new Promise(process.nextTick) + await new Promise(process.nextTick); expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed", - } - ]) + }, + ]); }); /*** @@ -301,14 +315,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = [] + const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification) - } + notifications.push(notification); + }; // Register initial tool const tool = mcpServer.tool("test", async () => ({ @@ -329,7 +343,7 @@ describe("tool()", () => { text: "Updated response", }, ], - }) + }), }); const [clientTransport, serverTransport] = @@ -348,7 +362,7 @@ describe("tool()", () => { name: "test", }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(result.content).toEqual([ @@ -359,7 +373,7 @@ describe("tool()", () => { ]); // Update happened before transport was connected, so no notifications should be expected - expect(notifications).toHaveLength(0) + expect(notifications).toHaveLength(0); }); /*** @@ -370,14 +384,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = [] + const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification) - } + notifications.push(notification); + }; // Register initial tool const tool = mcpServer.tool( @@ -392,7 +406,7 @@ describe("tool()", () => { text: `Initial: ${name}`, }, ], - }), + }) ); // Update the tool with a different schema @@ -408,7 +422,7 @@ describe("tool()", () => { text: `Updated: ${name}, ${value}`, }, ], - }) + }), }); const [clientTransport, serverTransport] = @@ -424,7 +438,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(listResult.tools[0].inputSchema).toMatchObject({ @@ -446,7 +460,7 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(callResult.content).toEqual([ @@ -457,7 +471,7 @@ describe("tool()", () => { ]); // Update happened before transport was connected, so no notifications should be expected - expect(notifications).toHaveLength(0) + expect(notifications).toHaveLength(0); }); /*** @@ -468,14 +482,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = [] + const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification) - } + notifications.push(notification); + }; // Register initial tool const tool = mcpServer.tool("test", async () => ({ @@ -495,7 +509,7 @@ describe("tool()", () => { mcpServer.connect(serverTransport), ]); - expect(notifications).toHaveLength(0) + expect(notifications).toHaveLength(0); // Now update the tool tool.update({ @@ -506,26 +520,26 @@ describe("tool()", () => { text: "Updated response", }, ], - }) + }), }); // Yield event loop to let the notification fly - await new Promise(process.nextTick) + await new Promise(process.nextTick); expect(notifications).toMatchObject([ - { method: "notifications/tools/list_changed" } - ]) + { method: "notifications/tools/list_changed" }, + ]); // Now delete the tool tool.remove(); // Yield event loop to let the notification fly - await new Promise(process.nextTick) + await new Promise(process.nextTick); expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed" }, { method: "notifications/tools/list_changed" }, - ]) + ]); }); /*** @@ -555,7 +569,7 @@ describe("tool()", () => { text: `${name}: ${value}`, }, ], - }), + }) ); // new api @@ -581,7 +595,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(2); @@ -636,7 +650,6 @@ describe("tool()", () => { }) ); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -649,7 +662,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(2); @@ -672,14 +685,18 @@ describe("tool()", () => { version: "1.0", }); - mcpServer.tool("test", { title: "Test Tool", readOnlyHint: true }, async () => ({ - content: [ - { - type: "text", - text: "Test response", - }, - ], - })); + mcpServer.tool( + "test", + { title: "Test Tool", readOnlyHint: true }, + async () => ({ + content: [ + { + type: "text", + text: "Test response", + }, + ], + }) + ); mcpServer.registerTool( "test (new api)", @@ -708,14 +725,20 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); - expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); + expect(result.tools[0].annotations).toEqual({ + title: "Test Tool", + readOnlyHint: true, + }); expect(result.tools[1].name).toBe("test (new api)"); - expect(result.tools[1].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); + expect(result.tools[1].annotations).toEqual({ + title: "Test Tool", + readOnlyHint: true, + }); }); /*** @@ -736,7 +759,7 @@ describe("tool()", () => { { name: z.string() }, { title: "Test Tool", readOnlyHint: true }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }] + content: [{ type: "text", text: `Hello, ${name}!` }], }) ); @@ -747,7 +770,7 @@ describe("tool()", () => { annotations: { title: "Test Tool", readOnlyHint: true }, }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }] + content: [{ type: "text", text: `Hello, ${name}!` }], }) ); @@ -761,16 +784,19 @@ describe("tool()", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", - properties: { name: { type: "string" } } + properties: { name: { type: "string" } }, + }); + expect(result.tools[0].annotations).toEqual({ + title: "Test Tool", + readOnlyHint: true, }); - expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); @@ -795,7 +821,7 @@ describe("tool()", () => { { name: z.string() }, { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }] + content: [{ type: "text", text: `Hello, ${name}!` }], }) ); @@ -804,10 +830,14 @@ describe("tool()", () => { { description: "A tool with everything", inputSchema: { name: z.string() }, - annotations: { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }, + annotations: { + title: "Complete Test Tool", + readOnlyHint: true, + openWorldHint: false, + }, }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }] + content: [{ type: "text", text: `Hello, ${name}!` }], }) ); @@ -821,7 +851,7 @@ describe("tool()", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(2); @@ -829,12 +859,12 @@ describe("tool()", () => { expect(result.tools[0].description).toBe("A tool with everything"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", - properties: { name: { type: "string" } } + properties: { name: { type: "string" } }, }); expect(result.tools[0].annotations).toEqual({ title: "Complete Test Tool", readOnlyHint: true, - openWorldHint: false + openWorldHint: false, }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].description).toBe("A tool with everything"); @@ -859,9 +889,13 @@ describe("tool()", () => { "test", "A tool with everything but empty params", {}, - { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, + { + title: "Complete Test Tool with empty params", + readOnlyHint: true, + openWorldHint: false, + }, async () => ({ - content: [{ type: "text", text: "Test response" }] + content: [{ type: "text", text: "Test response" }], }) ); @@ -870,10 +904,14 @@ describe("tool()", () => { { description: "A tool with everything but empty params", inputSchema: {}, - annotations: { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, + annotations: { + title: "Complete Test Tool with empty params", + readOnlyHint: true, + openWorldHint: false, + }, }, async () => ({ - content: [{ type: "text" as const, text: "Test response" }] + content: [{ type: "text" as const, text: "Test response" }], }) ); @@ -887,23 +925,27 @@ describe("tool()", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); - expect(result.tools[0].description).toBe("A tool with everything but empty params"); + expect(result.tools[0].description).toBe( + "A tool with everything but empty params" + ); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", - properties: {} + properties: {}, }); expect(result.tools[0].annotations).toEqual({ title: "Complete Test Tool with empty params", readOnlyHint: true, - openWorldHint: false + openWorldHint: false, }); expect(result.tools[1].name).toBe("test (new api)"); - expect(result.tools[1].description).toBe("A tool with everything but empty params"); + expect(result.tools[1].description).toBe( + "A tool with everything but empty params" + ); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); @@ -934,7 +976,7 @@ describe("tool()", () => { text: `${name}: ${value}`, }, ], - }), + }) ); mcpServer.registerTool( @@ -975,8 +1017,8 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema, - ), + CallToolResultSchema + ) ).rejects.toThrow(/Invalid arguments/); await expect( @@ -991,8 +1033,8 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema, - ), + CallToolResultSchema + ) ).rejects.toThrow(/Invalid arguments/); }); @@ -1067,14 +1109,14 @@ describe("tool()", () => { outputSchema: { processedInput: z.string(), resultType: z.string(), - timestamp: z.string() + timestamp: z.string(), }, }, async ({ input }) => ({ structuredContent: { processedInput: input, resultType: "structured", - timestamp: "2023-01-01T00:00:00Z" + timestamp: "2023-01-01T00:00:00Z", }, content: [ { @@ -1082,10 +1124,10 @@ describe("tool()", () => { text: JSON.stringify({ processedInput: input, resultType: "structured", - timestamp: "2023-01-01T00:00:00Z" + timestamp: "2023-01-01T00:00:00Z", }), }, - ] + ], }) ); @@ -1102,7 +1144,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema, + ListToolsResultSchema ); expect(listResult.tools).toHaveLength(1); @@ -1111,9 +1153,9 @@ describe("tool()", () => { properties: { processedInput: { type: "string" }, resultType: { type: "string" }, - timestamp: { type: "string" } + timestamp: { type: "string" }, }, - required: ["processedInput", "resultType", "timestamp"] + required: ["processedInput", "resultType", "timestamp"], }); // Call the tool and verify it returns valid structuredContent @@ -1127,7 +1169,7 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(result.structuredContent).toBeDefined(); @@ -1166,7 +1208,8 @@ describe("tool()", () => { mcpServer.registerTool( "test", { - description: "Test tool with output schema but missing structured content", + description: + "Test tool with output schema but missing structured content", inputSchema: { input: z.string(), }, @@ -1201,8 +1244,10 @@ describe("tool()", () => { arguments: { input: "hello", }, - }), - ).rejects.toThrow(/Tool test has an output schema but no structured content was provided/); + }) + ).rejects.toThrow( + /Tool test has an output schema but no structured content was provided/ + ); }); /*** * Test: Tool with Output Schema Must Provide Structured Content @@ -1221,7 +1266,8 @@ describe("tool()", () => { mcpServer.registerTool( "test", { - description: "Test tool with output schema but missing structured content", + description: + "Test tool with output schema but missing structured content", inputSchema: { input: z.string(), }, @@ -1255,7 +1301,7 @@ describe("tool()", () => { arguments: { input: "hello", }, - }), + }) ).resolves.toStrictEqual({ content: [ { @@ -1292,7 +1338,7 @@ describe("tool()", () => { outputSchema: { processedInput: z.string(), resultType: z.string(), - timestamp: z.string() + timestamp: z.string(), }, }, async ({ input }) => ({ @@ -1303,7 +1349,7 @@ describe("tool()", () => { processedInput: input, resultType: "structured", // Missing required 'timestamp' field - someExtraField: "unexpected" // Extra field not in schema + someExtraField: "unexpected", // Extra field not in schema }), }, ], @@ -1311,7 +1357,7 @@ describe("tool()", () => { processedInput: input, resultType: "structured", // Missing required 'timestamp' field - someExtraField: "unexpected" // Extra field not in schema + someExtraField: "unexpected", // Extra field not in schema }, }) ); @@ -1331,7 +1377,7 @@ describe("tool()", () => { arguments: { input: "hello", }, - }), + }) ).rejects.toThrow(/Invalid structured content for tool test/); }); @@ -1362,7 +1408,8 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); // Set a test sessionId on the server transport serverTransport.sessionId = "test-session-123"; @@ -1378,7 +1425,7 @@ describe("tool()", () => { name: "test-tool", }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(receivedSessionId).toBe("test-session-123"); @@ -1411,7 +1458,8 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1425,12 +1473,17 @@ describe("tool()", () => { name: "request-id-test", }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(receivedRequestId).toBeDefined(); - expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); - expect(result.content && result.content[0].text).toContain("Received request ID:"); + expect( + typeof receivedRequestId === "string" || + typeof receivedRequestId === "number" + ).toBe(true); + expect(result.content && result.content[0].text).toContain( + "Received request ID:" + ); }); /*** @@ -1442,7 +1495,7 @@ describe("tool()", () => { name: "test server", version: "1.0", }, - { capabilities: { logging: {} } }, + { capabilities: { logging: {} } } ); const client = new Client({ @@ -1453,12 +1506,18 @@ describe("tool()", () => { let receivedLogMessage: string | undefined; const loggingMessage = "hello here is log message 1"; - client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { - receivedLogMessage = notification.params.data as string; - }); + client.setNotificationHandler( + LoggingMessageNotificationSchema, + (notification) => { + receivedLogMessage = notification.params.data as string; + } + ); mcpServer.tool("test-tool", async ({ sendNotification }) => { - await sendNotification({ method: "notifications/message", params: { level: "debug", data: loggingMessage } }); + await sendNotification({ + method: "notifications/message", + params: { level: "debug", data: loggingMessage }, + }); return { content: [ { @@ -1469,7 +1528,8 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), @@ -1481,7 +1541,7 @@ describe("tool()", () => { name: "test-tool", }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(receivedLogMessage).toBe(loggingMessage); }); @@ -1513,7 +1573,7 @@ describe("tool()", () => { text: `Processed: ${input}`, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -1534,7 +1594,7 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(result.content).toEqual([ @@ -1578,7 +1638,7 @@ describe("tool()", () => { name: "error-test", }, }, - CallToolResultSchema, + CallToolResultSchema ); expect(result.isError).toBe(true); @@ -1629,8 +1689,8 @@ describe("tool()", () => { name: "nonexistent-tool", }, }, - CallToolResultSchema, - ), + CallToolResultSchema + ) ).rejects.toThrow(/Tool nonexistent-tool not found/); }); }); @@ -1670,7 +1730,7 @@ describe("resource()", () => { { method: "resources/list", }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(1); @@ -1696,14 +1756,18 @@ describe("resource()", () => { }; // Register initial resource - const resource = mcpServer.resource("test", "test://resource", async () => ({ - contents: [ - { - uri: "test://resource", - text: "Initial content", - }, - ], - })); + const resource = mcpServer.resource( + "test", + "test://resource", + async () => ({ + contents: [ + { + uri: "test://resource", + text: "Initial content", + }, + ], + }) + ); // Update the resource resource.update({ @@ -1714,7 +1778,7 @@ describe("resource()", () => { text: "Updated content", }, ], - }) + }), }); const [clientTransport, serverTransport] = @@ -1733,7 +1797,7 @@ describe("resource()", () => { uri: "test://resource", }, }, - ReadResourceResultSchema, + ReadResourceResultSchema ); expect(result.contents).toHaveLength(1); @@ -1771,7 +1835,7 @@ describe("resource()", () => { text: "Initial content", }, ], - }), + }) ); // Update the resource template @@ -1783,7 +1847,7 @@ describe("resource()", () => { text: "Updated content", }, ], - }) + }), }); const [clientTransport, serverTransport] = @@ -1802,7 +1866,7 @@ describe("resource()", () => { uri: "test://resource/123", }, }, - ReadResourceResultSchema, + ReadResourceResultSchema ); expect(result.contents).toHaveLength(1); @@ -1830,16 +1894,21 @@ describe("resource()", () => { }; // Register initial resource - const resource = mcpServer.resource("test", "test://resource", async () => ({ - contents: [ - { - uri: "test://resource", - text: "Test content", - }, - ], - })); + const resource = mcpServer.resource( + "test", + "test://resource", + async () => ({ + contents: [ + { + uri: "test://resource", + text: "Test content", + }, + ], + }) + ); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1857,14 +1926,14 @@ describe("resource()", () => { text: "Updated content", }, ], - }) + }), }); // Yield event loop to let the notification fly await new Promise(process.nextTick); expect(notifications).toMatchObject([ - { method: "notifications/resources/list_changed" } + { method: "notifications/resources/list_changed" }, ]); }); @@ -1886,15 +1955,20 @@ describe("resource()", () => { }; // Register initial resources - const resource1 = mcpServer.resource("resource1", "test://resource1", async () => ({ - contents: [{ uri: "test://resource1", text: "Resource 1 content" }], - })); + const resource1 = mcpServer.resource( + "resource1", + "test://resource1", + async () => ({ + contents: [{ uri: "test://resource1", text: "Resource 1 content" }], + }) + ); mcpServer.resource("resource2", "test://resource2", async () => ({ contents: [{ uri: "test://resource2", text: "Resource 2 content" }], })); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1904,7 +1978,7 @@ describe("resource()", () => { // Verify both resources are registered let result = await client.request( { method: "resources/list" }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(2); @@ -1912,20 +1986,20 @@ describe("resource()", () => { expect(notifications).toHaveLength(0); // Remove a resource - resource1.remove() + resource1.remove(); // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ - { method: "notifications/resources/list_changed" } + { method: "notifications/resources/list_changed" }, ]); // Verify the resource was removed result = await client.request( { method: "resources/list" }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(1); @@ -1960,10 +2034,11 @@ describe("resource()", () => { text: "Template content", }, ], - }), + }) ); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1973,27 +2048,27 @@ describe("resource()", () => { // Verify template is registered const result = await client.request( { method: "resources/templates/list" }, - ListResourceTemplatesResultSchema, + ListResourceTemplatesResultSchema ); expect(result.resourceTemplates).toHaveLength(1); expect(notifications).toHaveLength(0); // Remove the template - resourceTemplate.remove() + resourceTemplate.remove(); // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ - { method: "notifications/resources/list_changed" } + { method: "notifications/resources/list_changed" }, ]); // Verify the template was removed const result2 = await client.request( { method: "resources/templates/list" }, - ListResourceTemplatesResultSchema, + ListResourceTemplatesResultSchema ); expect(result2.resourceTemplates).toHaveLength(0); @@ -2026,7 +2101,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2041,7 +2116,7 @@ describe("resource()", () => { { method: "resources/list", }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(1); @@ -2072,7 +2147,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2087,13 +2162,13 @@ describe("resource()", () => { { method: "resources/templates/list", }, - ListResourceTemplatesResultSchema, + ListResourceTemplatesResultSchema ); expect(result.resourceTemplates).toHaveLength(1); expect(result.resourceTemplates[0].name).toBe("test"); expect(result.resourceTemplates[0].uriTemplate).toBe( - "test://resource/{id}", + "test://resource/{id}" ); }); @@ -2133,7 +2208,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2148,7 +2223,7 @@ describe("resource()", () => { { method: "resources/list", }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(2); @@ -2183,7 +2258,7 @@ describe("resource()", () => { text: `Category: ${category}, ID: ${id}`, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2201,7 +2276,7 @@ describe("resource()", () => { uri: "test://resource/books/123", }, }, - ReadResourceResultSchema, + ReadResourceResultSchema ); expect(result.contents[0].text).toBe("Category: books, ID: 123"); @@ -2286,7 +2361,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); expect(() => { @@ -2300,7 +2375,7 @@ describe("resource()", () => { text: "Test content 2", }, ], - }), + }) ); }).toThrow(/already registered/); }); @@ -2338,8 +2413,8 @@ describe("resource()", () => { uri: "test://error", }, }, - ReadResourceResultSchema, - ), + ReadResourceResultSchema + ) ).rejects.toThrow(/Resource read failed/); }); @@ -2381,8 +2456,8 @@ describe("resource()", () => { uri: "test://nonexistent", }, }, - ReadResourceResultSchema, - ), + ReadResourceResultSchema + ) ).rejects.toThrow(/Resource test:\/\/nonexistent not found/); }); @@ -2414,7 +2489,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2425,8 +2500,8 @@ describe("resource()", () => { mcpServer.server.connect(serverTransport), ]); - expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) - }) + expect(client.getServerCapabilities()).toMatchObject({ completions: {} }); + }); /*** * Test: Resource Template Parameter Completion @@ -2457,7 +2532,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2482,7 +2557,7 @@ describe("resource()", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result.completion.values).toEqual(["books", "movies", "music"]); @@ -2510,7 +2585,7 @@ describe("resource()", () => { complete: { category: (test: string) => ["books", "movies", "music"].filter((value) => - value.startsWith(test), + value.startsWith(test) ), }, }), @@ -2521,7 +2596,7 @@ describe("resource()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -2546,7 +2621,7 @@ describe("resource()", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result.completion.values).toEqual(["movies", "music"]); @@ -2568,19 +2643,24 @@ describe("resource()", () => { }); let receivedRequestId: string | number | undefined; - mcpServer.resource("request-id-test", "test://resource", async (_uri, extra) => { - receivedRequestId = extra.requestId; - return { - contents: [ - { - uri: "test://resource", - text: `Received request ID: ${extra.requestId}`, - }, - ], - }; - }); + mcpServer.resource( + "request-id-test", + "test://resource", + async (_uri, extra) => { + receivedRequestId = extra.requestId; + return { + contents: [ + { + uri: "test://resource", + text: `Received request ID: ${extra.requestId}`, + }, + ], + }; + } + ); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -2594,11 +2674,14 @@ describe("resource()", () => { uri: "test://resource", }, }, - ReadResourceResultSchema, + ReadResourceResultSchema ); expect(receivedRequestId).toBeDefined(); - expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); + expect( + typeof receivedRequestId === "string" || + typeof receivedRequestId === "number" + ).toBe(true); expect(result.contents[0].text).toContain("Received request ID:"); }); }); @@ -2641,7 +2724,7 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema, + ListPromptsResultSchema ); expect(result.prompts).toHaveLength(1); @@ -2690,7 +2773,7 @@ describe("prompt()", () => { }, }, ], - }) + }), }); const [clientTransport, serverTransport] = @@ -2709,7 +2792,7 @@ describe("prompt()", () => { name: "test", }, }, - GetPromptResultSchema, + GetPromptResultSchema ); expect(result.messages).toHaveLength(1); @@ -2752,7 +2835,7 @@ describe("prompt()", () => { }, }, ], - }), + }) ); // Update the prompt with a different schema @@ -2771,7 +2854,7 @@ describe("prompt()", () => { }, }, ], - }) + }), }); const [clientTransport, serverTransport] = @@ -2787,11 +2870,14 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema, + ListPromptsResultSchema ); expect(listResult.prompts[0].arguments).toHaveLength(2); - expect(listResult.prompts[0].arguments?.map(a => a.name).sort()).toEqual(["name", "value"]); + expect(listResult.prompts[0].arguments?.map((a) => a.name).sort()).toEqual([ + "name", + "value", + ]); // Call the prompt with the new schema const getResult = await client.request( @@ -2805,7 +2891,7 @@ describe("prompt()", () => { }, }, }, - GetPromptResultSchema, + GetPromptResultSchema ); expect(getResult.messages).toHaveLength(1); @@ -2845,7 +2931,8 @@ describe("prompt()", () => { ], })); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -2866,14 +2953,14 @@ describe("prompt()", () => { }, }, ], - }) + }), }); // Yield event loop to let the notification fly await new Promise(process.nextTick); expect(notifications).toMatchObject([ - { method: "notifications/prompts/list_changed" } + { method: "notifications/prompts/list_changed" }, ]); }); @@ -2919,7 +3006,8 @@ describe("prompt()", () => { ], })); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -2929,29 +3017,32 @@ describe("prompt()", () => { // Verify both prompts are registered let result = await client.request( { method: "prompts/list" }, - ListPromptsResultSchema, + ListPromptsResultSchema ); expect(result.prompts).toHaveLength(2); - expect(result.prompts.map(p => p.name).sort()).toEqual(["prompt1", "prompt2"]); + expect(result.prompts.map((p) => p.name).sort()).toEqual([ + "prompt1", + "prompt2", + ]); expect(notifications).toHaveLength(0); // Remove a prompt - prompt1.remove() + prompt1.remove(); // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ - { method: "notifications/prompts/list_changed" } + { method: "notifications/prompts/list_changed" }, ]); // Verify the prompt was removed result = await client.request( { method: "prompts/list" }, - ListPromptsResultSchema, + ListPromptsResultSchema ); expect(result.prompts).toHaveLength(1); @@ -2987,7 +3078,7 @@ describe("prompt()", () => { }, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3002,7 +3093,7 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema, + ListPromptsResultSchema ); expect(result.prompts).toHaveLength(1); @@ -3050,7 +3141,7 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema, + ListPromptsResultSchema ); expect(result.prompts).toHaveLength(1); @@ -3088,7 +3179,7 @@ describe("prompt()", () => { }, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3111,8 +3202,8 @@ describe("prompt()", () => { }, }, }, - GetPromptResultSchema, - ), + GetPromptResultSchema + ) ).rejects.toThrow(/Invalid arguments/); }); @@ -3198,19 +3289,17 @@ describe("prompt()", () => { }); // This should succeed - mcpServer.prompt( - "echo", - { message: z.string() }, - ({ message }) => ({ - messages: [{ + mcpServer.prompt("echo", { message: z.string() }, ({ message }) => ({ + messages: [ + { role: "user", content: { type: "text", - text: `Please process this message: ${message}` - } - }] - }) - ); + text: `Please process this message: ${message}`, + }, + }, + ], + })); }); /*** @@ -3238,7 +3327,7 @@ describe("prompt()", () => { text: "Test content", }, ], - }), + }) ); // Register a prompt with completion @@ -3246,13 +3335,15 @@ describe("prompt()", () => { "echo", { message: completable(z.string(), () => ["hello", "world"]) }, ({ message }) => ({ - messages: [{ - role: "user", - content: { - type: "text", - text: `Please process this message: ${message}` - } - }] + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please process this message: ${message}`, + }, + }, + ], }) ); }); @@ -3299,12 +3390,11 @@ describe("prompt()", () => { name: "nonexistent-prompt", }, }, - GetPromptResultSchema, - ), + GetPromptResultSchema + ) ).rejects.toThrow(/Prompt nonexistent-prompt not found/); }); - /*** * Test: Registering a prompt with a completable argument should update server capabilities to advertise support for completion */ @@ -3333,7 +3423,7 @@ describe("prompt()", () => { }, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3344,8 +3434,8 @@ describe("prompt()", () => { mcpServer.server.connect(serverTransport), ]); - expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) - }) + expect(client.getServerCapabilities()).toMatchObject({ completions: {} }); + }); /*** * Test: Prompt Argument Completion @@ -3376,7 +3466,7 @@ describe("prompt()", () => { }, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3401,7 +3491,7 @@ describe("prompt()", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result.completion.values).toEqual(["Alice", "Bob", "Charlie"]); @@ -3426,7 +3516,7 @@ describe("prompt()", () => { "test-prompt", { name: completable(z.string(), (test) => - ["Alice", "Bob", "Charlie"].filter((value) => value.startsWith(test)), + ["Alice", "Bob", "Charlie"].filter((value) => value.startsWith(test)) ), }, async ({ name }) => ({ @@ -3439,7 +3529,7 @@ describe("prompt()", () => { }, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3464,7 +3554,7 @@ describe("prompt()", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result.completion.values).toEqual(["Alice"]); @@ -3501,7 +3591,8 @@ describe("prompt()", () => { }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -3515,11 +3606,14 @@ describe("prompt()", () => { name: "request-id-test", }, }, - GetPromptResultSchema, + GetPromptResultSchema ); expect(receivedRequestId).toBeDefined(); - expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); + expect( + typeof receivedRequestId === "string" || + typeof receivedRequestId === "number" + ).toBe(true); expect(result.messages[0].content.text).toContain("Received request ID:"); }); @@ -3566,7 +3660,7 @@ describe("prompt()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3581,14 +3675,16 @@ describe("prompt()", () => { { method: "resources/list", }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(2); // Resource 1 should have its own metadata expect(result.resources[0].name).toBe("Resource 1"); - expect(result.resources[0].description).toBe("Individual resource description"); + expect(result.resources[0].description).toBe( + "Individual resource description" + ); expect(result.resources[0].mimeType).toBe("text/plain"); // Resource 2 should inherit template metadata @@ -3637,7 +3733,7 @@ describe("prompt()", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3652,7 +3748,7 @@ describe("prompt()", () => { { method: "resources/list", }, - ListResourcesResultSchema, + ListResourcesResultSchema ); expect(result.resources).toHaveLength(1); @@ -3676,19 +3772,16 @@ describe("Tool title precedence", () => { }); // Tool 1: Only name - mcpServer.tool( - "tool_name_only", - async () => ({ - content: [{ type: "text", text: "Response" }], - }) - ); + mcpServer.tool("tool_name_only", async () => ({ + content: [{ type: "text", text: "Response" }], + })); // Tool 2: Name and annotations.title mcpServer.tool( "tool_with_annotations_title", "Tool with annotations title", { - title: "Annotations Title" + title: "Annotations Title", }, async () => ({ content: [{ type: "text", text: "Response" }], @@ -3700,7 +3793,7 @@ describe("Tool title precedence", () => { "tool_with_title", { title: "Regular Title", - description: "Tool with regular title" + description: "Tool with regular title", }, async () => ({ content: [{ type: "text", text: "Response" }], @@ -3714,15 +3807,16 @@ describe("Tool title precedence", () => { title: "Regular Title Wins", description: "Tool with all titles", annotations: { - title: "Annotations Title Should Not Show" - } + title: "Annotations Title Should Not Show", + }, }, async () => ({ content: [{ type: "text", text: "Response" }], }) ); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), @@ -3730,31 +3824,32 @@ describe("Tool title precedence", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema, + ListToolsResultSchema ); - expect(result.tools).toHaveLength(4); // Tool 1: Only name - should display name - const tool1 = result.tools.find(t => t.name === "tool_name_only"); + const tool1 = result.tools.find((t) => t.name === "tool_name_only"); expect(tool1).toBeDefined(); expect(getDisplayName(tool1!)).toBe("tool_name_only"); // Tool 2: Name and annotations.title - should display annotations.title - const tool2 = result.tools.find(t => t.name === "tool_with_annotations_title"); + const tool2 = result.tools.find( + (t) => t.name === "tool_with_annotations_title" + ); expect(tool2).toBeDefined(); expect(tool2!.annotations?.title).toBe("Annotations Title"); expect(getDisplayName(tool2!)).toBe("Annotations Title"); // Tool 3: Name and title - should display title - const tool3 = result.tools.find(t => t.name === "tool_with_title"); + const tool3 = result.tools.find((t) => t.name === "tool_with_title"); expect(tool3).toBeDefined(); expect(tool3!.title).toBe("Regular Title"); expect(getDisplayName(tool3!)).toBe("Regular Title"); // Tool 4: All three - title should take precedence - const tool4 = result.tools.find(t => t.name === "tool_with_all_titles"); + const tool4 = result.tools.find((t) => t.name === "tool_with_all_titles"); expect(tool4).toBeDefined(); expect(tool4!.title).toBe("Regular Title Wins"); expect(tool4!.annotations?.title).toBe("Annotations Title Should Not Show"); @@ -3762,42 +3857,51 @@ describe("Tool title precedence", () => { }); test("getDisplayName unit tests for title precedence", () => { - // Test 1: Only name expect(getDisplayName({ name: "tool_name" })).toBe("tool_name"); // Test 2: Name and title - title wins - expect(getDisplayName({ - name: "tool_name", - title: "Tool Title" - })).toBe("Tool Title"); + expect( + getDisplayName({ + name: "tool_name", + title: "Tool Title", + }) + ).toBe("Tool Title"); // Test 3: Name and annotations.title - annotations.title wins - expect(getDisplayName({ - name: "tool_name", - annotations: { title: "Annotations Title" } - })).toBe("Annotations Title"); + expect( + getDisplayName({ + name: "tool_name", + annotations: { title: "Annotations Title" }, + }) + ).toBe("Annotations Title"); // Test 4: All three - title wins (correct precedence) - expect(getDisplayName({ - name: "tool_name", - title: "Regular Title", - annotations: { title: "Annotations Title" } - })).toBe("Regular Title"); + expect( + getDisplayName({ + name: "tool_name", + title: "Regular Title", + annotations: { title: "Annotations Title" }, + }) + ).toBe("Regular Title"); // Test 5: Empty title should not be used - expect(getDisplayName({ - name: "tool_name", - title: "", - annotations: { title: "Annotations Title" } - })).toBe("Annotations Title"); + expect( + getDisplayName({ + name: "tool_name", + title: "", + annotations: { title: "Annotations Title" }, + }) + ).toBe("Annotations Title"); // Test 6: Undefined vs null handling - expect(getDisplayName({ - name: "tool_name", - title: undefined, - annotations: { title: "Annotations Title" } - })).toBe("Annotations Title"); + expect( + getDisplayName({ + name: "tool_name", + title: undefined, + annotations: { title: "Annotations Title" }, + }) + ).toBe("Annotations Title"); }); test("should support resource template completion with resolved context", async () => { @@ -3818,9 +3922,13 @@ describe("Tool title precedence", () => { complete: { repo: (value, context) => { if (context?.arguments?.["owner"] === "org1") { - return ["project1", "project2", "project3"].filter(r => r.startsWith(value)); + return ["project1", "project2", "project3"].filter((r) => + r.startsWith(value) + ); } else if (context?.arguments?.["owner"] === "org2") { - return ["repo1", "repo2", "repo3"].filter(r => r.startsWith(value)); + return ["repo1", "repo2", "repo3"].filter((r) => + r.startsWith(value) + ); } return []; }, @@ -3828,7 +3936,7 @@ describe("Tool title precedence", () => { }), { title: "GitHub Repository", - description: "Repository information" + description: "Repository information", }, async () => ({ contents: [ @@ -3837,7 +3945,7 @@ describe("Tool title precedence", () => { text: "Test content", }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3868,10 +3976,14 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); - expect(result1.completion.values).toEqual(["project1", "project2", "project3"]); + expect(result1.completion.values).toEqual([ + "project1", + "project2", + "project3", + ]); expect(result1.completion.total).toBe(3); // Test with facebook owner @@ -3894,7 +4006,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result2.completion.values).toEqual(["repo1", "repo2", "repo3"]); @@ -3915,7 +4027,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result3.completion.values).toEqual([]); @@ -3940,20 +4052,28 @@ describe("Tool title precedence", () => { description: "Generate a greeting for team members", argsSchema: { department: completable(z.string(), (value) => { - return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value)); + return ["engineering", "sales", "marketing", "support"].filter( + (d) => d.startsWith(value) + ); }), name: completable(z.string(), (value, context) => { const department = context?.arguments?.["department"]; if (department === "engineering") { - return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value)); + return ["Alice", "Bob", "Charlie"].filter((n) => + n.startsWith(value) + ); } else if (department === "sales") { - return ["David", "Eve", "Frank"].filter(n => n.startsWith(value)); + return ["David", "Eve", "Frank"].filter((n) => + n.startsWith(value) + ); } else if (department === "marketing") { - return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value)); + return ["Grace", "Henry", "Iris"].filter((n) => + n.startsWith(value) + ); } - return ["Guest"].filter(n => n.startsWith(value)); + return ["Guest"].filter((n) => n.startsWith(value)); }), - } + }, }, async ({ department, name }) => ({ messages: [ @@ -3965,7 +4085,7 @@ describe("Tool title precedence", () => { }, }, ], - }), + }) ); const [clientTransport, serverTransport] = @@ -3996,7 +4116,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result1.completion.values).toEqual(["Alice"]); @@ -4021,7 +4141,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result2.completion.values).toEqual(["David"]); @@ -4046,7 +4166,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result3.completion.values).toEqual(["Grace"]); @@ -4066,7 +4186,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema, + CompleteResultSchema ); expect(result4.completion.values).toEqual(["Guest"]); @@ -4074,7 +4194,6 @@ describe("Tool title precedence", () => { }); describe("elicitInput()", () => { - const checkAvailability = jest.fn().mockResolvedValue(false); const findAlternatives = jest.fn().mockResolvedValue([]); const makeBooking = jest.fn().mockResolvedValue("BOOKING-123"); @@ -4097,7 +4216,7 @@ describe("elicitInput()", () => { { restaurant: z.string(), date: z.string(), - partySize: z.number() + partySize: z.number(), }, async ({ restaurant, date, partySize }) => { // Check availability @@ -4113,49 +4232,63 @@ describe("elicitInput()", () => { checkAlternatives: { type: "boolean", title: "Check alternative dates", - description: "Would you like me to check other dates?" + description: "Would you like me to check other dates?", }, flexibleDates: { type: "string", title: "Date flexibility", description: "How flexible are your dates?", enum: ["next_day", "same_week", "next_week"], - enumNames: ["Next day", "Same week", "Next week"] - } + enumNames: ["Next day", "Same week", "Next week"], + }, }, - required: ["checkAlternatives"] - } + required: ["checkAlternatives"], + }, }); - if (result.action === "accept" && result.content?.checkAlternatives) { + if ( + result.action === "accept" && + typeof result.content === "object" && + result.content !== null && + "checkAlternatives" in result.content && + (result.content as Record).checkAlternatives === + true + ) { const alternatives = await findAlternatives( restaurant, date, partySize, - result.content.flexibleDates as string + (result.content as Record) + .flexibleDates as string ); return { - content: [{ - type: "text", - text: `Found these alternatives: ${alternatives.join(", ")}` - }] + content: [ + { + type: "text", + text: `Found these alternatives: ${alternatives.join(", ")}`, + }, + ], }; } return { - content: [{ - type: "text", - text: "No booking made. Original date not available." - }] + content: [ + { + type: "text", + text: "No booking made. Original date not available.", + }, + ], }; } await makeBooking(restaurant, date, partySize); return { - content: [{ - type: "text", - text: `Booked table for ${partySize} at ${restaurant} on ${date}` - }] + content: [ + { + type: "text", + text: `Booked table for ${partySize} at ${restaurant} on ${date}`, + }, + ], }; } ); @@ -4177,21 +4310,28 @@ describe("elicitInput()", () => { test("should successfully elicit additional information", async () => { // Mock availability check to return false checkAvailability.mockResolvedValue(false); - findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]); + findAlternatives.mockResolvedValue([ + "2024-12-26", + "2024-12-27", + "2024-12-28", + ]); // Set up client to accept alternative date checking client.setRequestHandler(ElicitRequestSchema, async (request) => { - expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25"); + expect(request.params.message).toContain( + "No tables available at ABC Restaurant on 2024-12-25" + ); return { action: "accept", content: { checkAlternatives: true, - flexibleDates: "same_week" - } + flexibleDates: "same_week", + }, }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -4204,16 +4344,27 @@ describe("elicitInput()", () => { arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", - partySize: 2 - } + partySize: 2, + }, }); - expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); - expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week"); - expect(result.content).toEqual([{ - type: "text", - text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28" - }]); + expect(checkAvailability).toHaveBeenCalledWith( + "ABC Restaurant", + "2024-12-25", + 2 + ); + expect(findAlternatives).toHaveBeenCalledWith( + "ABC Restaurant", + "2024-12-25", + 2, + "same_week" + ); + expect(result.content).toEqual([ + { + type: "text", + text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28", + }, + ]); }); test("should handle user declining to elicitation request", async () => { @@ -4225,12 +4376,13 @@ describe("elicitInput()", () => { return { action: "accept", content: { - checkAlternatives: false - } + checkAlternatives: false, + }, }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -4243,16 +4395,22 @@ describe("elicitInput()", () => { arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", - partySize: 2 - } + partySize: 2, + }, }); - expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); + expect(checkAvailability).toHaveBeenCalledWith( + "ABC Restaurant", + "2024-12-25", + 2 + ); expect(findAlternatives).not.toHaveBeenCalled(); - expect(result.content).toEqual([{ - type: "text", - text: "No booking made. Original date not available." - }]); + expect(result.content).toEqual([ + { + type: "text", + text: "No booking made. Original date not available.", + }, + ]); }); test("should handle user cancelling the elicitation", async () => { @@ -4262,11 +4420,12 @@ describe("elicitInput()", () => { // Set up client to cancel the elicitation client.setRequestHandler(ElicitRequestSchema, async () => { return { - action: "cancel" + action: "cancel", }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -4279,15 +4438,21 @@ describe("elicitInput()", () => { arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", - partySize: 2 - } + partySize: 2, + }, }); - expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); + expect(checkAvailability).toHaveBeenCalledWith( + "ABC Restaurant", + "2024-12-25", + 2 + ); expect(findAlternatives).not.toHaveBeenCalled(); - expect(result.content).toEqual([{ - type: "text", - text: "No booking made. Original date not available." - }]); + expect(result.content).toEqual([ + { + type: "text", + text: "No booking made. Original date not available.", + }, + ]); }); }); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 791facef1..919110bcb 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -5,12 +5,9 @@ import { ZodRawShape, ZodObject, ZodString, - AnyZodObject, - ZodTypeAny, ZodType, - ZodTypeDef, ZodOptional, -} from "zod"; +} from "zod/v4"; import { Implementation, Tool, @@ -42,7 +39,7 @@ import { ServerNotification, ToolAnnotations, } from "../types.js"; -import { Completable, CompletableDef } from "./completable.js"; +import { CompletableDef, McpZodTypeKind } from "./completable.js"; import { UriTemplate, Variables } from "../shared/uriTemplate.js"; import { RequestHandlerExtra } from "../shared/protocol.js"; import { Transport } from "../shared/transport.js"; @@ -93,48 +90,45 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - ListToolsRequestSchema.shape.method.value, + ListToolsRequestSchema.shape.method.value ); this.server.assertCanSetRequestHandler( - CallToolRequestSchema.shape.method.value, + CallToolRequestSchema.shape.method.value ); this.server.registerCapabilities({ tools: { - listChanged: true - } - }) + listChanged: true, + }, + }); this.server.setRequestHandler( ListToolsRequestSchema, (): ListToolsResult => ({ - tools: Object.entries(this._registeredTools).filter( - ([, tool]) => tool.enabled, - ).map( - ([name, tool]): Tool => { + tools: Object.entries(this._registeredTools) + .filter(([, tool]) => tool.enabled) + .map(([name, tool]): Tool => { const toolDefinition: Tool = { name, title: tool.title, description: tool.description, inputSchema: tool.inputSchema ? (zodToJsonSchema(tool.inputSchema, { - strictUnions: true, - }) as Tool["inputSchema"]) + strictUnions: true, + }) as Tool["inputSchema"]) : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, }; if (tool.outputSchema) { - toolDefinition.outputSchema = zodToJsonSchema( - tool.outputSchema, - { strictUnions: true } - ) as Tool["outputSchema"]; + toolDefinition.outputSchema = zodToJsonSchema(tool.outputSchema, { + strictUnions: true, + }) as Tool["outputSchema"]; } return toolDefinition; - }, - ), - }), + }), + }) ); this.server.setRequestHandler( @@ -144,14 +138,14 @@ export class McpServer { if (!tool) { throw new McpError( ErrorCode.InvalidParams, - `Tool ${request.params.name} not found`, + `Tool ${request.params.name} not found` ); } if (!tool.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Tool ${request.params.name} disabled`, + `Tool ${request.params.name} disabled` ); } @@ -159,12 +153,12 @@ export class McpServer { if (tool.inputSchema) { const parseResult = await tool.inputSchema.safeParseAsync( - request.params.arguments, + request.params.arguments ); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, - `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`, + `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}` ); } @@ -204,24 +198,24 @@ export class McpServer { if (!result.structuredContent) { throw new McpError( ErrorCode.InvalidParams, - `Tool ${request.params.name} has an output schema but no structured content was provided`, + `Tool ${request.params.name} has an output schema but no structured content was provided` ); } // if the tool has an output schema, validate structured content const parseResult = await tool.outputSchema.safeParseAsync( - result.structuredContent, + result.structuredContent ); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, - `Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}`, + `Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}` ); } } return result; - }, + } ); this._toolHandlersInitialized = true; @@ -235,7 +229,7 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - CompleteRequestSchema.shape.method.value, + CompleteRequestSchema.shape.method.value ); this.server.registerCapabilities({ @@ -255,10 +249,10 @@ export class McpServer { default: throw new McpError( ErrorCode.InvalidParams, - `Invalid completion reference: ${request.params.ref}`, + `Invalid completion reference: ${request.params.ref}` ); } - }, + } ); this._completionHandlerInitialized = true; @@ -266,20 +260,20 @@ export class McpServer { private async handlePromptCompletion( request: CompleteRequest, - ref: PromptReference, + ref: PromptReference ): Promise { const prompt = this._registeredPrompts[ref.name]; if (!prompt) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${ref.name} not found`, + `Prompt ${ref.name} not found` ); } if (!prompt.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${ref.name} disabled`, + `Prompt ${ref.name} disabled` ); } @@ -288,21 +282,31 @@ export class McpServer { } const field = prompt.argsSchema.shape[request.params.argument.name]; - if (!(field instanceof Completable)) { + const defLike = (field as unknown as { _def?: { typeName?: unknown } }) + ._def; + if (!defLike || defLike.typeName !== McpZodTypeKind.Completable) { return EMPTY_COMPLETION_RESULT; } - const def: CompletableDef = field._def; - const suggestions = await def.complete(request.params.argument.value, request.params.context); + const def = (field as unknown as { _def: CompletableDef })._def; + const ctx = request.params.context; + const ctxForComplete = + ctx && typeof ctx.arguments !== "string" + ? { arguments: ctx.arguments } + : undefined; + const suggestions = await def.complete( + request.params.argument.value, + ctxForComplete + ); return createCompletionResult(suggestions); } private async handleResourceCompletion( request: CompleteRequest, - ref: ResourceTemplateReference, + ref: ResourceTemplateReference ): Promise { const template = Object.values(this._registeredResourceTemplates).find( - (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri, + (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri ); if (!template) { @@ -313,18 +317,26 @@ export class McpServer { throw new McpError( ErrorCode.InvalidParams, - `Resource template ${request.params.ref.uri} not found`, + `Resource template ${request.params.ref.uri} not found` ); } const completer = template.resourceTemplate.completeCallback( - request.params.argument.name, + request.params.argument.name ); if (!completer) { return EMPTY_COMPLETION_RESULT; } - const suggestions = await completer(request.params.argument.value, request.params.context); + const ctx = request.params.context; + const ctxForComplete = + ctx && typeof ctx.arguments !== "string" + ? { arguments: ctx.arguments } + : undefined; + const suggestions = await completer( + request.params.argument.value, + ctxForComplete + ); return createCompletionResult(suggestions); } @@ -336,37 +348,35 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - ListResourcesRequestSchema.shape.method.value, + ListResourcesRequestSchema.shape.method.value ); this.server.assertCanSetRequestHandler( - ListResourceTemplatesRequestSchema.shape.method.value, + ListResourceTemplatesRequestSchema.shape.method.value ); this.server.assertCanSetRequestHandler( - ReadResourceRequestSchema.shape.method.value, + ReadResourceRequestSchema.shape.method.value ); this.server.registerCapabilities({ resources: { - listChanged: true - } - }) + listChanged: true, + }, + }); this.server.setRequestHandler( ListResourcesRequestSchema, async (request, extra) => { - const resources = Object.entries(this._registeredResources).filter( - ([_, resource]) => resource.enabled, - ).map( - ([uri, resource]) => ({ + const resources = Object.entries(this._registeredResources) + .filter(([_, resource]) => resource.enabled) + .map(([uri, resource]) => ({ uri, name: resource.name, ...resource.metadata, - }), - ); + })); const templateResources: Resource[] = []; for (const template of Object.values( - this._registeredResourceTemplates, + this._registeredResourceTemplates )) { if (!template.resourceTemplate.listCallback) { continue; @@ -383,14 +393,14 @@ export class McpServer { } return { resources: [...resources, ...templateResources] }; - }, + } ); this.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => { const resourceTemplates = Object.entries( - this._registeredResourceTemplates, + this._registeredResourceTemplates ).map(([name, template]) => ({ name, uriTemplate: template.resourceTemplate.uriTemplate.toString(), @@ -398,7 +408,7 @@ export class McpServer { })); return { resourceTemplates }; - }, + } ); this.server.setRequestHandler( @@ -412,7 +422,7 @@ export class McpServer { if (!resource.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Resource ${uri} disabled`, + `Resource ${uri} disabled` ); } return resource.readCallback(uri, extra); @@ -420,10 +430,10 @@ export class McpServer { // Then check templates for (const template of Object.values( - this._registeredResourceTemplates, + this._registeredResourceTemplates )) { const variables = template.resourceTemplate.uriTemplate.match( - uri.toString(), + uri.toString() ); if (variables) { return template.readCallback(uri, variables, extra); @@ -432,9 +442,9 @@ export class McpServer { throw new McpError( ErrorCode.InvalidParams, - `Resource ${uri} not found`, + `Resource ${uri} not found` ); - }, + } ); this.setCompletionRequestHandler(); @@ -450,25 +460,24 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - ListPromptsRequestSchema.shape.method.value, + ListPromptsRequestSchema.shape.method.value ); this.server.assertCanSetRequestHandler( - GetPromptRequestSchema.shape.method.value, + GetPromptRequestSchema.shape.method.value ); this.server.registerCapabilities({ prompts: { - listChanged: true - } - }) + listChanged: true, + }, + }); this.server.setRequestHandler( ListPromptsRequestSchema, (): ListPromptsResult => ({ - prompts: Object.entries(this._registeredPrompts).filter( - ([, prompt]) => prompt.enabled, - ).map( - ([name, prompt]): Prompt => { + prompts: Object.entries(this._registeredPrompts) + .filter(([, prompt]) => prompt.enabled) + .map(([name, prompt]): Prompt => { return { name, title: prompt.title, @@ -477,9 +486,8 @@ export class McpServer { ? promptArgumentsFromSchema(prompt.argsSchema) : undefined, }; - }, - ), - }), + }), + }) ); this.server.setRequestHandler( @@ -489,25 +497,25 @@ export class McpServer { if (!prompt) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${request.params.name} not found`, + `Prompt ${request.params.name} not found` ); } if (!prompt.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${request.params.name} disabled`, + `Prompt ${request.params.name} disabled` ); } if (prompt.argsSchema) { const parseResult = await prompt.argsSchema.safeParseAsync( - request.params.arguments, + request.params.arguments ); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, - `Invalid arguments for prompt ${request.params.name}: ${parseResult.error.message}`, + `Invalid arguments for prompt ${request.params.name}: ${parseResult.error.message}` ); } @@ -518,7 +526,7 @@ export class McpServer { const cb = prompt.callback as PromptCallback; return await Promise.resolve(cb(extra)); } - }, + } ); this.setCompletionRequestHandler(); @@ -529,7 +537,11 @@ export class McpServer { /** * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. */ - resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; + resource( + name: string, + uri: string, + readCallback: ReadResourceCallback + ): RegisteredResource; /** * Registers a resource `name` at a fixed URI with metadata, which will use the given callback to respond to read requests. @@ -538,7 +550,7 @@ export class McpServer { name: string, uri: string, metadata: ResourceMetadata, - readCallback: ReadResourceCallback, + readCallback: ReadResourceCallback ): RegisteredResource; /** @@ -547,7 +559,7 @@ export class McpServer { resource( name: string, template: ResourceTemplate, - readCallback: ReadResourceTemplateCallback, + readCallback: ReadResourceTemplateCallback ): RegisteredResourceTemplate; /** @@ -557,7 +569,7 @@ export class McpServer { name: string, template: ResourceTemplate, metadata: ResourceMetadata, - readCallback: ReadResourceTemplateCallback, + readCallback: ReadResourceTemplateCallback ): RegisteredResourceTemplate; resource( @@ -684,15 +696,21 @@ export class McpServer { remove: () => registeredResource.update({ uri: null }), update: (updates) => { if (typeof updates.uri !== "undefined" && updates.uri !== uri) { - delete this._registeredResources[uri] - if (updates.uri) this._registeredResources[updates.uri] = registeredResource + delete this._registeredResources[uri]; + if (updates.uri) + this._registeredResources[updates.uri] = registeredResource; } - if (typeof updates.name !== "undefined") registeredResource.name = updates.name - if (typeof updates.title !== "undefined") registeredResource.title = updates.title - if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled - this.sendResourceListChanged() + if (typeof updates.name !== "undefined") + registeredResource.name = updates.name; + if (typeof updates.title !== "undefined") + registeredResource.title = updates.title; + if (typeof updates.metadata !== "undefined") + registeredResource.metadata = updates.metadata; + if (typeof updates.callback !== "undefined") + registeredResource.readCallback = updates.callback; + if (typeof updates.enabled !== "undefined") + registeredResource.enabled = updates.enabled; + this.sendResourceListChanged(); }, }; this._registeredResources[uri] = registeredResource; @@ -717,15 +735,22 @@ export class McpServer { remove: () => registeredResourceTemplate.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredResourceTemplates[name] - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate + delete this._registeredResourceTemplates[name]; + if (updates.name) + this._registeredResourceTemplates[updates.name] = + registeredResourceTemplate; } - if (typeof updates.title !== "undefined") registeredResourceTemplate.title = updates.title - if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template - if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata - if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback - if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled - this.sendResourceListChanged() + if (typeof updates.title !== "undefined") + registeredResourceTemplate.title = updates.title; + if (typeof updates.template !== "undefined") + registeredResourceTemplate.resourceTemplate = updates.template; + if (typeof updates.metadata !== "undefined") + registeredResourceTemplate.metadata = updates.metadata; + if (typeof updates.callback !== "undefined") + registeredResourceTemplate.readCallback = updates.callback; + if (typeof updates.enabled !== "undefined") + registeredResourceTemplate.enabled = updates.enabled; + this.sendResourceListChanged(); }, }; this._registeredResourceTemplates[name] = registeredResourceTemplate; @@ -750,15 +775,21 @@ export class McpServer { remove: () => registeredPrompt.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredPrompts[name] - if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt + delete this._registeredPrompts[name]; + if (updates.name) + this._registeredPrompts[updates.name] = registeredPrompt; } - if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title - if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description - if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = z.object(updates.argsSchema) - if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback - if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled - this.sendPromptListChanged() + if (typeof updates.title !== "undefined") + registeredPrompt.title = updates.title; + if (typeof updates.description !== "undefined") + registeredPrompt.description = updates.description; + if (typeof updates.argsSchema !== "undefined") + registeredPrompt.argsSchema = z.object(updates.argsSchema); + if (typeof updates.callback !== "undefined") + registeredPrompt.callback = updates.callback; + if (typeof updates.enabled !== "undefined") + registeredPrompt.enabled = updates.enabled; + this.sendPromptListChanged(); }, }; this._registeredPrompts[name] = registeredPrompt; @@ -789,24 +820,31 @@ export class McpServer { remove: () => registeredTool.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { - delete this._registeredTools[name] - if (updates.name) this._registeredTools[updates.name] = registeredTool + delete this._registeredTools[name]; + if (updates.name) + this._registeredTools[updates.name] = registeredTool; } - if (typeof updates.title !== "undefined") registeredTool.title = updates.title - if (typeof updates.description !== "undefined") registeredTool.description = updates.description - if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = z.object(updates.paramsSchema) - if (typeof updates.callback !== "undefined") registeredTool.callback = updates.callback - if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations - if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled - this.sendToolListChanged() + if (typeof updates.title !== "undefined") + registeredTool.title = updates.title; + if (typeof updates.description !== "undefined") + registeredTool.description = updates.description; + if (typeof updates.paramsSchema !== "undefined") + registeredTool.inputSchema = z.object(updates.paramsSchema); + if (typeof updates.callback !== "undefined") + registeredTool.callback = updates.callback; + if (typeof updates.annotations !== "undefined") + registeredTool.annotations = updates.annotations; + if (typeof updates.enabled !== "undefined") + registeredTool.enabled = updates.enabled; + this.sendToolListChanged(); }, }; this._registeredTools[name] = registeredTool; this.setToolRequestHandlers(); - this.sendToolListChanged() + this.sendToolListChanged(); - return registeredTool + return registeredTool; } /** @@ -822,21 +860,21 @@ export class McpServer { /** * Registers a tool taking either a parameter schema for validation or annotations for additional metadata. * This unified overload handles both `tool(name, paramsSchema, cb)` and `tool(name, annotations, cb)` cases. - * + * * Note: We use a union type for the second parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ tool( name: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** * Registers a tool `name` (with a description) taking either parameter schema or annotations. - * This unified overload handles both `tool(name, description, paramsSchema, cb)` and + * This unified overload handles both `tool(name, description, paramsSchema, cb)` and * `tool(name, description, annotations, cb)` cases. - * + * * Note: We use a union type for the third parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ @@ -844,7 +882,7 @@ export class McpServer { name: string, description: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** @@ -854,7 +892,7 @@ export class McpServer { name: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** @@ -865,10 +903,9 @@ export class McpServer { description: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; - /** * tool() implementation. Parses arguments passed to overrides defined above. */ @@ -900,7 +937,12 @@ export class McpServer { inputSchema = rest.shift() as ZodRawShape; // Check if the next arg is potentially annotations - if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) { + if ( + rest.length > 1 && + typeof rest[0] === "object" && + rest[0] !== null && + !isZodRawShape(rest[0]) + ) { // Case: tool(name, paramsSchema, annotations, cb) // Or: tool(name, description, paramsSchema, annotations, cb) annotations = rest.shift() as ToolAnnotations; @@ -914,7 +956,15 @@ export class McpServer { } const callback = rest[0] as ToolCallback; - return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback) + return this._createRegisteredTool( + name, + undefined, + description, + inputSchema, + outputSchema, + annotations, + callback + ); } /** @@ -935,7 +985,8 @@ export class McpServer { throw new Error(`Tool ${name} is already registered`); } - const { title, description, inputSchema, outputSchema, annotations } = config; + const { title, description, inputSchema, outputSchema, annotations } = + config; return this._createRegisteredTool( name, @@ -956,7 +1007,11 @@ export class McpServer { /** * Registers a zero-argument prompt `name` (with a description) which will run the given function when the client calls it. */ - prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; + prompt( + name: string, + description: string, + cb: PromptCallback + ): RegisteredPrompt; /** * Registers a prompt `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. @@ -964,7 +1019,7 @@ export class McpServer { prompt( name: string, argsSchema: Args, - cb: PromptCallback, + cb: PromptCallback ): RegisteredPrompt; /** @@ -974,7 +1029,7 @@ export class McpServer { name: string, description: string, argsSchema: Args, - cb: PromptCallback, + cb: PromptCallback ): RegisteredPrompt; prompt(name: string, ...rest: unknown[]): RegisteredPrompt { @@ -1002,9 +1057,9 @@ export class McpServer { ); this.setPromptRequestHandlers(); - this.sendPromptListChanged() + this.sendPromptListChanged(); - return registeredPrompt + return registeredPrompt; } /** @@ -1034,7 +1089,7 @@ export class McpServer { ); this.setPromptRequestHandlers(); - this.sendPromptListChanged() + this.sendPromptListChanged(); return registeredPrompt; } @@ -1044,7 +1099,7 @@ export class McpServer { * @returns True if the server is connected */ isConnected() { - return this.server.transport !== undefined + return this.server.transport !== undefined; } /** @@ -1082,7 +1137,7 @@ export type CompleteResourceTemplateCallback = ( value: string, context?: { arguments?: Record; - }, + } ) => string[] | Promise; /** @@ -1106,7 +1161,7 @@ export class ResourceTemplate { complete?: { [variable: string]: CompleteResourceTemplateCallback; }; - }, + } ) { this._uriTemplate = typeof uriTemplate === "string" @@ -1132,7 +1187,7 @@ export class ResourceTemplate { * Gets the callback for completing a specific URI template variable, if one was provided. */ completeCallback( - variable: string, + variable: string ): CompleteResourceTemplateCallback | undefined { return this._callbacks.complete?.[variable]; } @@ -1150,34 +1205,38 @@ export class ResourceTemplate { */ export type ToolCallback = Args extends ZodRawShape - ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; + ? ( + args: z.infer>, + extra: RequestHandlerExtra + ) => CallToolResult | Promise + : ( + extra: RequestHandlerExtra + ) => CallToolResult | Promise; export type RegisteredTool = { title?: string; description?: string; - inputSchema?: AnyZodObject; - outputSchema?: AnyZodObject; + inputSchema?: ZodObject; + outputSchema?: ZodObject; annotations?: ToolAnnotations; callback: ToolCallback; enabled: boolean; enable(): void; disable(): void; - update( - updates: { - name?: string | null, - title?: string, - description?: string, - paramsSchema?: InputArgs, - outputSchema?: OutputArgs, - annotations?: ToolAnnotations, - callback?: ToolCallback, - enabled?: boolean - }): void - remove(): void + update< + InputArgs extends ZodRawShape, + OutputArgs extends ZodRawShape + >(updates: { + name?: string | null; + title?: string; + description?: string; + paramsSchema?: InputArgs; + outputSchema?: OutputArgs; + annotations?: ToolAnnotations; + callback?: ToolCallback; + enabled?: boolean; + }): void; + remove(): void; }; const EMPTY_OBJECT_JSON_SCHEMA = { @@ -1197,10 +1256,14 @@ function isZodRawShape(obj: unknown): obj is ZodRawShape { } function isZodTypeLike(value: unknown): value is ZodType { - return value !== null && - typeof value === 'object' && - 'parse' in value && typeof value.parse === 'function' && - 'safeParse' in value && typeof value.safeParse === 'function'; + return ( + value !== null && + typeof value === "object" && + "parse" in value && + typeof value.parse === "function" && + "safeParse" in value && + typeof value.safeParse === "function" + ); } /** @@ -1212,7 +1275,7 @@ export type ResourceMetadata = Omit; * Callback to list all resources matching a given template. */ export type ListResourcesCallback = ( - extra: RequestHandlerExtra, + extra: RequestHandlerExtra ) => ListResourcesResult | Promise; /** @@ -1220,7 +1283,7 @@ export type ListResourcesCallback = ( */ export type ReadResourceCallback = ( uri: URL, - extra: RequestHandlerExtra, + extra: RequestHandlerExtra ) => ReadResourceResult | Promise; export type RegisteredResource = { @@ -1231,8 +1294,15 @@ export type RegisteredResource = { enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string, title?: string, uri?: string | null, metadata?: ResourceMetadata, callback?: ReadResourceCallback, enabled?: boolean }): void - remove(): void + update(updates: { + name?: string; + title?: string; + uri?: string | null; + metadata?: ResourceMetadata; + callback?: ReadResourceCallback; + enabled?: boolean; + }): void; + remove(): void; }; /** @@ -1241,7 +1311,7 @@ export type RegisteredResource = { export type ReadResourceTemplateCallback = ( uri: URL, variables: Variables, - extra: RequestHandlerExtra, + extra: RequestHandlerExtra ) => ReadResourceResult | Promise; export type RegisteredResourceTemplate = { @@ -1252,24 +1322,29 @@ export type RegisteredResourceTemplate = { enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, title?: string, template?: ResourceTemplate, metadata?: ResourceMetadata, callback?: ReadResourceTemplateCallback, enabled?: boolean }): void - remove(): void + update(updates: { + name?: string | null; + title?: string; + template?: ResourceTemplate; + metadata?: ResourceMetadata; + callback?: ReadResourceTemplateCallback; + enabled?: boolean; + }): void; + remove(): void; }; -type PromptArgsRawShape = { - [k: string]: - | ZodType - | ZodOptional>; -}; +type PromptArgsRawShape = Record>; export type PromptCallback< - Args extends undefined | PromptArgsRawShape = undefined, + Args extends undefined | PromptArgsRawShape = undefined > = Args extends PromptArgsRawShape ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => GetPromptResult | Promise - : (extra: RequestHandlerExtra) => GetPromptResult | Promise; + args: z.infer>, + extra: RequestHandlerExtra + ) => GetPromptResult | Promise + : ( + extra: RequestHandlerExtra + ) => GetPromptResult | Promise; export type RegisteredPrompt = { title?: string; @@ -1279,19 +1354,26 @@ export type RegisteredPrompt = { enabled: boolean; enable(): void; disable(): void; - update(updates: { name?: string | null, title?: string, description?: string, argsSchema?: Args, callback?: PromptCallback, enabled?: boolean }): void - remove(): void + update(updates: { + name?: string | null; + title?: string; + description?: string; + argsSchema?: Args; + callback?: PromptCallback; + enabled?: boolean; + }): void; + remove(): void; }; function promptArgumentsFromSchema( - schema: ZodObject, + schema: ZodObject ): PromptArgument[] { return Object.entries(schema.shape).map( ([name, field]): PromptArgument => ({ name, description: field.description, required: !field.isOptional(), - }), + }) ); } diff --git a/src/server/sse.test.ts b/src/server/sse.test.ts index a7f180961..aa10a534a 100644 --- a/src/server/sse.test.ts +++ b/src/server/sse.test.ts @@ -4,7 +4,7 @@ import { SSEServerTransport } from './sse.js'; import { McpServer } from './mcp.js'; import { createServer, type Server } from "node:http"; import { AddressInfo } from "node:net"; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { CallToolResult, JSONRPCMessage } from 'src/types.js'; const createMockResponse = () => { diff --git a/src/server/streamableHttp.test.ts b/src/server/streamableHttp.test.ts index 3a0a5c066..9c5ccc3cf 100644 --- a/src/server/streamableHttp.test.ts +++ b/src/server/streamableHttp.test.ts @@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto"; import { EventStore, StreamableHTTPServerTransport, EventId, StreamId } from "./streamableHttp.js"; import { McpServer } from "./mcp.js"; import { CallToolResult, JSONRPCMessage } from "../types.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { AuthInfo } from "./auth/types.js"; async function getFreePort() { diff --git a/src/server/title.test.ts b/src/server/title.test.ts index 3f64570b8..f9d9e46a7 100644 --- a/src/server/title.test.ts +++ b/src/server/title.test.ts @@ -1,7 +1,7 @@ import { Server } from "./index.js"; import { Client } from "../client/index.js"; import { InMemoryTransport } from "../inMemory.js"; -import { z } from "zod"; +import { z } from "zod/v4"; import { McpServer, ResourceTemplate } from "./mcp.js"; describe("Title field backwards compatibility", () => { diff --git a/src/shared/auth.ts b/src/shared/auth.ts index 47eba9ac5..d4d006808 100644 --- a/src/shared/auth.ts +++ b/src/shared/auth.ts @@ -1,32 +1,29 @@ -import { z } from "zod"; +import { z } from "zod/v4"; /** * RFC 9728 OAuth Protected Resource Metadata */ -export const OAuthProtectedResourceMetadataSchema = z - .object({ - resource: z.string().url(), - authorization_servers: z.array(z.string().url()).optional(), - jwks_uri: z.string().url().optional(), +export const OAuthProtectedResourceMetadataSchema = z.looseObject({ + resource: z.url(), + authorization_servers: z.array(z.url()).optional(), + jwks_uri: z.url().optional(), scopes_supported: z.array(z.string()).optional(), bearer_methods_supported: z.array(z.string()).optional(), resource_signing_alg_values_supported: z.array(z.string()).optional(), resource_name: z.string().optional(), resource_documentation: z.string().optional(), - resource_policy_uri: z.string().url().optional(), - resource_tos_uri: z.string().url().optional(), + resource_policy_uri: z.url().optional(), + resource_tos_uri: z.url().optional(), tls_client_certificate_bound_access_tokens: z.boolean().optional(), authorization_details_types_supported: z.array(z.string()).optional(), dpop_signing_alg_values_supported: z.array(z.string()).optional(), dpop_bound_access_tokens_required: z.boolean().optional(), - }) - .passthrough(); + }); /** * RFC 8414 OAuth 2.0 Authorization Server Metadata */ -export const OAuthMetadataSchema = z - .object({ +export const OAuthMetadataSchema = z.looseObject({ issuer: z.string(), authorization_endpoint: z.string(), token_endpoint: z.string(), @@ -53,15 +50,13 @@ export const OAuthMetadataSchema = z .array(z.string()) .optional(), code_challenge_methods_supported: z.array(z.string()).optional(), - }) - .passthrough(); + }); /** * OpenID Connect Discovery 1.0 Provider Metadata * see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata */ -export const OpenIdProviderMetadataSchema = z - .object({ +export const OpenIdProviderMetadataSchema = z.looseObject({ issuer: z.string(), authorization_endpoint: z.string(), token_endpoint: z.string(), @@ -103,8 +98,7 @@ export const OpenIdProviderMetadataSchema = z require_request_uri_registration: z.boolean().optional(), op_policy_uri: z.string().optional(), op_tos_uri: z.string().optional(), - }) - .passthrough(); + }); /** * OpenID Connect Discovery metadata that may include OAuth 2.0 fields @@ -121,16 +115,14 @@ export const OpenIdProviderDiscoveryMetadataSchema = /** * OAuth 2.1 token response */ -export const OAuthTokensSchema = z - .object({ +export const OAuthTokensSchema = z.object({ access_token: z.string(), id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect token_type: z.string(), expires_in: z.number().optional(), scope: z.string().optional(), refresh_token: z.string().optional(), - }) - .strip(); + }); /** * OAuth 2.1 error response @@ -146,7 +138,9 @@ export const OAuthErrorResponseSchema = z * RFC 7591 OAuth 2.0 Dynamic Client Registration metadata */ export const OAuthClientMetadataSchema = z.object({ - redirect_uris: z.array(z.string()).refine((uris) => uris.every((uri) => URL.canParse(uri)), { message: "redirect_uris must contain valid URLs" }), + redirect_uris: z.array(z.string()).refine((uris) => uris.every((uri) => URL.canParse(uri)), { + error: "redirect_uris must contain valid URLs" +}), token_endpoint_auth_method: z.string().optional(), grant_types: z.array(z.string()).optional(), response_types: z.array(z.string()).optional(), @@ -162,7 +156,7 @@ export const OAuthClientMetadataSchema = z.object({ software_id: z.string().optional(), software_version: z.string().optional(), software_statement: z.string().optional(), -}).strip(); +}); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration client information @@ -172,7 +166,7 @@ export const OAuthClientInformationSchema = z.object({ client_secret: z.string().optional(), client_id_issued_at: z.number().optional(), client_secret_expires_at: z.number().optional(), -}).strip(); +}); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata) @@ -185,7 +179,7 @@ export const OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge( export const OAuthClientRegistrationErrorSchema = z.object({ error: z.string(), error_description: z.string().optional(), -}).strip(); +}); /** * RFC 7009 OAuth 2.0 Token Revocation request @@ -193,7 +187,7 @@ export const OAuthClientRegistrationErrorSchema = z.object({ export const OAuthTokenRevocationRequestSchema = z.object({ token: z.string(), token_type_hint: z.string().optional(), -}).strip(); +}); export type OAuthMetadata = z.infer; export type OpenIdProviderMetadata = z.infer; diff --git a/src/shared/protocol-transport-handling.test.ts b/src/shared/protocol-transport-handling.test.ts index 3baa9b638..4dd46851b 100644 --- a/src/shared/protocol-transport-handling.test.ts +++ b/src/shared/protocol-transport-handling.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, beforeEach } from "@jest/globals"; import { Protocol } from "./protocol.js"; import { Transport } from "./transport.js"; import { Request, Notification, Result, JSONRPCMessage } from "../types.js"; -import { z } from "zod"; +import { z } from "zod/v4"; // Mock Transport class class MockTransport implements Transport { diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index f4e74c8bb..e57983a6e 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -1,4 +1,4 @@ -import { ZodType, z } from "zod"; +import { ZodType, z } from "zod/v4"; import { ClientCapabilities, ErrorCode, diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 7df190ba1..cf1f61600 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -1,4 +1,4 @@ -import { ZodLiteral, ZodObject, ZodType, z } from "zod"; +import { ZodLiteral, ZodObject, ZodType, z } from "zod/v4"; import { CancelledNotificationSchema, ClientCapabilities, diff --git a/src/types.ts b/src/types.ts index 323e37389..8144d9fcd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { z, ZodTypeAny } from "zod"; +import { z, ZodTypeAny } from "zod/v4"; import { AuthInfo } from "./server/auth/types.js"; export const LATEST_PROTOCOL_VERSION = "2025-06-18"; @@ -16,73 +16,64 @@ export const JSONRPC_VERSION = "2.0"; /** * A progress token, used to associate progress notifications with the original request. */ -export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); +export const ProgressTokenSchema = z.union([z.string(), z.int()]); /** * An opaque token used to represent a cursor for pagination. */ export const CursorSchema = z.string(); -const RequestMetaSchema = z - .object({ - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken: z.optional(ProgressTokenSchema), - }) - .passthrough(); +const RequestMetaSchema = z.looseObject({ + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken: z.optional(ProgressTokenSchema), +}); -const BaseRequestParamsSchema = z - .object({ - _meta: z.optional(RequestMetaSchema), - }) - .passthrough(); +const BaseRequestParamsSchema = z.looseObject({ + _meta: z.optional(RequestMetaSchema), +}); export const RequestSchema = z.object({ method: z.string(), params: z.optional(BaseRequestParamsSchema), }); -const BaseNotificationParamsSchema = z - .object({ - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +const BaseNotificationParamsSchema = z.looseObject({ + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); export const NotificationSchema = z.object({ method: z.string(), params: z.optional(BaseNotificationParamsSchema), }); -export const ResultSchema = z - .object({ - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +export const ResultSchema = z.looseObject({ + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * A uniquely identifying ID for a request in JSON-RPC. */ -export const RequestIdSchema = z.union([z.string(), z.number().int()]); +export const RequestIdSchema = z.union([z.string(), z.int()]); /** * A request that expects a response. */ export const JSONRPCRequestSchema = z - .object({ + .strictObject({ jsonrpc: z.literal(JSONRPC_VERSION), id: RequestIdSchema, }) - .merge(RequestSchema) - .strict(); + .extend(RequestSchema.shape); export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => JSONRPCRequestSchema.safeParse(value).success; @@ -91,11 +82,10 @@ export const isJSONRPCRequest = (value: unknown): value is JSONRPCRequest => * A notification which does not expect a response. */ export const JSONRPCNotificationSchema = z - .object({ + .strictObject({ jsonrpc: z.literal(JSONRPC_VERSION), }) - .merge(NotificationSchema) - .strict(); + .extend(NotificationSchema.shape); export const isJSONRPCNotification = ( value: unknown @@ -105,13 +95,11 @@ export const isJSONRPCNotification = ( /** * A successful (non-error) response to a request. */ -export const JSONRPCResponseSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema, - result: ResultSchema, - }) - .strict(); +export const JSONRPCResponseSchema = z.strictObject({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema, + result: ResultSchema, +}); export const isJSONRPCResponse = (value: unknown): value is JSONRPCResponse => JSONRPCResponseSchema.safeParse(value).success; @@ -135,26 +123,24 @@ export enum ErrorCode { /** * A response to a request that indicates an error occurred. */ -export const JSONRPCErrorSchema = z - .object({ - jsonrpc: z.literal(JSONRPC_VERSION), - id: RequestIdSchema, - error: z.object({ - /** - * The error type that occurred. - */ - code: z.number().int(), - /** - * A short description of the error. The message SHOULD be limited to a concise single sentence. - */ - message: z.string(), - /** - * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - */ - data: z.optional(z.unknown()), - }), - }) - .strict(); +export const JSONRPCErrorSchema = z.strictObject({ + jsonrpc: z.literal(JSONRPC_VERSION), + id: RequestIdSchema, + error: z.strictObject({ + /** + * The error type that occurred. + */ + code: z.int(), + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: z.string(), + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data: z.optional(z.unknown()), + }), +}); export const isJSONRPCError = (value: unknown): value is JSONRPCError => JSONRPCErrorSchema.safeParse(value).success; @@ -203,21 +189,19 @@ export const CancelledNotificationSchema = NotificationSchema.extend({ /** * Base metadata interface for common properties across resources, tools, prompts, and implementations. */ -export const BaseMetadataSchema = z - .object({ - /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */ - name: z.string(), - /** - * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, - * even by those unfamiliar with domain-specific terminology. - * - * If not provided, the name should be used for display (except for Tool, - * where `annotations.title` should be given precedence over using `name`, - * if present). - */ - title: z.optional(z.string()), - }) - .passthrough(); +export const BaseMetadataSchema = z.looseObject({ + /** Intended for programmatic or logical use, but used as a display name in past specs or fallback */ + name: z.string(), + /** + * Intended for UI and end-user contexts — optimized to be human-readable and easily understood, + * even by those unfamiliar with domain-specific terminology. + * + * If not provided, the name should be used for display (except for Tool, + * where `annotations.title` should be given precedence over using `name`, + * if present). + */ + title: z.optional(z.string()), +}); /* Initialization */ /** @@ -230,35 +214,31 @@ export const ImplementationSchema = BaseMetadataSchema.extend({ /** * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. */ -export const ClientCapabilitiesSchema = z - .object({ - /** - * Experimental, non-standard capabilities that the client supports. - */ - experimental: z.optional(z.object({}).passthrough()), - /** - * Present if the client supports sampling from an LLM. - */ - sampling: z.optional(z.object({}).passthrough()), - /** - * Present if the client supports eliciting user input. - */ - elicitation: z.optional(z.object({}).passthrough()), - /** - * Present if the client supports listing roots. - */ - roots: z.optional( - z - .object({ - /** - * Whether the client supports issuing notifications for changes to the roots list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - }) - .passthrough(); +export const ClientCapabilitiesSchema = z.looseObject({ + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental: z.optional(z.looseObject({})), + /** + * Present if the client supports sampling from an LLM. + */ + sampling: z.optional(z.looseObject({})), + /** + * Present if the client supports eliciting user input. + */ + elicitation: z.optional(z.looseObject({})), + /** + * Present if the client supports listing roots. + */ + roots: z.optional( + z.looseObject({ + /** + * Whether the client supports issuing notifications for changes to the roots list. + */ + listChanged: z.optional(z.boolean()), + }) + ), +}); /** * This request is sent from the client to the server when it first connects, asking it to begin initialization. @@ -275,73 +255,66 @@ export const InitializeRequestSchema = RequestSchema.extend({ }), }); -export const isInitializeRequest = (value: unknown): value is InitializeRequest => +export const isInitializeRequest = ( + value: unknown +): value is InitializeRequest => InitializeRequestSchema.safeParse(value).success; - /** * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. */ -export const ServerCapabilitiesSchema = z - .object({ - /** - * Experimental, non-standard capabilities that the server supports. - */ - experimental: z.optional(z.object({}).passthrough()), - /** - * Present if the server supports sending log messages to the client. - */ - logging: z.optional(z.object({}).passthrough()), - /** - * Present if the server supports sending completions to the client. - */ - completions: z.optional(z.object({}).passthrough()), - /** - * Present if the server offers any prompt templates. - */ - prompts: z.optional( - z - .object({ - /** - * Whether this server supports issuing notifications for changes to the prompt list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - /** - * Present if the server offers any resources to read. - */ - resources: z.optional( - z - .object({ - /** - * Whether this server supports clients subscribing to resource updates. - */ - subscribe: z.optional(z.boolean()), - - /** - * Whether this server supports issuing notifications for changes to the resource list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - /** - * Present if the server offers any tools to call. - */ - tools: z.optional( - z - .object({ - /** - * Whether this server supports issuing notifications for changes to the tool list. - */ - listChanged: z.optional(z.boolean()), - }) - .passthrough(), - ), - }) - .passthrough(); +export const ServerCapabilitiesSchema = z.looseObject({ + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental: z.optional(z.looseObject({})), + /** + * Present if the server supports sending log messages to the client. + */ + logging: z.optional(z.looseObject({})), + /** + * Present if the server supports sending completions to the client. + */ + completions: z.optional(z.looseObject({})), + /** + * Present if the server offers any prompt templates. + */ + prompts: z.optional( + z.looseObject({ + /** + * Whether this server supports issuing notifications for changes to the prompt list. + */ + listChanged: z.optional(z.boolean()), + }) + ), + /** + * Present if the server offers any resources to read. + */ + resources: z.optional( + z.looseObject({ + /** + * Whether this server supports clients subscribing to resource updates. + */ + subscribe: z.optional(z.boolean()), + + /** + * Whether this server supports issuing notifications for changes to the resource list. + */ + listChanged: z.optional(z.boolean()), + }) + ), + /** + * Present if the server offers any tools to call. + */ + tools: z.optional( + z.looseObject({ + /** + * Whether this server supports issuing notifications for changes to the tool list. + */ + listChanged: z.optional(z.boolean()), + }) + ), +}); /** * After receiving an initialize request from the client, the server sends this response. @@ -368,7 +341,9 @@ export const InitializedNotificationSchema = NotificationSchema.extend({ method: z.literal("notifications/initialized"), }); -export const isInitializedNotification = (value: unknown): value is InitializedNotification => +export const isInitializedNotification = ( + value: unknown +): value is InitializedNotification => InitializedNotificationSchema.safeParse(value).success; /* Ping */ @@ -380,22 +355,20 @@ export const PingRequestSchema = RequestSchema.extend({ }); /* Progress notifications */ -export const ProgressSchema = z - .object({ - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - */ - progress: z.number(), - /** - * Total number of items to process (or total progress required), if known. - */ - total: z.optional(z.number()), - /** - * An optional message describing the current progress. - */ - message: z.optional(z.string()), - }) - .passthrough(); +export const ProgressSchema = z.looseObject({ + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + */ + progress: z.number(), + /** + * Total number of items to process (or total progress required), if known. + */ + total: z.optional(z.number()), + /** + * An optional message describing the current progress. + */ + message: z.optional(z.string()), +}); /** * An out-of-band notification used to inform the receiver of a progress update for a long-running request. @@ -433,23 +406,21 @@ export const PaginatedResultSchema = ResultSchema.extend({ /** * The contents of a specific resource or sub-resource. */ -export const ResourceContentsSchema = z - .object({ - /** - * The URI of this resource. - */ - uri: z.string(), - /** - * The MIME type of this resource, if known. - */ - mimeType: z.optional(z.string()), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +export const ResourceContentsSchema = z.looseObject({ + /** + * The URI of this resource. + */ + uri: z.string(), + /** + * The MIME type of this resource, if known. + */ + mimeType: z.optional(z.string()), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); export const TextResourceContentsSchema = ResourceContentsSchema.extend({ /** @@ -458,24 +429,25 @@ export const TextResourceContentsSchema = ResourceContentsSchema.extend({ text: z.string(), }); - /** * A Zod schema for validating Base64 strings that is more performant and * robust for very large inputs than the default regex-based check. It avoids * stack overflows by using the native `atob` function for validation. */ const Base64Schema = z.string().refine( - (val) => { - try { - // atob throws a DOMException if the string contains characters - // that are not part of the Base64 character set. - atob(val); - return true; - } catch { - return false; - } - }, - { message: "Invalid Base64 string" }, + (val) => { + try { + // atob throws a DOMException if the string contains characters + // that are not part of the Base64 character set. + atob(val); + return true; + } catch { + return false; + } + }, + { + error: "Invalid Base64 string", + } ); export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ @@ -510,7 +482,7 @@ export const ResourceSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -538,7 +510,7 @@ export const ResourceTemplateSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -561,7 +533,7 @@ export const ListResourcesResultSchema = PaginatedResultSchema.extend({ export const ListResourceTemplatesRequestSchema = PaginatedRequestSchema.extend( { method: z.literal("resources/templates/list"), - }, + } ); /** @@ -589,7 +561,7 @@ export const ReadResourceRequestSchema = RequestSchema.extend({ */ export const ReadResourceResultSchema = ResultSchema.extend({ contents: z.array( - z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + z.union([TextResourceContentsSchema, BlobResourceContentsSchema]) ), }); @@ -643,22 +615,20 @@ export const ResourceUpdatedNotificationSchema = NotificationSchema.extend({ /** * Describes an argument that a prompt can accept. */ -export const PromptArgumentSchema = z - .object({ - /** - * The name of the argument. - */ - name: z.string(), - /** - * A human-readable description of the argument. - */ - description: z.optional(z.string()), - /** - * Whether this argument must be provided. - */ - required: z.optional(z.boolean()), - }) - .passthrough(); +export const PromptArgumentSchema = z.looseObject({ + /** + * The name of the argument. + */ + name: z.string(), + /** + * A human-readable description of the argument. + */ + description: z.optional(z.string()), + /** + * Whether this argument must be provided. + */ + required: z.optional(z.boolean()), +}); /** * A prompt or prompt template that the server offers. @@ -676,7 +646,7 @@ export const PromptSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -706,89 +676,81 @@ export const GetPromptRequestSchema = RequestSchema.extend({ /** * Arguments to use for templating the prompt. */ - arguments: z.optional(z.record(z.string())), + arguments: z.record(z.string(), z.string()).optional(), }), }); /** * Text provided to or from an LLM. */ -export const TextContentSchema = z - .object({ - type: z.literal("text"), - /** - * The text content of the message. - */ - text: z.string(), +export const TextContentSchema = z.looseObject({ + type: z.literal("text"), + /** + * The text content of the message. + */ + text: z.string(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * An image provided to or from an LLM. */ -export const ImageContentSchema = z - .object({ - type: z.literal("image"), - /** - * The base64-encoded image data. - */ - data: Base64Schema, - /** - * The MIME type of the image. Different providers may support different image types. - */ - mimeType: z.string(), +export const ImageContentSchema = z.looseObject({ + type: z.literal("image"), + /** + * The base64-encoded image data. + */ + data: Base64Schema, + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: z.string(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * An Audio provided to or from an LLM. */ -export const AudioContentSchema = z - .object({ - type: z.literal("audio"), - /** - * The base64-encoded audio data. - */ - data: Base64Schema, - /** - * The MIME type of the audio. Different providers may support different audio types. - */ - mimeType: z.string(), +export const AudioContentSchema = z.looseObject({ + type: z.literal("audio"), + /** + * The base64-encoded audio data. + */ + data: Base64Schema, + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: z.string(), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * The contents of a resource, embedded into a prompt or tool call result. */ -export const EmbeddedResourceSchema = z - .object({ - type: z.literal("resource"), - resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); +export const EmbeddedResourceSchema = z.looseObject({ + type: z.literal("resource"), + resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * A resource that the server is capable of reading, included in a prompt or tool call result. @@ -813,12 +775,10 @@ export const ContentBlockSchema = z.union([ /** * Describes a message returned as part of a prompt. */ -export const PromptMessageSchema = z - .object({ - role: z.enum(["user", "assistant"]), - content: ContentBlockSchema, - }) - .passthrough(); +export const PromptMessageSchema = z.looseObject({ + role: z.enum(["user", "assistant"]), + content: ContentBlockSchema, +}); /** * The server's response to a prompts/get request from the client. @@ -849,51 +809,49 @@ export const PromptListChangedNotificationSchema = NotificationSchema.extend({ * Clients should never make tool use decisions based on ToolAnnotations * received from untrusted servers. */ -export const ToolAnnotationsSchema = z - .object({ - /** - * A human-readable title for the tool. - */ - title: z.optional(z.string()), +export const ToolAnnotationsSchema = z.looseObject({ + /** + * A human-readable title for the tool. + */ + title: z.optional(z.string()), - /** - * If true, the tool does not modify its environment. - * - * Default: false - */ - readOnlyHint: z.optional(z.boolean()), + /** + * If true, the tool does not modify its environment. + * + * Default: false + */ + readOnlyHint: z.optional(z.boolean()), - /** - * If true, the tool may perform destructive updates to its environment. - * If false, the tool performs only additive updates. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: true - */ - destructiveHint: z.optional(z.boolean()), + /** + * If true, the tool may perform destructive updates to its environment. + * If false, the tool performs only additive updates. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: true + */ + destructiveHint: z.optional(z.boolean()), - /** - * If true, calling the tool repeatedly with the same arguments - * will have no additional effect on the its environment. - * - * (This property is meaningful only when `readOnlyHint == false`) - * - * Default: false - */ - idempotentHint: z.optional(z.boolean()), + /** + * If true, calling the tool repeatedly with the same arguments + * will have no additional effect on the its environment. + * + * (This property is meaningful only when `readOnlyHint == false`) + * + * Default: false + */ + idempotentHint: z.optional(z.boolean()), - /** - * If true, this tool may interact with an "open world" of external - * entities. If false, the tool's domain of interaction is closed. - * For example, the world of a web search tool is open, whereas that - * of a memory tool is not. - * - * Default: true - */ - openWorldHint: z.optional(z.boolean()), - }) - .passthrough(); + /** + * If true, this tool may interact with an "open world" of external + * entities. If false, the tool's domain of interaction is closed. + * For example, the world of a web search tool is open, whereas that + * of a memory tool is not. + * + * Default: true + */ + openWorldHint: z.optional(z.boolean()), +}); /** * Definition for a tool the client can call. @@ -906,24 +864,21 @@ export const ToolSchema = BaseMetadataSchema.extend({ /** * A JSON Schema object defining the expected parameters for the tool. */ - inputSchema: z - .object({ - type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), - required: z.optional(z.array(z.string())), - }) - .passthrough(), + inputSchema: z.looseObject({ + type: z.literal("object"), + properties: z.optional(z.looseObject({})), + required: z.optional(z.array(z.string())), + }), /** * An optional JSON Schema object defining the structure of the tool's output returned in * the structuredContent field of a CallToolResult. */ outputSchema: z.optional( - z.object({ + z.looseObject({ type: z.literal("object"), - properties: z.optional(z.object({}).passthrough()), + properties: z.optional(z.looseObject({})), required: z.optional(z.array(z.string())), }) - .passthrough() ), /** * Optional additional tool information. @@ -934,7 +889,7 @@ export const ToolSchema = BaseMetadataSchema.extend({ * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ - _meta: z.optional(z.object({}).passthrough()), + _meta: z.optional(z.looseObject({})), }); /** @@ -961,14 +916,14 @@ export const CallToolResultSchema = ResultSchema.extend({ * If the Tool does not define an outputSchema, this field MUST be present in the result. * For backwards compatibility, this field is always present, but it may be empty. */ - content: z.array(ContentBlockSchema).default([]), + content: z.array(ContentBlockSchema).prefault([]), /** * An object containing structured tool output. * * If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. */ - structuredContent: z.object({}).passthrough().optional(), + structuredContent: z.looseObject({}).optional(), /** * Whether the tool call ended in an error. @@ -993,7 +948,7 @@ export const CallToolResultSchema = ResultSchema.extend({ export const CompatibilityCallToolResultSchema = CallToolResultSchema.or( ResultSchema.extend({ toolResult: z.unknown(), - }), + }) ); /** @@ -1003,7 +958,7 @@ export const CallToolRequestSchema = RequestSchema.extend({ method: z.literal("tools/call"), params: BaseRequestParamsSchema.extend({ name: z.string(), - arguments: z.optional(z.record(z.unknown())), + arguments: z.record(z.string(), z.unknown()).optional(), }), }); @@ -1067,48 +1022,42 @@ export const LoggingMessageNotificationSchema = NotificationSchema.extend({ /** * Hints to use for model selection. */ -export const ModelHintSchema = z - .object({ - /** - * A hint for a model name. - */ - name: z.string().optional(), - }) - .passthrough(); +export const ModelHintSchema = z.looseObject({ + /** + * A hint for a model name. + */ + name: z.string().optional(), +}); /** * The server's preferences for model selection, requested of the client during sampling. */ -export const ModelPreferencesSchema = z - .object({ - /** - * Optional hints to use for model selection. - */ - hints: z.optional(z.array(ModelHintSchema)), - /** - * How much to prioritize cost when selecting a model. - */ - costPriority: z.optional(z.number().min(0).max(1)), - /** - * How much to prioritize sampling speed (latency) when selecting a model. - */ - speedPriority: z.optional(z.number().min(0).max(1)), - /** - * How much to prioritize intelligence and capabilities when selecting a model. - */ - intelligencePriority: z.optional(z.number().min(0).max(1)), - }) - .passthrough(); +export const ModelPreferencesSchema = z.looseObject({ + /** + * Optional hints to use for model selection. + */ + hints: z.optional(z.array(ModelHintSchema)), + /** + * How much to prioritize cost when selecting a model. + */ + costPriority: z.optional(z.number().min(0).max(1)), + /** + * How much to prioritize sampling speed (latency) when selecting a model. + */ + speedPriority: z.optional(z.number().min(0).max(1)), + /** + * How much to prioritize intelligence and capabilities when selecting a model. + */ + intelligencePriority: z.optional(z.number().min(0).max(1)), +}); /** * Describes a message issued to or received from an LLM API. */ -export const SamplingMessageSchema = z - .object({ - role: z.enum(["user", "assistant"]), - content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), - }) - .passthrough(); +export const SamplingMessageSchema = z.looseObject({ + role: z.enum(["user", "assistant"]), + content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), +}); /** * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. @@ -1129,12 +1078,12 @@ export const CreateMessageRequestSchema = RequestSchema.extend({ /** * The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested. */ - maxTokens: z.number().int(), + maxTokens: z.int(), stopSequences: z.optional(z.array(z.string())), /** * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. */ - metadata: z.optional(z.object({}).passthrough()), + metadata: z.optional(z.looseObject({})), /** * The server's preferences for which model to select. */ @@ -1154,13 +1103,13 @@ export const CreateMessageResultSchema = ResultSchema.extend({ * The reason why sampling stopped. */ stopReason: z.optional( - z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()), + z.enum(["endTurn", "stopSequence", "maxTokens"]).or(z.string()) ), role: z.enum(["user", "assistant"]), content: z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, - AudioContentSchema + AudioContentSchema, ]), }); @@ -1168,54 +1117,46 @@ export const CreateMessageResultSchema = ResultSchema.extend({ /** * Primitive schema definition for boolean fields. */ -export const BooleanSchemaSchema = z - .object({ - type: z.literal("boolean"), - title: z.optional(z.string()), - description: z.optional(z.string()), - default: z.optional(z.boolean()), - }) - .passthrough(); +export const BooleanSchemaSchema = z.looseObject({ + type: z.literal("boolean"), + title: z.optional(z.string()), + description: z.optional(z.string()), + default: z.optional(z.boolean()), +}); /** * Primitive schema definition for string fields. */ -export const StringSchemaSchema = z - .object({ - type: z.literal("string"), - title: z.optional(z.string()), - description: z.optional(z.string()), - minLength: z.optional(z.number()), - maxLength: z.optional(z.number()), - format: z.optional(z.enum(["email", "uri", "date", "date-time"])), - }) - .passthrough(); +export const StringSchemaSchema = z.looseObject({ + type: z.literal("string"), + title: z.optional(z.string()), + description: z.optional(z.string()), + minLength: z.optional(z.number()), + maxLength: z.optional(z.number()), + format: z.optional(z.enum(["email", "uri", "date", "date-time"])), +}); /** * Primitive schema definition for number fields. */ -export const NumberSchemaSchema = z - .object({ - type: z.enum(["number", "integer"]), - title: z.optional(z.string()), - description: z.optional(z.string()), - minimum: z.optional(z.number()), - maximum: z.optional(z.number()), - }) - .passthrough(); +export const NumberSchemaSchema = z.looseObject({ + type: z.enum(["number", "integer"]), + title: z.optional(z.string()), + description: z.optional(z.string()), + minimum: z.optional(z.number()), + maximum: z.optional(z.number()), +}); /** * Primitive schema definition for enum fields. */ -export const EnumSchemaSchema = z - .object({ - type: z.literal("string"), - title: z.optional(z.string()), - description: z.optional(z.string()), - enum: z.array(z.string()), - enumNames: z.optional(z.array(z.string())), - }) - .passthrough(); +export const EnumSchemaSchema = z.looseObject({ + type: z.literal("string"), + title: z.optional(z.string()), + description: z.optional(z.string()), + enum: z.array(z.string()), + enumNames: z.optional(z.array(z.string())), +}); /** * Union of all primitive schema definitions. @@ -1241,13 +1182,11 @@ export const ElicitRequestSchema = RequestSchema.extend({ /** * The schema for the requested user input. */ - requestedSchema: z - .object({ - type: z.literal("object"), - properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), - required: z.optional(z.array(z.string())), - }) - .passthrough(), + requestedSchema: z.looseObject({ + type: z.literal("object"), + properties: z.record(z.string(), PrimitiveSchemaDefinitionSchema), + required: z.optional(z.array(z.string())), + }), }), }); @@ -1262,22 +1201,22 @@ export const ElicitResultSchema = ResultSchema.extend({ /** * The collected user input content (only present if action is "accept"). */ - content: z.optional(z.record(z.string(), z.unknown())), + content: z + .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) + .optional(), }); /* Autocomplete */ /** * A reference to a resource or resource template definition. */ -export const ResourceTemplateReferenceSchema = z - .object({ - type: z.literal("ref/resource"), - /** - * The URI or URI template of the resource. - */ - uri: z.string(), - }) - .passthrough(); +export const ResourceTemplateReferenceSchema = z.looseObject({ + type: z.literal("ref/resource"), + /** + * The URI or URI template of the resource. + */ + uri: z.string(), +}); /** * @deprecated Use ResourceTemplateReferenceSchema instead @@ -1287,15 +1226,13 @@ export const ResourceReferenceSchema = ResourceTemplateReferenceSchema; /** * Identifies a prompt. */ -export const PromptReferenceSchema = z - .object({ - type: z.literal("ref/prompt"), - /** - * The name of the prompt or prompt template - */ - name: z.string(), - }) - .passthrough(); +export const PromptReferenceSchema = z.looseObject({ + type: z.literal("ref/prompt"), + /** + * The name of the prompt or prompt template + */ + name: z.string(), +}); /** * A request from the client to the server, to ask for completion options. @@ -1307,24 +1244,22 @@ export const CompleteRequestSchema = RequestSchema.extend({ /** * The argument's information */ - argument: z - .object({ - /** - * The name of the argument - */ - name: z.string(), - /** - * The value of the argument to use for completion matching. - */ - value: z.string(), - }) - .passthrough(), + argument: z.looseObject({ + /** + * The name of the argument + */ + name: z.string(), + /** + * The value of the argument to use for completion matching. + */ + value: z.string(), + }), context: z.optional( z.object({ /** * Previously-resolved variables in a URI template or prompt. */ - arguments: z.optional(z.record(z.string(), z.string())), + arguments: z.record(z.string(), z.string()).optional(), }) ), }), @@ -1334,46 +1269,42 @@ export const CompleteRequestSchema = RequestSchema.extend({ * The server's response to a completion/complete request */ export const CompleteResultSchema = ResultSchema.extend({ - completion: z - .object({ - /** - * An array of completion values. Must not exceed 100 items. - */ - values: z.array(z.string()).max(100), - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - total: z.optional(z.number().int()), - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - hasMore: z.optional(z.boolean()), - }) - .passthrough(), + completion: z.looseObject({ + /** + * An array of completion values. Must not exceed 100 items. + */ + values: z.array(z.string()).max(100), + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total: z.optional(z.int()), + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore: z.optional(z.boolean()), + }), }); /* Roots */ /** * Represents a root directory or file that the server can operate on. */ -export const RootSchema = z - .object({ - /** - * The URI identifying the root. This *must* start with file:// for now. - */ - uri: z.string().startsWith("file://"), - /** - * An optional name for the root. - */ - name: z.optional(z.string()), +export const RootSchema = z.looseObject({ + /** + * The URI identifying the root. This *must* start with file:// for now. + */ + uri: z.string().startsWith("file://"), + /** + * An optional name for the root. + */ + name: z.optional(z.string()), - /** - * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) - * for notes on _meta usage. - */ - _meta: z.optional(z.object({}).passthrough()), - }) - .passthrough(); + /** + * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) + * for notes on _meta usage. + */ + _meta: z.optional(z.looseObject({})), +}); /** * Sent from the server to request a list of root URIs from the client. @@ -1462,7 +1393,7 @@ export class McpError extends Error { constructor( public readonly code: number, message: string, - public readonly data?: unknown, + public readonly data?: unknown ) { super(`MCP error ${code}: ${message}`); this.name = "McpError"; @@ -1543,7 +1474,9 @@ export type ClientCapabilities = Infer; export type InitializeRequest = Infer; export type ServerCapabilities = Infer; export type InitializeResult = Infer; -export type InitializedNotification = Infer; +export type InitializedNotification = Infer< + typeof InitializedNotificationSchema +>; /* Ping */ export type PingRequest = Infer; @@ -1564,14 +1497,22 @@ export type Resource = Infer; export type ResourceTemplate = Infer; export type ListResourcesRequest = Infer; export type ListResourcesResult = Infer; -export type ListResourceTemplatesRequest = Infer; -export type ListResourceTemplatesResult = Infer; +export type ListResourceTemplatesRequest = Infer< + typeof ListResourceTemplatesRequestSchema +>; +export type ListResourceTemplatesResult = Infer< + typeof ListResourceTemplatesResultSchema +>; export type ReadResourceRequest = Infer; export type ReadResourceResult = Infer; -export type ResourceListChangedNotification = Infer; +export type ResourceListChangedNotification = Infer< + typeof ResourceListChangedNotificationSchema +>; export type SubscribeRequest = Infer; export type UnsubscribeRequest = Infer; -export type ResourceUpdatedNotification = Infer; +export type ResourceUpdatedNotification = Infer< + typeof ResourceUpdatedNotificationSchema +>; /* Prompts */ export type PromptArgument = Infer; @@ -1587,7 +1528,9 @@ export type ResourceLink = Infer; export type ContentBlock = Infer; export type PromptMessage = Infer; export type GetPromptResult = Infer; -export type PromptListChangedNotification = Infer; +export type PromptListChangedNotification = Infer< + typeof PromptListChangedNotificationSchema +>; /* Tools */ export type ToolAnnotations = Infer; @@ -1595,14 +1538,20 @@ export type Tool = Infer; export type ListToolsRequest = Infer; export type ListToolsResult = Infer; export type CallToolResult = Infer; -export type CompatibilityCallToolResult = Infer; +export type CompatibilityCallToolResult = Infer< + typeof CompatibilityCallToolResultSchema +>; export type CallToolRequest = Infer; -export type ToolListChangedNotification = Infer; +export type ToolListChangedNotification = Infer< + typeof ToolListChangedNotificationSchema +>; /* Logging */ export type LoggingLevel = Infer; export type SetLevelRequest = Infer; -export type LoggingMessageNotification = Infer; +export type LoggingMessageNotification = Infer< + typeof LoggingMessageNotificationSchema +>; /* Sampling */ export type SamplingMessage = Infer; @@ -1614,12 +1563,16 @@ export type BooleanSchema = Infer; export type StringSchema = Infer; export type NumberSchema = Infer; export type EnumSchema = Infer; -export type PrimitiveSchemaDefinition = Infer; +export type PrimitiveSchemaDefinition = Infer< + typeof PrimitiveSchemaDefinitionSchema +>; export type ElicitRequest = Infer; export type ElicitResult = Infer; /* Autocomplete */ -export type ResourceTemplateReference = Infer; +export type ResourceTemplateReference = Infer< + typeof ResourceTemplateReferenceSchema +>; /** * @deprecated Use ResourceTemplateReference instead */ @@ -1632,7 +1585,9 @@ export type CompleteResult = Infer; export type Root = Infer; export type ListRootsRequest = Infer; export type ListRootsResult = Infer; -export type RootsListChangedNotification = Infer; +export type RootsListChangedNotification = Infer< + typeof RootsListChangedNotificationSchema +>; /* Client messages */ export type ClientRequest = Infer; From 08513d322b388fd18256d06e053e27c441a1a95b Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Tue, 12 Aug 2025 09:40:17 -0700 Subject: [PATCH 2/7] chore: replace zod-to-json-schema temporarily --- jest.config.js | 1 + package-lock.json | 22 +++---- package.json | 4 +- src/server/mcp.ts | 164 ++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 171 insertions(+), 20 deletions(-) diff --git a/jest.config.js b/jest.config.js index f8f621c8b..1ea3839c4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,4 +13,5 @@ export default { "/node_modules/(?!eventsource)/" ], testPathIgnorePatterns: ["/node_modules/", "/dist/"], + modulePathIgnorePatterns: ["/dist"], }; diff --git a/package-lock.json b/package-lock.json index b8ac05e03..01fa26ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.17.2", "license": "MIT", "dependencies": { + "@finom/zod-to-json-schema": "^3.24.11", "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", @@ -19,8 +20,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^4.0.17", - "zod-to-json-schema": "^3.24.1" + "zod": "^4.0.17" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -1067,6 +1067,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@finom/zod-to-json-schema": { + "version": "3.24.11", + "resolved": "https://registry.npmjs.org/@finom/zod-to-json-schema/-/zod-to-json-schema-3.24.11.tgz", + "integrity": "sha512-fL656yBPiWebtfGItvtXLWrFNGlF1NcDFS0WdMQXMs9LluVg0CfT5E2oXYp0pidl0vVG53XkW55ysijNkU5/hA==", + "license": "ISC", + "peerDependencies": { + "zod": "^4.0.14" + } + }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", @@ -6646,15 +6655,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", - "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index 146fdaafd..83c75b83a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "client": "tsx src/cli.ts client" }, "dependencies": { + "@finom/zod-to-json-schema": "^3.24.11", "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", @@ -60,8 +61,7 @@ "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^4.0.17", - "zod-to-json-schema": "^3.24.1" + "zod": "^4.0.17" }, "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 919110bcb..f435e3e96 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1,5 +1,7 @@ import { Server, ServerOptions } from "./index.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; +// Lightweight replacement for zod-to-json-schema tailored for this SDK's needs. +// Produces a minimal JSON Schema subset: { type: "object", properties, required? } +// Supports primitives, enums, and nested objects. Sufficient for our tool list output tests. import { z, ZodRawShape, @@ -44,6 +46,156 @@ import { UriTemplate, Variables } from "../shared/uriTemplate.js"; import { RequestHandlerExtra } from "../shared/protocol.js"; import { Transport } from "../shared/transport.js"; +// Minimal JSON Schema generation for Zod v4 +type JsonSchema = { + type: "object" | "string" | "number" | "boolean" | "integer"; + properties?: Record; + required?: string[]; + enum?: string[]; +}; + +function zodToJsonSchema(schema: ZodObject): JsonSchema { + const shape = schema.shape; + const properties: NonNullable = {}; + const required: string[] = []; + + for (const [key, value] of Object.entries(shape)) { + const [unwrapped, isOptional] = unwrapOptional(value as ZodType); + const propSchema = toJsonForAny(unwrapped); + if (propSchema) { + properties[key] = propSchema; + } else { + // Fallback to empty object schema for unsupported types + properties[key] = {} as unknown as JsonSchema; + } + if (!isOptional) required.push(key); + } + + const result: JsonSchema = { type: "object", properties }; + if (required.length > 0) result.required = required; + return result; +} + +function unwrapOptional(s: ZodType): [ZodType, boolean] { + // Zod v4 Optional has unwrap(); fall back to heuristic if unavailable + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anySchema = s as any; + if (typeof anySchema.isOptional === "function" && anySchema.isOptional()) { + if (typeof anySchema.unwrap === "function") { + return [anySchema.unwrap(), true]; + } + // Best-effort: try _def.innerType if present + if (anySchema._def?.innerType) { + return [anySchema._def.innerType as ZodType, true]; + } + return [s, true]; + } + return [s, false]; +} + +function toJsonForAny( + s: ZodType +): JsonSchema | { enum: string[]; type?: "string" } | undefined { + // Prefer Zod v4 built-in conversion when available, then normalize + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anySchema = s as any; + if (typeof anySchema.toJsonSchema === "function") { + try { + const js = anySchema.toJsonSchema(); + const normalized = normalizeJsonFragment(js); + if (normalized) return normalized; + } catch { + // fall through to manual mapping + } + } + + // Manual mapping for common primitives and objects + if (isInstanceOf(anySchema, z.ZodString)) return { type: "string" }; + if (isInstanceOf(anySchema, z.ZodBoolean)) return { type: "boolean" }; + if (isInstanceOf(anySchema, z.ZodNumber)) return { type: "number" }; + if (isInstanceOf(anySchema, z.ZodObject)) + return zodToJsonSchema(anySchema as ZodObject); + // Enums + if (isInstanceOf(anySchema, z.ZodEnum)) { + const values = extractEnumValues(anySchema); + if (values) return { type: "string", enum: values }; + } + return undefined; +} + +function extractEnumValues(s: unknown): string[] | undefined { + // Try well-known locations across Zod versions + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anySchema = s as any; + if (Array.isArray(anySchema.options)) return anySchema.options as string[]; + if (Array.isArray(anySchema.values)) return anySchema.values as string[]; + if (Array.isArray(anySchema._def?.values)) + return anySchema._def.values as string[]; + return undefined; +} + +// Avoid `any` in constructor typing for linter compliance +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isInstanceOf( + value: unknown, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ctor: new (...args: any[]) => T +): value is T { + // Avoid crashes across dual bundles by checking name and prototype + return ( + typeof value === "object" && + value !== null && + (value as object) instanceof ctor + ); +} + +function normalizeJsonFragment( + js: unknown +): JsonSchema | { enum: string[]; type?: "string" } | undefined { + if (!js || typeof js !== "object") return undefined; + // If this already looks like a primitive schema + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const frag = js as any; + if ( + frag.type === "string" || + frag.type === "number" || + frag.type === "boolean" || + frag.type === "integer" + ) { + const res: JsonSchema = { type: frag.type }; + if (Array.isArray(frag.enum)) res.enum = frag.enum; + return res; + } + if (Array.isArray(frag.enum)) { + return { + type: typeof frag.type === "string" ? frag.type : "string", + enum: frag.enum, + }; + } + if ( + frag.type === "object" && + frag.properties && + typeof frag.properties === "object" + ) { + const properties: Record< + string, + JsonSchema | { enum: string[]; type?: "string" } + > = {}; + for (const [k, v] of Object.entries( + frag.properties as Record + )) { + const nv = normalizeJsonFragment(v); + if (nv) properties[k] = nv; + else properties[k] = {} as unknown as JsonSchema; + } + const out: JsonSchema = { type: "object", properties }; + if (Array.isArray(frag.required) && frag.required.length > 0) + out.required = frag.required.slice(); + return out; + } + return undefined; +} + /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying @@ -113,17 +265,15 @@ export class McpServer { title: tool.title, description: tool.description, inputSchema: tool.inputSchema - ? (zodToJsonSchema(tool.inputSchema, { - strictUnions: true, - }) as Tool["inputSchema"]) + ? (zodToJsonSchema(tool.inputSchema) as Tool["inputSchema"]) : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, }; if (tool.outputSchema) { - toolDefinition.outputSchema = zodToJsonSchema(tool.outputSchema, { - strictUnions: true, - }) as Tool["outputSchema"]; + toolDefinition.outputSchema = zodToJsonSchema( + tool.outputSchema + ) as Tool["outputSchema"]; } return toolDefinition; From 3b28aac046eff3c2943e8fb969f4856bf08eadd1 Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Tue, 12 Aug 2025 09:41:43 -0700 Subject: [PATCH 3/7] chore: remove dependency that is unused --- package-lock.json | 10 ---------- package.json | 1 - 2 files changed, 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01fa26ae3..d95991827 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.17.2", "license": "MIT", "dependencies": { - "@finom/zod-to-json-schema": "^3.24.11", "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", @@ -1067,15 +1066,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@finom/zod-to-json-schema": { - "version": "3.24.11", - "resolved": "https://registry.npmjs.org/@finom/zod-to-json-schema/-/zod-to-json-schema-3.24.11.tgz", - "integrity": "sha512-fL656yBPiWebtfGItvtXLWrFNGlF1NcDFS0WdMQXMs9LluVg0CfT5E2oXYp0pidl0vVG53XkW55ysijNkU5/hA==", - "license": "ISC", - "peerDependencies": { - "zod": "^4.0.14" - } - }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", diff --git a/package.json b/package.json index 83c75b83a..08d36712c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "client": "tsx src/cli.ts client" }, "dependencies": { - "@finom/zod-to-json-schema": "^3.24.11", "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", From 1cc8c7b4829065bba7c89ad82144857b300b3e72 Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Tue, 12 Aug 2025 09:52:18 -0700 Subject: [PATCH 4/7] chore: fix formatting of mcp.test --- src/server/mcp.test.ts | 931 +++++++++++++++++------------------------ 1 file changed, 383 insertions(+), 548 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 4df5062de..10e550df4 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1,7 +1,7 @@ import { McpServer } from "./mcp.js"; import { Client } from "../client/index.js"; import { InMemoryTransport } from "../inMemory.js"; -import { z } from "zod/v4"; +import { z } from "zod"; import { ListToolsResultSchema, CallToolResultSchema, @@ -14,7 +14,7 @@ import { LoggingMessageNotificationSchema, Notification, TextContent, - ElicitRequestSchema, + ElicitRequestSchema } from "../types.js"; import { ResourceTemplate } from "./mcp.js"; import { completable } from "./completable.js"; @@ -43,17 +43,17 @@ describe("McpServer", () => { name: "test server", version: "1.0", }, - { capabilities: { logging: {} } } + { capabilities: { logging: {} } }, ); - const notifications: Notification[] = []; + const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification); - }; + notifications.push(notification) + } const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -68,28 +68,30 @@ describe("McpServer", () => { mcpServer.server.sendLoggingMessage({ level: "info", data: "Test log message", - }) + }), ).resolves.not.toThrow(); expect(notifications).toMatchObject([ { - method: "notifications/message", + "method": "notifications/message", params: { level: "info", data: "Test log message", - }, - }, - ]); + } + } + ]) }); /*** * Test: Progress Notification with Message Field */ test("should send progress notifications with message field", async () => { - const mcpServer = new McpServer({ - name: "test server", - version: "1.0", - }); + const mcpServer = new McpServer( + { + name: "test server", + version: "1.0", + } + ); // Create a tool that sends progress updates mcpServer.tool( @@ -116,30 +118,18 @@ describe("McpServer", () => { } } - return { - content: [ - { - type: "text" as const, - text: `Operation completed with ${steps} steps`, - }, - ], - }; + return { content: [{ type: "text" as const, text: `Operation completed with ${steps} steps` }] }; } ); - const progressUpdates: Array<{ - progress: number; - total?: number; - message?: string; - }> = []; + const progressUpdates: Array<{ progress: number, total?: number, message?: string }> = []; const client = new Client({ name: "test client", version: "1.0", }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -154,15 +144,15 @@ describe("McpServer", () => { name: "long-operation", arguments: { steps: 3 }, _meta: { - progressToken: "progress-test-1", - }, - }, + progressToken: "progress-test-1" + } + } }, CallToolResultSchema, { onprogress: (progress) => { progressUpdates.push(progress); - }, + } } ); @@ -222,13 +212,9 @@ describe("ResourceTemplate", () => { const abortController = new AbortController(); const result = await template.listCallback?.({ signal: abortController.signal, - requestId: "not-implemented", - sendRequest: () => { - throw new Error("Not implemented"); - }, - sendNotification: () => { - throw new Error("Not implemented"); - }, + requestId: 'not-implemented', + sendRequest: () => { throw new Error("Not implemented") }, + sendNotification: () => { throw new Error("Not implemented") } }); expect(result?.resources).toHaveLength(1); expect(list).toHaveBeenCalled(); @@ -244,14 +230,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = []; + const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification); - }; + notifications.push(notification) + } mcpServer.tool("test", async () => ({ content: [ @@ -274,7 +260,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(1); @@ -285,7 +271,7 @@ describe("tool()", () => { }); // Adding the tool before the connection was established means no notification was sent - expect(notifications).toHaveLength(0); + expect(notifications).toHaveLength(0) // Adding another tool triggers the update notification mcpServer.tool("test2", async () => ({ @@ -298,13 +284,13 @@ describe("tool()", () => { })); // Yield event loop to let the notification fly - await new Promise(process.nextTick); + await new Promise(process.nextTick) expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed", - }, - ]); + } + ]) }); /*** @@ -315,14 +301,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = []; + const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification); - }; + notifications.push(notification) + } // Register initial tool const tool = mcpServer.tool("test", async () => ({ @@ -343,7 +329,7 @@ describe("tool()", () => { text: "Updated response", }, ], - }), + }) }); const [clientTransport, serverTransport] = @@ -362,7 +348,7 @@ describe("tool()", () => { name: "test", }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(result.content).toEqual([ @@ -373,7 +359,7 @@ describe("tool()", () => { ]); // Update happened before transport was connected, so no notifications should be expected - expect(notifications).toHaveLength(0); + expect(notifications).toHaveLength(0) }); /*** @@ -384,14 +370,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = []; + const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification); - }; + notifications.push(notification) + } // Register initial tool const tool = mcpServer.tool( @@ -406,7 +392,7 @@ describe("tool()", () => { text: `Initial: ${name}`, }, ], - }) + }), ); // Update the tool with a different schema @@ -422,7 +408,7 @@ describe("tool()", () => { text: `Updated: ${name}, ${value}`, }, ], - }), + }) }); const [clientTransport, serverTransport] = @@ -438,7 +424,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(listResult.tools[0].inputSchema).toMatchObject({ @@ -460,7 +446,7 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(callResult.content).toEqual([ @@ -471,7 +457,7 @@ describe("tool()", () => { ]); // Update happened before transport was connected, so no notifications should be expected - expect(notifications).toHaveLength(0); + expect(notifications).toHaveLength(0) }); /*** @@ -482,14 +468,14 @@ describe("tool()", () => { name: "test server", version: "1.0", }); - const notifications: Notification[] = []; + const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { - notifications.push(notification); - }; + notifications.push(notification) + } // Register initial tool const tool = mcpServer.tool("test", async () => ({ @@ -509,7 +495,7 @@ describe("tool()", () => { mcpServer.connect(serverTransport), ]); - expect(notifications).toHaveLength(0); + expect(notifications).toHaveLength(0) // Now update the tool tool.update({ @@ -520,26 +506,26 @@ describe("tool()", () => { text: "Updated response", }, ], - }), + }) }); // Yield event loop to let the notification fly - await new Promise(process.nextTick); + await new Promise(process.nextTick) expect(notifications).toMatchObject([ - { method: "notifications/tools/list_changed" }, - ]); + { method: "notifications/tools/list_changed" } + ]) // Now delete the tool tool.remove(); // Yield event loop to let the notification fly - await new Promise(process.nextTick); + await new Promise(process.nextTick) expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed" }, { method: "notifications/tools/list_changed" }, - ]); + ]) }); /*** @@ -569,7 +555,7 @@ describe("tool()", () => { text: `${name}: ${value}`, }, ], - }) + }), ); // new api @@ -595,7 +581,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); @@ -650,6 +636,7 @@ describe("tool()", () => { }) ); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -662,7 +649,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); @@ -685,18 +672,14 @@ describe("tool()", () => { version: "1.0", }); - mcpServer.tool( - "test", - { title: "Test Tool", readOnlyHint: true }, - async () => ({ - content: [ - { - type: "text", - text: "Test response", - }, - ], - }) - ); + mcpServer.tool("test", { title: "Test Tool", readOnlyHint: true }, async () => ({ + content: [ + { + type: "text", + text: "Test response", + }, + ], + })); mcpServer.registerTool( "test (new api)", @@ -725,20 +708,14 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); - expect(result.tools[0].annotations).toEqual({ - title: "Test Tool", - readOnlyHint: true, - }); + expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); expect(result.tools[1].name).toBe("test (new api)"); - expect(result.tools[1].annotations).toEqual({ - title: "Test Tool", - readOnlyHint: true, - }); + expect(result.tools[1].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); }); /*** @@ -759,7 +736,7 @@ describe("tool()", () => { { name: z.string() }, { title: "Test Tool", readOnlyHint: true }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }], + content: [{ type: "text", text: `Hello, ${name}!` }] }) ); @@ -770,7 +747,7 @@ describe("tool()", () => { annotations: { title: "Test Tool", readOnlyHint: true }, }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }], + content: [{ type: "text", text: `Hello, ${name}!` }] }) ); @@ -784,19 +761,16 @@ describe("tool()", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", - properties: { name: { type: "string" } }, - }); - expect(result.tools[0].annotations).toEqual({ - title: "Test Tool", - readOnlyHint: true, + properties: { name: { type: "string" } } }); + expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); @@ -821,7 +795,7 @@ describe("tool()", () => { { name: z.string() }, { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }], + content: [{ type: "text", text: `Hello, ${name}!` }] }) ); @@ -830,14 +804,10 @@ describe("tool()", () => { { description: "A tool with everything", inputSchema: { name: z.string() }, - annotations: { - title: "Complete Test Tool", - readOnlyHint: true, - openWorldHint: false, - }, + annotations: { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }, }, async ({ name }) => ({ - content: [{ type: "text", text: `Hello, ${name}!` }], + content: [{ type: "text", text: `Hello, ${name}!` }] }) ); @@ -851,7 +821,7 @@ describe("tool()", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); @@ -859,12 +829,12 @@ describe("tool()", () => { expect(result.tools[0].description).toBe("A tool with everything"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", - properties: { name: { type: "string" } }, + properties: { name: { type: "string" } } }); expect(result.tools[0].annotations).toEqual({ title: "Complete Test Tool", readOnlyHint: true, - openWorldHint: false, + openWorldHint: false }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].description).toBe("A tool with everything"); @@ -889,13 +859,9 @@ describe("tool()", () => { "test", "A tool with everything but empty params", {}, - { - title: "Complete Test Tool with empty params", - readOnlyHint: true, - openWorldHint: false, - }, + { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, async () => ({ - content: [{ type: "text", text: "Test response" }], + content: [{ type: "text", text: "Test response" }] }) ); @@ -904,14 +870,10 @@ describe("tool()", () => { { description: "A tool with everything but empty params", inputSchema: {}, - annotations: { - title: "Complete Test Tool with empty params", - readOnlyHint: true, - openWorldHint: false, - }, + annotations: { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, }, async () => ({ - content: [{ type: "text" as const, text: "Test response" }], + content: [{ type: "text" as const, text: "Test response" }] }) ); @@ -925,27 +887,23 @@ describe("tool()", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); - expect(result.tools[0].description).toBe( - "A tool with everything but empty params" - ); + expect(result.tools[0].description).toBe("A tool with everything but empty params"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", - properties: {}, + properties: {} }); expect(result.tools[0].annotations).toEqual({ title: "Complete Test Tool with empty params", readOnlyHint: true, - openWorldHint: false, + openWorldHint: false }); expect(result.tools[1].name).toBe("test (new api)"); - expect(result.tools[1].description).toBe( - "A tool with everything but empty params" - ); + expect(result.tools[1].description).toBe("A tool with everything but empty params"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); @@ -976,7 +934,7 @@ describe("tool()", () => { text: `${name}: ${value}`, }, ], - }) + }), ); mcpServer.registerTool( @@ -1017,8 +975,8 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema - ) + CallToolResultSchema, + ), ).rejects.toThrow(/Invalid arguments/); await expect( @@ -1033,8 +991,8 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema - ) + CallToolResultSchema, + ), ).rejects.toThrow(/Invalid arguments/); }); @@ -1109,14 +1067,14 @@ describe("tool()", () => { outputSchema: { processedInput: z.string(), resultType: z.string(), - timestamp: z.string(), + timestamp: z.string() }, }, async ({ input }) => ({ structuredContent: { processedInput: input, resultType: "structured", - timestamp: "2023-01-01T00:00:00Z", + timestamp: "2023-01-01T00:00:00Z" }, content: [ { @@ -1124,10 +1082,10 @@ describe("tool()", () => { text: JSON.stringify({ processedInput: input, resultType: "structured", - timestamp: "2023-01-01T00:00:00Z", + timestamp: "2023-01-01T00:00:00Z" }), }, - ], + ] }) ); @@ -1144,7 +1102,7 @@ describe("tool()", () => { { method: "tools/list", }, - ListToolsResultSchema + ListToolsResultSchema, ); expect(listResult.tools).toHaveLength(1); @@ -1153,9 +1111,9 @@ describe("tool()", () => { properties: { processedInput: { type: "string" }, resultType: { type: "string" }, - timestamp: { type: "string" }, + timestamp: { type: "string" } }, - required: ["processedInput", "resultType", "timestamp"], + required: ["processedInput", "resultType", "timestamp"] }); // Call the tool and verify it returns valid structuredContent @@ -1169,7 +1127,7 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(result.structuredContent).toBeDefined(); @@ -1208,8 +1166,7 @@ describe("tool()", () => { mcpServer.registerTool( "test", { - description: - "Test tool with output schema but missing structured content", + description: "Test tool with output schema but missing structured content", inputSchema: { input: z.string(), }, @@ -1244,10 +1201,8 @@ describe("tool()", () => { arguments: { input: "hello", }, - }) - ).rejects.toThrow( - /Tool test has an output schema but no structured content was provided/ - ); + }), + ).rejects.toThrow(/Tool test has an output schema but no structured content was provided/); }); /*** * Test: Tool with Output Schema Must Provide Structured Content @@ -1266,8 +1221,7 @@ describe("tool()", () => { mcpServer.registerTool( "test", { - description: - "Test tool with output schema but missing structured content", + description: "Test tool with output schema but missing structured content", inputSchema: { input: z.string(), }, @@ -1301,7 +1255,7 @@ describe("tool()", () => { arguments: { input: "hello", }, - }) + }), ).resolves.toStrictEqual({ content: [ { @@ -1338,7 +1292,7 @@ describe("tool()", () => { outputSchema: { processedInput: z.string(), resultType: z.string(), - timestamp: z.string(), + timestamp: z.string() }, }, async ({ input }) => ({ @@ -1349,7 +1303,7 @@ describe("tool()", () => { processedInput: input, resultType: "structured", // Missing required 'timestamp' field - someExtraField: "unexpected", // Extra field not in schema + someExtraField: "unexpected" // Extra field not in schema }), }, ], @@ -1357,7 +1311,7 @@ describe("tool()", () => { processedInput: input, resultType: "structured", // Missing required 'timestamp' field - someExtraField: "unexpected", // Extra field not in schema + someExtraField: "unexpected" // Extra field not in schema }, }) ); @@ -1377,7 +1331,7 @@ describe("tool()", () => { arguments: { input: "hello", }, - }) + }), ).rejects.toThrow(/Invalid structured content for tool test/); }); @@ -1408,8 +1362,7 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); // Set a test sessionId on the server transport serverTransport.sessionId = "test-session-123"; @@ -1425,7 +1378,7 @@ describe("tool()", () => { name: "test-tool", }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(receivedSessionId).toBe("test-session-123"); @@ -1458,8 +1411,7 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1473,17 +1425,12 @@ describe("tool()", () => { name: "request-id-test", }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(receivedRequestId).toBeDefined(); - expect( - typeof receivedRequestId === "string" || - typeof receivedRequestId === "number" - ).toBe(true); - expect(result.content && result.content[0].text).toContain( - "Received request ID:" - ); + expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); + expect(result.content && result.content[0].text).toContain("Received request ID:"); }); /*** @@ -1495,7 +1442,7 @@ describe("tool()", () => { name: "test server", version: "1.0", }, - { capabilities: { logging: {} } } + { capabilities: { logging: {} } }, ); const client = new Client({ @@ -1506,18 +1453,12 @@ describe("tool()", () => { let receivedLogMessage: string | undefined; const loggingMessage = "hello here is log message 1"; - client.setNotificationHandler( - LoggingMessageNotificationSchema, - (notification) => { - receivedLogMessage = notification.params.data as string; - } - ); + client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { + receivedLogMessage = notification.params.data as string; + }); mcpServer.tool("test-tool", async ({ sendNotification }) => { - await sendNotification({ - method: "notifications/message", - params: { level: "debug", data: loggingMessage }, - }); + await sendNotification({ method: "notifications/message", params: { level: "debug", data: loggingMessage } }); return { content: [ { @@ -1528,8 +1469,7 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), @@ -1541,7 +1481,7 @@ describe("tool()", () => { name: "test-tool", }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(receivedLogMessage).toBe(loggingMessage); }); @@ -1573,7 +1513,7 @@ describe("tool()", () => { text: `Processed: ${input}`, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -1594,7 +1534,7 @@ describe("tool()", () => { }, }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(result.content).toEqual([ @@ -1638,7 +1578,7 @@ describe("tool()", () => { name: "error-test", }, }, - CallToolResultSchema + CallToolResultSchema, ); expect(result.isError).toBe(true); @@ -1689,8 +1629,8 @@ describe("tool()", () => { name: "nonexistent-tool", }, }, - CallToolResultSchema - ) + CallToolResultSchema, + ), ).rejects.toThrow(/Tool nonexistent-tool not found/); }); }); @@ -1730,7 +1670,7 @@ describe("resource()", () => { { method: "resources/list", }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); @@ -1756,18 +1696,14 @@ describe("resource()", () => { }; // Register initial resource - const resource = mcpServer.resource( - "test", - "test://resource", - async () => ({ - contents: [ - { - uri: "test://resource", - text: "Initial content", - }, - ], - }) - ); + const resource = mcpServer.resource("test", "test://resource", async () => ({ + contents: [ + { + uri: "test://resource", + text: "Initial content", + }, + ], + })); // Update the resource resource.update({ @@ -1778,7 +1714,7 @@ describe("resource()", () => { text: "Updated content", }, ], - }), + }) }); const [clientTransport, serverTransport] = @@ -1797,7 +1733,7 @@ describe("resource()", () => { uri: "test://resource", }, }, - ReadResourceResultSchema + ReadResourceResultSchema, ); expect(result.contents).toHaveLength(1); @@ -1835,7 +1771,7 @@ describe("resource()", () => { text: "Initial content", }, ], - }) + }), ); // Update the resource template @@ -1847,7 +1783,7 @@ describe("resource()", () => { text: "Updated content", }, ], - }), + }) }); const [clientTransport, serverTransport] = @@ -1866,7 +1802,7 @@ describe("resource()", () => { uri: "test://resource/123", }, }, - ReadResourceResultSchema + ReadResourceResultSchema, ); expect(result.contents).toHaveLength(1); @@ -1894,21 +1830,16 @@ describe("resource()", () => { }; // Register initial resource - const resource = mcpServer.resource( - "test", - "test://resource", - async () => ({ - contents: [ - { - uri: "test://resource", - text: "Test content", - }, - ], - }) - ); + const resource = mcpServer.resource("test", "test://resource", async () => ({ + contents: [ + { + uri: "test://resource", + text: "Test content", + }, + ], + })); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1926,14 +1857,14 @@ describe("resource()", () => { text: "Updated content", }, ], - }), + }) }); // Yield event loop to let the notification fly await new Promise(process.nextTick); expect(notifications).toMatchObject([ - { method: "notifications/resources/list_changed" }, + { method: "notifications/resources/list_changed" } ]); }); @@ -1955,20 +1886,15 @@ describe("resource()", () => { }; // Register initial resources - const resource1 = mcpServer.resource( - "resource1", - "test://resource1", - async () => ({ - contents: [{ uri: "test://resource1", text: "Resource 1 content" }], - }) - ); + const resource1 = mcpServer.resource("resource1", "test://resource1", async () => ({ + contents: [{ uri: "test://resource1", text: "Resource 1 content" }], + })); mcpServer.resource("resource2", "test://resource2", async () => ({ contents: [{ uri: "test://resource2", text: "Resource 2 content" }], })); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -1978,7 +1904,7 @@ describe("resource()", () => { // Verify both resources are registered let result = await client.request( { method: "resources/list" }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(2); @@ -1986,20 +1912,20 @@ describe("resource()", () => { expect(notifications).toHaveLength(0); // Remove a resource - resource1.remove(); + resource1.remove() // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ - { method: "notifications/resources/list_changed" }, + { method: "notifications/resources/list_changed" } ]); // Verify the resource was removed result = await client.request( { method: "resources/list" }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); @@ -2034,11 +1960,10 @@ describe("resource()", () => { text: "Template content", }, ], - }) + }), ); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -2048,27 +1973,27 @@ describe("resource()", () => { // Verify template is registered const result = await client.request( { method: "resources/templates/list" }, - ListResourceTemplatesResultSchema + ListResourceTemplatesResultSchema, ); expect(result.resourceTemplates).toHaveLength(1); expect(notifications).toHaveLength(0); // Remove the template - resourceTemplate.remove(); + resourceTemplate.remove() // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ - { method: "notifications/resources/list_changed" }, + { method: "notifications/resources/list_changed" } ]); // Verify the template was removed const result2 = await client.request( { method: "resources/templates/list" }, - ListResourceTemplatesResultSchema + ListResourceTemplatesResultSchema, ); expect(result2.resourceTemplates).toHaveLength(0); @@ -2101,7 +2026,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2116,7 +2041,7 @@ describe("resource()", () => { { method: "resources/list", }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); @@ -2147,7 +2072,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2162,13 +2087,13 @@ describe("resource()", () => { { method: "resources/templates/list", }, - ListResourceTemplatesResultSchema + ListResourceTemplatesResultSchema, ); expect(result.resourceTemplates).toHaveLength(1); expect(result.resourceTemplates[0].name).toBe("test"); expect(result.resourceTemplates[0].uriTemplate).toBe( - "test://resource/{id}" + "test://resource/{id}", ); }); @@ -2208,7 +2133,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2223,7 +2148,7 @@ describe("resource()", () => { { method: "resources/list", }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(2); @@ -2258,7 +2183,7 @@ describe("resource()", () => { text: `Category: ${category}, ID: ${id}`, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2276,7 +2201,7 @@ describe("resource()", () => { uri: "test://resource/books/123", }, }, - ReadResourceResultSchema + ReadResourceResultSchema, ); expect(result.contents[0].text).toBe("Category: books, ID: 123"); @@ -2361,7 +2286,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); expect(() => { @@ -2375,7 +2300,7 @@ describe("resource()", () => { text: "Test content 2", }, ], - }) + }), ); }).toThrow(/already registered/); }); @@ -2413,8 +2338,8 @@ describe("resource()", () => { uri: "test://error", }, }, - ReadResourceResultSchema - ) + ReadResourceResultSchema, + ), ).rejects.toThrow(/Resource read failed/); }); @@ -2456,8 +2381,8 @@ describe("resource()", () => { uri: "test://nonexistent", }, }, - ReadResourceResultSchema - ) + ReadResourceResultSchema, + ), ).rejects.toThrow(/Resource test:\/\/nonexistent not found/); }); @@ -2489,7 +2414,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2500,8 +2425,8 @@ describe("resource()", () => { mcpServer.server.connect(serverTransport), ]); - expect(client.getServerCapabilities()).toMatchObject({ completions: {} }); - }); + expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) + }) /*** * Test: Resource Template Parameter Completion @@ -2532,7 +2457,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2557,7 +2482,7 @@ describe("resource()", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result.completion.values).toEqual(["books", "movies", "music"]); @@ -2585,7 +2510,7 @@ describe("resource()", () => { complete: { category: (test: string) => ["books", "movies", "music"].filter((value) => - value.startsWith(test) + value.startsWith(test), ), }, }), @@ -2596,7 +2521,7 @@ describe("resource()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -2621,7 +2546,7 @@ describe("resource()", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result.completion.values).toEqual(["movies", "music"]); @@ -2643,24 +2568,19 @@ describe("resource()", () => { }); let receivedRequestId: string | number | undefined; - mcpServer.resource( - "request-id-test", - "test://resource", - async (_uri, extra) => { - receivedRequestId = extra.requestId; - return { - contents: [ - { - uri: "test://resource", - text: `Received request ID: ${extra.requestId}`, - }, - ], - }; - } - ); + mcpServer.resource("request-id-test", "test://resource", async (_uri, extra) => { + receivedRequestId = extra.requestId; + return { + contents: [ + { + uri: "test://resource", + text: `Received request ID: ${extra.requestId}`, + }, + ], + }; + }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -2674,14 +2594,11 @@ describe("resource()", () => { uri: "test://resource", }, }, - ReadResourceResultSchema + ReadResourceResultSchema, ); expect(receivedRequestId).toBeDefined(); - expect( - typeof receivedRequestId === "string" || - typeof receivedRequestId === "number" - ).toBe(true); + expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); expect(result.contents[0].text).toContain("Received request ID:"); }); }); @@ -2724,7 +2641,7 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema + ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); @@ -2773,7 +2690,7 @@ describe("prompt()", () => { }, }, ], - }), + }) }); const [clientTransport, serverTransport] = @@ -2792,7 +2709,7 @@ describe("prompt()", () => { name: "test", }, }, - GetPromptResultSchema + GetPromptResultSchema, ); expect(result.messages).toHaveLength(1); @@ -2835,7 +2752,7 @@ describe("prompt()", () => { }, }, ], - }) + }), ); // Update the prompt with a different schema @@ -2854,7 +2771,7 @@ describe("prompt()", () => { }, }, ], - }), + }) }); const [clientTransport, serverTransport] = @@ -2870,14 +2787,11 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema + ListPromptsResultSchema, ); expect(listResult.prompts[0].arguments).toHaveLength(2); - expect(listResult.prompts[0].arguments?.map((a) => a.name).sort()).toEqual([ - "name", - "value", - ]); + expect(listResult.prompts[0].arguments?.map(a => a.name).sort()).toEqual(["name", "value"]); // Call the prompt with the new schema const getResult = await client.request( @@ -2891,7 +2805,7 @@ describe("prompt()", () => { }, }, }, - GetPromptResultSchema + GetPromptResultSchema, ); expect(getResult.messages).toHaveLength(1); @@ -2931,8 +2845,7 @@ describe("prompt()", () => { ], })); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -2953,14 +2866,14 @@ describe("prompt()", () => { }, }, ], - }), + }) }); // Yield event loop to let the notification fly await new Promise(process.nextTick); expect(notifications).toMatchObject([ - { method: "notifications/prompts/list_changed" }, + { method: "notifications/prompts/list_changed" } ]); }); @@ -3006,8 +2919,7 @@ describe("prompt()", () => { ], })); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -3017,32 +2929,29 @@ describe("prompt()", () => { // Verify both prompts are registered let result = await client.request( { method: "prompts/list" }, - ListPromptsResultSchema + ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(2); - expect(result.prompts.map((p) => p.name).sort()).toEqual([ - "prompt1", - "prompt2", - ]); + expect(result.prompts.map(p => p.name).sort()).toEqual(["prompt1", "prompt2"]); expect(notifications).toHaveLength(0); // Remove a prompt - prompt1.remove(); + prompt1.remove() // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ - { method: "notifications/prompts/list_changed" }, + { method: "notifications/prompts/list_changed" } ]); // Verify the prompt was removed result = await client.request( { method: "prompts/list" }, - ListPromptsResultSchema + ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); @@ -3078,7 +2987,7 @@ describe("prompt()", () => { }, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3093,7 +3002,7 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema + ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); @@ -3141,7 +3050,7 @@ describe("prompt()", () => { { method: "prompts/list", }, - ListPromptsResultSchema + ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); @@ -3179,7 +3088,7 @@ describe("prompt()", () => { }, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3202,8 +3111,8 @@ describe("prompt()", () => { }, }, }, - GetPromptResultSchema - ) + GetPromptResultSchema, + ), ).rejects.toThrow(/Invalid arguments/); }); @@ -3289,17 +3198,19 @@ describe("prompt()", () => { }); // This should succeed - mcpServer.prompt("echo", { message: z.string() }, ({ message }) => ({ - messages: [ - { + mcpServer.prompt( + "echo", + { message: z.string() }, + ({ message }) => ({ + messages: [{ role: "user", content: { type: "text", - text: `Please process this message: ${message}`, - }, - }, - ], - })); + text: `Please process this message: ${message}` + } + }] + }) + ); }); /*** @@ -3327,7 +3238,7 @@ describe("prompt()", () => { text: "Test content", }, ], - }) + }), ); // Register a prompt with completion @@ -3335,15 +3246,13 @@ describe("prompt()", () => { "echo", { message: completable(z.string(), () => ["hello", "world"]) }, ({ message }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: `Please process this message: ${message}`, - }, - }, - ], + messages: [{ + role: "user", + content: { + type: "text", + text: `Please process this message: ${message}` + } + }] }) ); }); @@ -3390,11 +3299,12 @@ describe("prompt()", () => { name: "nonexistent-prompt", }, }, - GetPromptResultSchema - ) + GetPromptResultSchema, + ), ).rejects.toThrow(/Prompt nonexistent-prompt not found/); }); + /*** * Test: Registering a prompt with a completable argument should update server capabilities to advertise support for completion */ @@ -3423,7 +3333,7 @@ describe("prompt()", () => { }, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3434,8 +3344,8 @@ describe("prompt()", () => { mcpServer.server.connect(serverTransport), ]); - expect(client.getServerCapabilities()).toMatchObject({ completions: {} }); - }); + expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) + }) /*** * Test: Prompt Argument Completion @@ -3466,7 +3376,7 @@ describe("prompt()", () => { }, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3491,7 +3401,7 @@ describe("prompt()", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result.completion.values).toEqual(["Alice", "Bob", "Charlie"]); @@ -3516,7 +3426,7 @@ describe("prompt()", () => { "test-prompt", { name: completable(z.string(), (test) => - ["Alice", "Bob", "Charlie"].filter((value) => value.startsWith(test)) + ["Alice", "Bob", "Charlie"].filter((value) => value.startsWith(test)), ), }, async ({ name }) => ({ @@ -3529,7 +3439,7 @@ describe("prompt()", () => { }, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3554,7 +3464,7 @@ describe("prompt()", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result.completion.values).toEqual(["Alice"]); @@ -3591,8 +3501,7 @@ describe("prompt()", () => { }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -3606,14 +3515,11 @@ describe("prompt()", () => { name: "request-id-test", }, }, - GetPromptResultSchema + GetPromptResultSchema, ); expect(receivedRequestId).toBeDefined(); - expect( - typeof receivedRequestId === "string" || - typeof receivedRequestId === "number" - ).toBe(true); + expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); expect(result.messages[0].content.text).toContain("Received request ID:"); }); @@ -3660,7 +3566,7 @@ describe("prompt()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3675,16 +3581,14 @@ describe("prompt()", () => { { method: "resources/list", }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(2); // Resource 1 should have its own metadata expect(result.resources[0].name).toBe("Resource 1"); - expect(result.resources[0].description).toBe( - "Individual resource description" - ); + expect(result.resources[0].description).toBe("Individual resource description"); expect(result.resources[0].mimeType).toBe("text/plain"); // Resource 2 should inherit template metadata @@ -3733,7 +3637,7 @@ describe("prompt()", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3748,7 +3652,7 @@ describe("prompt()", () => { { method: "resources/list", }, - ListResourcesResultSchema + ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); @@ -3772,16 +3676,19 @@ describe("Tool title precedence", () => { }); // Tool 1: Only name - mcpServer.tool("tool_name_only", async () => ({ - content: [{ type: "text", text: "Response" }], - })); + mcpServer.tool( + "tool_name_only", + async () => ({ + content: [{ type: "text", text: "Response" }], + }) + ); // Tool 2: Name and annotations.title mcpServer.tool( "tool_with_annotations_title", "Tool with annotations title", { - title: "Annotations Title", + title: "Annotations Title" }, async () => ({ content: [{ type: "text", text: "Response" }], @@ -3793,7 +3700,7 @@ describe("Tool title precedence", () => { "tool_with_title", { title: "Regular Title", - description: "Tool with regular title", + description: "Tool with regular title" }, async () => ({ content: [{ type: "text", text: "Response" }], @@ -3807,16 +3714,15 @@ describe("Tool title precedence", () => { title: "Regular Title Wins", description: "Tool with all titles", annotations: { - title: "Annotations Title Should Not Show", - }, + title: "Annotations Title Should Not Show" + } }, async () => ({ content: [{ type: "text", text: "Response" }], }) ); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), @@ -3824,32 +3730,31 @@ describe("Tool title precedence", () => { const result = await client.request( { method: "tools/list" }, - ListToolsResultSchema + ListToolsResultSchema, ); + expect(result.tools).toHaveLength(4); // Tool 1: Only name - should display name - const tool1 = result.tools.find((t) => t.name === "tool_name_only"); + const tool1 = result.tools.find(t => t.name === "tool_name_only"); expect(tool1).toBeDefined(); expect(getDisplayName(tool1!)).toBe("tool_name_only"); // Tool 2: Name and annotations.title - should display annotations.title - const tool2 = result.tools.find( - (t) => t.name === "tool_with_annotations_title" - ); + const tool2 = result.tools.find(t => t.name === "tool_with_annotations_title"); expect(tool2).toBeDefined(); expect(tool2!.annotations?.title).toBe("Annotations Title"); expect(getDisplayName(tool2!)).toBe("Annotations Title"); // Tool 3: Name and title - should display title - const tool3 = result.tools.find((t) => t.name === "tool_with_title"); + const tool3 = result.tools.find(t => t.name === "tool_with_title"); expect(tool3).toBeDefined(); expect(tool3!.title).toBe("Regular Title"); expect(getDisplayName(tool3!)).toBe("Regular Title"); // Tool 4: All three - title should take precedence - const tool4 = result.tools.find((t) => t.name === "tool_with_all_titles"); + const tool4 = result.tools.find(t => t.name === "tool_with_all_titles"); expect(tool4).toBeDefined(); expect(tool4!.title).toBe("Regular Title Wins"); expect(tool4!.annotations?.title).toBe("Annotations Title Should Not Show"); @@ -3857,51 +3762,42 @@ describe("Tool title precedence", () => { }); test("getDisplayName unit tests for title precedence", () => { + // Test 1: Only name expect(getDisplayName({ name: "tool_name" })).toBe("tool_name"); // Test 2: Name and title - title wins - expect( - getDisplayName({ - name: "tool_name", - title: "Tool Title", - }) - ).toBe("Tool Title"); + expect(getDisplayName({ + name: "tool_name", + title: "Tool Title" + })).toBe("Tool Title"); // Test 3: Name and annotations.title - annotations.title wins - expect( - getDisplayName({ - name: "tool_name", - annotations: { title: "Annotations Title" }, - }) - ).toBe("Annotations Title"); + expect(getDisplayName({ + name: "tool_name", + annotations: { title: "Annotations Title" } + })).toBe("Annotations Title"); // Test 4: All three - title wins (correct precedence) - expect( - getDisplayName({ - name: "tool_name", - title: "Regular Title", - annotations: { title: "Annotations Title" }, - }) - ).toBe("Regular Title"); + expect(getDisplayName({ + name: "tool_name", + title: "Regular Title", + annotations: { title: "Annotations Title" } + })).toBe("Regular Title"); // Test 5: Empty title should not be used - expect( - getDisplayName({ - name: "tool_name", - title: "", - annotations: { title: "Annotations Title" }, - }) - ).toBe("Annotations Title"); + expect(getDisplayName({ + name: "tool_name", + title: "", + annotations: { title: "Annotations Title" } + })).toBe("Annotations Title"); // Test 6: Undefined vs null handling - expect( - getDisplayName({ - name: "tool_name", - title: undefined, - annotations: { title: "Annotations Title" }, - }) - ).toBe("Annotations Title"); + expect(getDisplayName({ + name: "tool_name", + title: undefined, + annotations: { title: "Annotations Title" } + })).toBe("Annotations Title"); }); test("should support resource template completion with resolved context", async () => { @@ -3922,13 +3818,9 @@ describe("Tool title precedence", () => { complete: { repo: (value, context) => { if (context?.arguments?.["owner"] === "org1") { - return ["project1", "project2", "project3"].filter((r) => - r.startsWith(value) - ); + return ["project1", "project2", "project3"].filter(r => r.startsWith(value)); } else if (context?.arguments?.["owner"] === "org2") { - return ["repo1", "repo2", "repo3"].filter((r) => - r.startsWith(value) - ); + return ["repo1", "repo2", "repo3"].filter(r => r.startsWith(value)); } return []; }, @@ -3936,7 +3828,7 @@ describe("Tool title precedence", () => { }), { title: "GitHub Repository", - description: "Repository information", + description: "Repository information" }, async () => ({ contents: [ @@ -3945,7 +3837,7 @@ describe("Tool title precedence", () => { text: "Test content", }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -3976,14 +3868,10 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); - expect(result1.completion.values).toEqual([ - "project1", - "project2", - "project3", - ]); + expect(result1.completion.values).toEqual(["project1", "project2", "project3"]); expect(result1.completion.total).toBe(3); // Test with facebook owner @@ -4006,7 +3894,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result2.completion.values).toEqual(["repo1", "repo2", "repo3"]); @@ -4027,7 +3915,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result3.completion.values).toEqual([]); @@ -4052,28 +3940,20 @@ describe("Tool title precedence", () => { description: "Generate a greeting for team members", argsSchema: { department: completable(z.string(), (value) => { - return ["engineering", "sales", "marketing", "support"].filter( - (d) => d.startsWith(value) - ); + return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value)); }), name: completable(z.string(), (value, context) => { const department = context?.arguments?.["department"]; if (department === "engineering") { - return ["Alice", "Bob", "Charlie"].filter((n) => - n.startsWith(value) - ); + return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value)); } else if (department === "sales") { - return ["David", "Eve", "Frank"].filter((n) => - n.startsWith(value) - ); + return ["David", "Eve", "Frank"].filter(n => n.startsWith(value)); } else if (department === "marketing") { - return ["Grace", "Henry", "Iris"].filter((n) => - n.startsWith(value) - ); + return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value)); } - return ["Guest"].filter((n) => n.startsWith(value)); + return ["Guest"].filter(n => n.startsWith(value)); }), - }, + } }, async ({ department, name }) => ({ messages: [ @@ -4085,7 +3965,7 @@ describe("Tool title precedence", () => { }, }, ], - }) + }), ); const [clientTransport, serverTransport] = @@ -4116,7 +3996,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result1.completion.values).toEqual(["Alice"]); @@ -4141,7 +4021,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result2.completion.values).toEqual(["David"]); @@ -4166,7 +4046,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result3.completion.values).toEqual(["Grace"]); @@ -4186,7 +4066,7 @@ describe("Tool title precedence", () => { }, }, }, - CompleteResultSchema + CompleteResultSchema, ); expect(result4.completion.values).toEqual(["Guest"]); @@ -4194,6 +4074,7 @@ describe("Tool title precedence", () => { }); describe("elicitInput()", () => { + const checkAvailability = jest.fn().mockResolvedValue(false); const findAlternatives = jest.fn().mockResolvedValue([]); const makeBooking = jest.fn().mockResolvedValue("BOOKING-123"); @@ -4216,7 +4097,7 @@ describe("elicitInput()", () => { { restaurant: z.string(), date: z.string(), - partySize: z.number(), + partySize: z.number() }, async ({ restaurant, date, partySize }) => { // Check availability @@ -4232,63 +4113,49 @@ describe("elicitInput()", () => { checkAlternatives: { type: "boolean", title: "Check alternative dates", - description: "Would you like me to check other dates?", + description: "Would you like me to check other dates?" }, flexibleDates: { type: "string", title: "Date flexibility", description: "How flexible are your dates?", enum: ["next_day", "same_week", "next_week"], - enumNames: ["Next day", "Same week", "Next week"], - }, + enumNames: ["Next day", "Same week", "Next week"] + } }, - required: ["checkAlternatives"], - }, + required: ["checkAlternatives"] + } }); - if ( - result.action === "accept" && - typeof result.content === "object" && - result.content !== null && - "checkAlternatives" in result.content && - (result.content as Record).checkAlternatives === - true - ) { + if (result.action === "accept" && result.content?.checkAlternatives) { const alternatives = await findAlternatives( restaurant, date, partySize, - (result.content as Record) - .flexibleDates as string + result.content.flexibleDates as string ); return { - content: [ - { - type: "text", - text: `Found these alternatives: ${alternatives.join(", ")}`, - }, - ], + content: [{ + type: "text", + text: `Found these alternatives: ${alternatives.join(", ")}` + }] }; } return { - content: [ - { - type: "text", - text: "No booking made. Original date not available.", - }, - ], + content: [{ + type: "text", + text: "No booking made. Original date not available." + }] }; } await makeBooking(restaurant, date, partySize); return { - content: [ - { - type: "text", - text: `Booked table for ${partySize} at ${restaurant} on ${date}`, - }, - ], + content: [{ + type: "text", + text: `Booked table for ${partySize} at ${restaurant} on ${date}` + }] }; } ); @@ -4310,28 +4177,21 @@ describe("elicitInput()", () => { test("should successfully elicit additional information", async () => { // Mock availability check to return false checkAvailability.mockResolvedValue(false); - findAlternatives.mockResolvedValue([ - "2024-12-26", - "2024-12-27", - "2024-12-28", - ]); + findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]); // Set up client to accept alternative date checking client.setRequestHandler(ElicitRequestSchema, async (request) => { - expect(request.params.message).toContain( - "No tables available at ABC Restaurant on 2024-12-25" - ); + expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25"); return { action: "accept", content: { checkAlternatives: true, - flexibleDates: "same_week", - }, + flexibleDates: "same_week" + } }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -4344,27 +4204,16 @@ describe("elicitInput()", () => { arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", - partySize: 2, - }, + partySize: 2 + } }); - expect(checkAvailability).toHaveBeenCalledWith( - "ABC Restaurant", - "2024-12-25", - 2 - ); - expect(findAlternatives).toHaveBeenCalledWith( - "ABC Restaurant", - "2024-12-25", - 2, - "same_week" - ); - expect(result.content).toEqual([ - { - type: "text", - text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28", - }, - ]); + expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); + expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week"); + expect(result.content).toEqual([{ + type: "text", + text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28" + }]); }); test("should handle user declining to elicitation request", async () => { @@ -4376,13 +4225,12 @@ describe("elicitInput()", () => { return { action: "accept", content: { - checkAlternatives: false, - }, + checkAlternatives: false + } }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -4395,22 +4243,16 @@ describe("elicitInput()", () => { arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", - partySize: 2, - }, + partySize: 2 + } }); - expect(checkAvailability).toHaveBeenCalledWith( - "ABC Restaurant", - "2024-12-25", - 2 - ); + expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); expect(findAlternatives).not.toHaveBeenCalled(); - expect(result.content).toEqual([ - { - type: "text", - text: "No booking made. Original date not available.", - }, - ]); + expect(result.content).toEqual([{ + type: "text", + text: "No booking made. Original date not available." + }]); }); test("should handle user cancelling the elicitation", async () => { @@ -4420,12 +4262,11 @@ describe("elicitInput()", () => { // Set up client to cancel the elicitation client.setRequestHandler(ElicitRequestSchema, async () => { return { - action: "cancel", + action: "cancel" }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), @@ -4438,21 +4279,15 @@ describe("elicitInput()", () => { arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", - partySize: 2, - }, + partySize: 2 + } }); - expect(checkAvailability).toHaveBeenCalledWith( - "ABC Restaurant", - "2024-12-25", - 2 - ); + expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); expect(findAlternatives).not.toHaveBeenCalled(); - expect(result.content).toEqual([ - { - type: "text", - text: "No booking made. Original date not available.", - }, - ]); + expect(result.content).toEqual([{ + type: "text", + text: "No booking made. Original date not available." + }]); }); }); From 9b178b7bd6860e817628534bea0fd272494aad9b Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Tue, 12 Aug 2025 10:03:08 -0700 Subject: [PATCH 5/7] chore: add commas --- src/server/mcp.ts | 180 +++++++++++++++++++++++----------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index f435e3e96..ee32b08cf 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -94,7 +94,7 @@ function unwrapOptional(s: ZodType): [ZodType, boolean] { } function toJsonForAny( - s: ZodType + s: ZodType, ): JsonSchema | { enum: string[]; type?: "string" } | undefined { // Prefer Zod v4 built-in conversion when available, then normalize // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -139,7 +139,7 @@ function extractEnumValues(s: unknown): string[] | undefined { function isInstanceOf( value: unknown, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ctor: new (...args: any[]) => T + ctor: new (...args: any[]) => T, ): value is T { // Avoid crashes across dual bundles by checking name and prototype return ( @@ -150,7 +150,7 @@ function isInstanceOf( } function normalizeJsonFragment( - js: unknown + js: unknown, ): JsonSchema | { enum: string[]; type?: "string" } | undefined { if (!js || typeof js !== "object") return undefined; // If this already looks like a primitive schema @@ -182,7 +182,7 @@ function normalizeJsonFragment( JsonSchema | { enum: string[]; type?: "string" } > = {}; for (const [k, v] of Object.entries( - frag.properties as Record + frag.properties as Record, )) { const nv = normalizeJsonFragment(v); if (nv) properties[k] = nv; @@ -242,10 +242,10 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - ListToolsRequestSchema.shape.method.value + ListToolsRequestSchema.shape.method.value, ); this.server.assertCanSetRequestHandler( - CallToolRequestSchema.shape.method.value + CallToolRequestSchema.shape.method.value, ); this.server.registerCapabilities({ @@ -272,13 +272,13 @@ export class McpServer { if (tool.outputSchema) { toolDefinition.outputSchema = zodToJsonSchema( - tool.outputSchema + tool.outputSchema, ) as Tool["outputSchema"]; } return toolDefinition; }), - }) + }), ); this.server.setRequestHandler( @@ -288,14 +288,14 @@ export class McpServer { if (!tool) { throw new McpError( ErrorCode.InvalidParams, - `Tool ${request.params.name} not found` + `Tool ${request.params.name} not found`, ); } if (!tool.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Tool ${request.params.name} disabled` + `Tool ${request.params.name} disabled`, ); } @@ -303,12 +303,12 @@ export class McpServer { if (tool.inputSchema) { const parseResult = await tool.inputSchema.safeParseAsync( - request.params.arguments + request.params.arguments, ); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, - `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}` + `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`, ); } @@ -348,24 +348,24 @@ export class McpServer { if (!result.structuredContent) { throw new McpError( ErrorCode.InvalidParams, - `Tool ${request.params.name} has an output schema but no structured content was provided` + `Tool ${request.params.name} has an output schema but no structured content was provided`, ); } // if the tool has an output schema, validate structured content const parseResult = await tool.outputSchema.safeParseAsync( - result.structuredContent + result.structuredContent, ); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, - `Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}` + `Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}`, ); } } return result; - } + }, ); this._toolHandlersInitialized = true; @@ -379,7 +379,7 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - CompleteRequestSchema.shape.method.value + CompleteRequestSchema.shape.method.value, ); this.server.registerCapabilities({ @@ -399,10 +399,10 @@ export class McpServer { default: throw new McpError( ErrorCode.InvalidParams, - `Invalid completion reference: ${request.params.ref}` + `Invalid completion reference: ${request.params.ref}`, ); } - } + }, ); this._completionHandlerInitialized = true; @@ -410,20 +410,20 @@ export class McpServer { private async handlePromptCompletion( request: CompleteRequest, - ref: PromptReference + ref: PromptReference, ): Promise { const prompt = this._registeredPrompts[ref.name]; if (!prompt) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${ref.name} not found` + `Prompt ${ref.name} not found`, ); } if (!prompt.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${ref.name} disabled` + `Prompt ${ref.name} disabled`, ); } @@ -446,17 +446,17 @@ export class McpServer { : undefined; const suggestions = await def.complete( request.params.argument.value, - ctxForComplete + ctxForComplete, ); return createCompletionResult(suggestions); } private async handleResourceCompletion( request: CompleteRequest, - ref: ResourceTemplateReference + ref: ResourceTemplateReference, ): Promise { const template = Object.values(this._registeredResourceTemplates).find( - (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri + (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri, ); if (!template) { @@ -467,12 +467,12 @@ export class McpServer { throw new McpError( ErrorCode.InvalidParams, - `Resource template ${request.params.ref.uri} not found` + `Resource template ${request.params.ref.uri} not found`, ); } const completer = template.resourceTemplate.completeCallback( - request.params.argument.name + request.params.argument.name, ); if (!completer) { return EMPTY_COMPLETION_RESULT; @@ -485,7 +485,7 @@ export class McpServer { : undefined; const suggestions = await completer( request.params.argument.value, - ctxForComplete + ctxForComplete, ); return createCompletionResult(suggestions); } @@ -498,13 +498,13 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - ListResourcesRequestSchema.shape.method.value + ListResourcesRequestSchema.shape.method.value, ); this.server.assertCanSetRequestHandler( - ListResourceTemplatesRequestSchema.shape.method.value + ListResourceTemplatesRequestSchema.shape.method.value, ); this.server.assertCanSetRequestHandler( - ReadResourceRequestSchema.shape.method.value + ReadResourceRequestSchema.shape.method.value, ); this.server.registerCapabilities({ @@ -526,7 +526,7 @@ export class McpServer { const templateResources: Resource[] = []; for (const template of Object.values( - this._registeredResourceTemplates + this._registeredResourceTemplates, )) { if (!template.resourceTemplate.listCallback) { continue; @@ -543,14 +543,14 @@ export class McpServer { } return { resources: [...resources, ...templateResources] }; - } + }, ); this.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => { const resourceTemplates = Object.entries( - this._registeredResourceTemplates + this._registeredResourceTemplates, ).map(([name, template]) => ({ name, uriTemplate: template.resourceTemplate.uriTemplate.toString(), @@ -558,7 +558,7 @@ export class McpServer { })); return { resourceTemplates }; - } + }, ); this.server.setRequestHandler( @@ -572,7 +572,7 @@ export class McpServer { if (!resource.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Resource ${uri} disabled` + `Resource ${uri} disabled`, ); } return resource.readCallback(uri, extra); @@ -580,10 +580,10 @@ export class McpServer { // Then check templates for (const template of Object.values( - this._registeredResourceTemplates + this._registeredResourceTemplates, )) { const variables = template.resourceTemplate.uriTemplate.match( - uri.toString() + uri.toString(), ); if (variables) { return template.readCallback(uri, variables, extra); @@ -592,9 +592,9 @@ export class McpServer { throw new McpError( ErrorCode.InvalidParams, - `Resource ${uri} not found` + `Resource ${uri} not found`, ); - } + }, ); this.setCompletionRequestHandler(); @@ -610,10 +610,10 @@ export class McpServer { } this.server.assertCanSetRequestHandler( - ListPromptsRequestSchema.shape.method.value + ListPromptsRequestSchema.shape.method.value, ); this.server.assertCanSetRequestHandler( - GetPromptRequestSchema.shape.method.value + GetPromptRequestSchema.shape.method.value, ); this.server.registerCapabilities({ @@ -637,7 +637,7 @@ export class McpServer { : undefined, }; }), - }) + }), ); this.server.setRequestHandler( @@ -647,25 +647,25 @@ export class McpServer { if (!prompt) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${request.params.name} not found` + `Prompt ${request.params.name} not found`, ); } if (!prompt.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Prompt ${request.params.name} disabled` + `Prompt ${request.params.name} disabled`, ); } if (prompt.argsSchema) { const parseResult = await prompt.argsSchema.safeParseAsync( - request.params.arguments + request.params.arguments, ); if (!parseResult.success) { throw new McpError( ErrorCode.InvalidParams, - `Invalid arguments for prompt ${request.params.name}: ${parseResult.error.message}` + `Invalid arguments for prompt ${request.params.name}: ${parseResult.error.message}`, ); } @@ -676,7 +676,7 @@ export class McpServer { const cb = prompt.callback as PromptCallback; return await Promise.resolve(cb(extra)); } - } + }, ); this.setCompletionRequestHandler(); @@ -690,7 +690,7 @@ export class McpServer { resource( name: string, uri: string, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource; /** @@ -700,7 +700,7 @@ export class McpServer { name: string, uri: string, metadata: ResourceMetadata, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource; /** @@ -709,7 +709,7 @@ export class McpServer { resource( name: string, template: ResourceTemplate, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate; /** @@ -719,7 +719,7 @@ export class McpServer { name: string, template: ResourceTemplate, metadata: ResourceMetadata, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate; resource( @@ -746,7 +746,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback, ); this.setResourceRequestHandlers(); @@ -762,7 +762,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceTemplateCallback + readCallback as ReadResourceTemplateCallback, ); this.setResourceRequestHandlers(); @@ -779,19 +779,19 @@ export class McpServer { name: string, uriOrTemplate: string, config: ResourceMetadata, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource; registerResource( name: string, uriOrTemplate: ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate; registerResource( name: string, uriOrTemplate: string | ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceCallback | ReadResourceTemplateCallback + readCallback: ReadResourceCallback | ReadResourceTemplateCallback, ): RegisteredResource | RegisteredResourceTemplate { if (typeof uriOrTemplate === "string") { if (this._registeredResources[uriOrTemplate]) { @@ -803,7 +803,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback, ); this.setResourceRequestHandlers(); @@ -819,7 +819,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceTemplateCallback + readCallback as ReadResourceTemplateCallback, ); this.setResourceRequestHandlers(); @@ -833,7 +833,7 @@ export class McpServer { title: string | undefined, uri: string, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource { const registeredResource: RegisteredResource = { name, @@ -872,7 +872,7 @@ export class McpServer { title: string | undefined, template: ResourceTemplate, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate { const registeredResourceTemplate: RegisteredResourceTemplate = { resourceTemplate: template, @@ -912,7 +912,7 @@ export class McpServer { title: string | undefined, description: string | undefined, argsSchema: PromptArgsRawShape | undefined, - callback: PromptCallback + callback: PromptCallback, ): RegisteredPrompt { const registeredPrompt: RegisteredPrompt = { title, @@ -953,7 +953,7 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, - callback: ToolCallback + callback: ToolCallback, ): RegisteredTool { const registeredTool: RegisteredTool = { title, @@ -1017,7 +1017,7 @@ export class McpServer { tool( name: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -1032,7 +1032,7 @@ export class McpServer { name: string, description: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -1042,7 +1042,7 @@ export class McpServer { name: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -1053,7 +1053,7 @@ export class McpServer { description: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -1113,7 +1113,7 @@ export class McpServer { inputSchema, outputSchema, annotations, - callback + callback, ); } @@ -1129,7 +1129,7 @@ export class McpServer { outputSchema?: OutputArgs; annotations?: ToolAnnotations; }, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); @@ -1145,7 +1145,7 @@ export class McpServer { inputSchema, outputSchema, annotations, - cb as ToolCallback + cb as ToolCallback, ); } @@ -1160,7 +1160,7 @@ export class McpServer { prompt( name: string, description: string, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt; /** @@ -1169,7 +1169,7 @@ export class McpServer { prompt( name: string, argsSchema: Args, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt; /** @@ -1179,7 +1179,7 @@ export class McpServer { name: string, description: string, argsSchema: Args, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt; prompt(name: string, ...rest: unknown[]): RegisteredPrompt { @@ -1203,7 +1203,7 @@ export class McpServer { undefined, description, argsSchema, - cb + cb, ); this.setPromptRequestHandlers(); @@ -1222,7 +1222,7 @@ export class McpServer { description?: string; argsSchema?: Args; }, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); @@ -1235,7 +1235,7 @@ export class McpServer { title, description, argsSchema, - cb as PromptCallback + cb as PromptCallback, ); this.setPromptRequestHandlers(); @@ -1287,7 +1287,7 @@ export type CompleteResourceTemplateCallback = ( value: string, context?: { arguments?: Record; - } + }, ) => string[] | Promise; /** @@ -1311,7 +1311,7 @@ export class ResourceTemplate { complete?: { [variable: string]: CompleteResourceTemplateCallback; }; - } + }, ) { this._uriTemplate = typeof uriTemplate === "string" @@ -1337,7 +1337,7 @@ export class ResourceTemplate { * Gets the callback for completing a specific URI template variable, if one was provided. */ completeCallback( - variable: string + variable: string, ): CompleteResourceTemplateCallback | undefined { return this._callbacks.complete?.[variable]; } @@ -1357,10 +1357,10 @@ export type ToolCallback = Args extends ZodRawShape ? ( args: z.infer>, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => CallToolResult | Promise : ( - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => CallToolResult | Promise; export type RegisteredTool = { @@ -1375,7 +1375,7 @@ export type RegisteredTool = { disable(): void; update< InputArgs extends ZodRawShape, - OutputArgs extends ZodRawShape + OutputArgs extends ZodRawShape, >(updates: { name?: string | null; title?: string; @@ -1425,7 +1425,7 @@ export type ResourceMetadata = Omit; * Callback to list all resources matching a given template. */ export type ListResourcesCallback = ( - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => ListResourcesResult | Promise; /** @@ -1433,7 +1433,7 @@ export type ListResourcesCallback = ( */ export type ReadResourceCallback = ( uri: URL, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => ReadResourceResult | Promise; export type RegisteredResource = { @@ -1461,7 +1461,7 @@ export type RegisteredResource = { export type ReadResourceTemplateCallback = ( uri: URL, variables: Variables, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => ReadResourceResult | Promise; export type RegisteredResourceTemplate = { @@ -1486,14 +1486,14 @@ export type RegisteredResourceTemplate = { type PromptArgsRawShape = Record>; export type PromptCallback< - Args extends undefined | PromptArgsRawShape = undefined + Args extends undefined | PromptArgsRawShape = undefined, > = Args extends PromptArgsRawShape ? ( args: z.infer>, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => GetPromptResult | Promise : ( - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => GetPromptResult | Promise; export type RegisteredPrompt = { @@ -1516,14 +1516,14 @@ export type RegisteredPrompt = { }; function promptArgumentsFromSchema( - schema: ZodObject + schema: ZodObject, ): PromptArgument[] { return Object.entries(schema.shape).map( ([name, field]): PromptArgument => ({ name, description: field.description, required: !field.isOptional(), - }) + }), ); } From aca892b39f1797cf451db9c0026d7b0b933528eb Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Wed, 13 Aug 2025 13:35:02 -0700 Subject: [PATCH 6/7] chore: skip custom json schema generation --- src/server/mcp.ts | 153 +--------------------------------------------- 1 file changed, 2 insertions(+), 151 deletions(-) diff --git a/src/server/mcp.ts b/src/server/mcp.ts index ee32b08cf..2890d7269 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -46,155 +46,6 @@ import { UriTemplate, Variables } from "../shared/uriTemplate.js"; import { RequestHandlerExtra } from "../shared/protocol.js"; import { Transport } from "../shared/transport.js"; -// Minimal JSON Schema generation for Zod v4 -type JsonSchema = { - type: "object" | "string" | "number" | "boolean" | "integer"; - properties?: Record; - required?: string[]; - enum?: string[]; -}; - -function zodToJsonSchema(schema: ZodObject): JsonSchema { - const shape = schema.shape; - const properties: NonNullable = {}; - const required: string[] = []; - - for (const [key, value] of Object.entries(shape)) { - const [unwrapped, isOptional] = unwrapOptional(value as ZodType); - const propSchema = toJsonForAny(unwrapped); - if (propSchema) { - properties[key] = propSchema; - } else { - // Fallback to empty object schema for unsupported types - properties[key] = {} as unknown as JsonSchema; - } - if (!isOptional) required.push(key); - } - - const result: JsonSchema = { type: "object", properties }; - if (required.length > 0) result.required = required; - return result; -} - -function unwrapOptional(s: ZodType): [ZodType, boolean] { - // Zod v4 Optional has unwrap(); fall back to heuristic if unavailable - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const anySchema = s as any; - if (typeof anySchema.isOptional === "function" && anySchema.isOptional()) { - if (typeof anySchema.unwrap === "function") { - return [anySchema.unwrap(), true]; - } - // Best-effort: try _def.innerType if present - if (anySchema._def?.innerType) { - return [anySchema._def.innerType as ZodType, true]; - } - return [s, true]; - } - return [s, false]; -} - -function toJsonForAny( - s: ZodType, -): JsonSchema | { enum: string[]; type?: "string" } | undefined { - // Prefer Zod v4 built-in conversion when available, then normalize - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const anySchema = s as any; - if (typeof anySchema.toJsonSchema === "function") { - try { - const js = anySchema.toJsonSchema(); - const normalized = normalizeJsonFragment(js); - if (normalized) return normalized; - } catch { - // fall through to manual mapping - } - } - - // Manual mapping for common primitives and objects - if (isInstanceOf(anySchema, z.ZodString)) return { type: "string" }; - if (isInstanceOf(anySchema, z.ZodBoolean)) return { type: "boolean" }; - if (isInstanceOf(anySchema, z.ZodNumber)) return { type: "number" }; - if (isInstanceOf(anySchema, z.ZodObject)) - return zodToJsonSchema(anySchema as ZodObject); - // Enums - if (isInstanceOf(anySchema, z.ZodEnum)) { - const values = extractEnumValues(anySchema); - if (values) return { type: "string", enum: values }; - } - return undefined; -} - -function extractEnumValues(s: unknown): string[] | undefined { - // Try well-known locations across Zod versions - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const anySchema = s as any; - if (Array.isArray(anySchema.options)) return anySchema.options as string[]; - if (Array.isArray(anySchema.values)) return anySchema.values as string[]; - if (Array.isArray(anySchema._def?.values)) - return anySchema._def.values as string[]; - return undefined; -} - -// Avoid `any` in constructor typing for linter compliance -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isInstanceOf( - value: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ctor: new (...args: any[]) => T, -): value is T { - // Avoid crashes across dual bundles by checking name and prototype - return ( - typeof value === "object" && - value !== null && - (value as object) instanceof ctor - ); -} - -function normalizeJsonFragment( - js: unknown, -): JsonSchema | { enum: string[]; type?: "string" } | undefined { - if (!js || typeof js !== "object") return undefined; - // If this already looks like a primitive schema - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const frag = js as any; - if ( - frag.type === "string" || - frag.type === "number" || - frag.type === "boolean" || - frag.type === "integer" - ) { - const res: JsonSchema = { type: frag.type }; - if (Array.isArray(frag.enum)) res.enum = frag.enum; - return res; - } - if (Array.isArray(frag.enum)) { - return { - type: typeof frag.type === "string" ? frag.type : "string", - enum: frag.enum, - }; - } - if ( - frag.type === "object" && - frag.properties && - typeof frag.properties === "object" - ) { - const properties: Record< - string, - JsonSchema | { enum: string[]; type?: "string" } - > = {}; - for (const [k, v] of Object.entries( - frag.properties as Record, - )) { - const nv = normalizeJsonFragment(v); - if (nv) properties[k] = nv; - else properties[k] = {} as unknown as JsonSchema; - } - const out: JsonSchema = { type: "object", properties }; - if (Array.isArray(frag.required) && frag.required.length > 0) - out.required = frag.required.slice(); - return out; - } - return undefined; -} /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. @@ -265,13 +116,13 @@ export class McpServer { title: tool.title, description: tool.description, inputSchema: tool.inputSchema - ? (zodToJsonSchema(tool.inputSchema) as Tool["inputSchema"]) + ? z.toJSONSchema(tool.inputSchema) as Tool["inputSchema"] : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, }; if (tool.outputSchema) { - toolDefinition.outputSchema = zodToJsonSchema( + toolDefinition.outputSchema = z.toJSONSchema( tool.outputSchema, ) as Tool["outputSchema"]; } From 8531d8e2f3d1a4a6b0cf6c944e1517f0e64108c8 Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Tue, 19 Aug 2025 14:24:43 -0700 Subject: [PATCH 7/7] chore: use default instead of "prefault" --- src/examples/server/simpleSseServer.ts | 4 ++-- src/examples/server/simpleStatelessStreamableHttp.ts | 4 ++-- src/examples/server/simpleStreamableHttp.ts | 4 ++-- src/examples/server/sseAndStreamableHttpCompatibleServer.ts | 4 ++-- src/integration-tests/stateManagementStreamableHttp.test.ts | 2 +- src/integration-tests/taskResumability.test.ts | 6 +++--- src/types.ts | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index bae583f7a..4b34a41b6 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -25,8 +25,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications', { - interval: z.number().describe('Interval in milliseconds between notifications').prefault(1000), - count: z.number().describe('Number of notifications to send').prefault(10), + interval: z.number().describe('Interval in milliseconds between notifications').default(1000), + count: z.number().describe('Number of notifications to send').default(10), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index 365700933..e41b059e0 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -39,8 +39,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').prefault(100), - count: z.number().describe('Number of notifications to send (0 for 100)').prefault(10), + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(10), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index ca19adb39..c347aa879 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -270,8 +270,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').prefault(100), - count: z.number().describe('Number of notifications to send (0 for 100)').prefault(50), + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(50), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index 89107a009..653cc8b16 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -30,8 +30,8 @@ const getServer = () => { 'start-notification-stream', 'Starts sending periodic notifications for testing resumability', { - interval: z.number().describe('Interval in milliseconds between notifications').prefault(100), - count: z.number().describe('Number of notifications to send (0 for 100)').prefault(50), + interval: z.number().describe('Interval in milliseconds between notifications').default(100), + count: z.number().describe('Number of notifications to send (0 for 100)').default(50), }, async ({ interval, count }, { sendNotification }): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/integration-tests/stateManagementStreamableHttp.test.ts b/src/integration-tests/stateManagementStreamableHttp.test.ts index d2e3d56b0..48ceac2e8 100644 --- a/src/integration-tests/stateManagementStreamableHttp.test.ts +++ b/src/integration-tests/stateManagementStreamableHttp.test.ts @@ -55,7 +55,7 @@ describe('Streamable HTTP Transport Session Management', () => { 'greet', 'A simple greeting tool', { - name: z.string().describe('Name to greet').prefault('World'), + name: z.string().describe('Name to greet').default('World'), }, async ({ name }) => { return { diff --git a/src/integration-tests/taskResumability.test.ts b/src/integration-tests/taskResumability.test.ts index 9802e0582..e5402b95e 100644 --- a/src/integration-tests/taskResumability.test.ts +++ b/src/integration-tests/taskResumability.test.ts @@ -33,7 +33,7 @@ describe('Transport resumability', () => { 'send-notification', 'Sends a single notification', { - message: z.string().describe('Message to send').prefault('Test notification') + message: z.string().describe('Message to send').default('Test notification') }, async ({ message }, { sendNotification }) => { // Send notification immediately @@ -56,8 +56,8 @@ describe('Transport resumability', () => { 'run-notifications', 'Sends multiple notifications over time', { - count: z.number().describe('Number of notifications to send').prefault(10), - interval: z.number().describe('Interval between notifications in ms').prefault(50) + count: z.number().describe('Number of notifications to send').default(10), + interval: z.number().describe('Interval between notifications in ms').default(50) }, async ({ count, interval }, { sendNotification }) => { // Send notifications at specified intervals diff --git a/src/types.ts b/src/types.ts index 8144d9fcd..ccf75b4f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -916,7 +916,7 @@ export const CallToolResultSchema = ResultSchema.extend({ * If the Tool does not define an outputSchema, this field MUST be present in the result. * For backwards compatibility, this field is always present, but it may be empty. */ - content: z.array(ContentBlockSchema).prefault([]), + content: z.array(ContentBlockSchema).default([]), /** * An object containing structured tool output.