From 3a9a6c944becba59da151d96c4a875c930121d34 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 25 Apr 2025 09:16:28 -0400 Subject: [PATCH 1/9] Update protocol version * This fixes #378 * In types.ts - set LATEST_PROTOCOL_VERSION to "2025-03-26" - add "2025-03-26" to SUPPORTED_PROTOCOL_VERSIONS * In auth.test.ts - in test "returns metadata when discovery succeeds" - expect "MCP-Protocol-Version" to be "2025-03-26" --- src/client/auth.test.ts | 4 ++-- src/types.ts | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index cc4717ec..629feab7 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -39,7 +39,7 @@ describe("OAuth Authorization", () => { const [url, options] = calls[0]; expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server"); expect(options.headers).toEqual({ - "MCP-Protocol-Version": "2024-11-05" + "MCP-Protocol-Version": "2025-03-26" }); }); @@ -478,4 +478,4 @@ describe("OAuth Authorization", () => { ).rejects.toThrow("Dynamic client registration failed"); }); }); -}); \ No newline at end of file +}); diff --git a/src/types.ts b/src/types.ts index 17b485f8..a2b906e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,9 @@ import { z, ZodTypeAny } from "zod"; -export const LATEST_PROTOCOL_VERSION = "2024-11-05"; +export const LATEST_PROTOCOL_VERSION = "2025-03-26"; export const SUPPORTED_PROTOCOL_VERSIONS = [ LATEST_PROTOCOL_VERSION, + "2024-11-05", "2024-10-07", ]; @@ -754,11 +755,11 @@ export const PromptListChangedNotificationSchema = NotificationSchema.extend({ /* Tools */ /** * Additional properties describing a Tool to clients. - * - * NOTE: all properties in ToolAnnotations are **hints**. - * They are not guaranteed to provide a faithful description of + * + * NOTE: all properties in ToolAnnotations are **hints**. + * They are not guaranteed to provide a faithful description of * tool behavior (including descriptive properties like `title`). - * + * * Clients should never make tool use decisions based on ToolAnnotations * received from untrusted servers. */ @@ -771,7 +772,7 @@ export const ToolAnnotationsSchema = z /** * If true, the tool does not modify its environment. - * + * * Default: false */ readOnlyHint: z.optional(z.boolean()), @@ -779,19 +780,19 @@ export const ToolAnnotationsSchema = z /** * 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 + * 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()), @@ -801,7 +802,7 @@ export const ToolAnnotationsSchema = z * 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()), From 1e9d5d548a4f095c7398e506d9fa7f18b34b30a4 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 25 Apr 2025 09:28:16 -0400 Subject: [PATCH 2/9] Update protocol version * Added types.test.ts - "should have correct latest protocol version" - "should have correct supported protocol versions" --- src/types.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/types.test.ts diff --git a/src/types.test.ts b/src/types.test.ts new file mode 100644 index 00000000..bdb5ffb3 --- /dev/null +++ b/src/types.test.ts @@ -0,0 +1,17 @@ +import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS} from "./types.js"; + +describe("Types", () => { + + test("should have correct latest protocol version", () => { + expect(LATEST_PROTOCOL_VERSION).toBeDefined(); + expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26"); + }); + test("should have correct supported protocol versions", () => { + expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined(); + expect(SUPPORTED_PROTOCOL_VERSIONS).toBeInstanceOf(Array); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-11-05"); + expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-10-07"); + }); + +}); From 84dc914231b91289401e446b16fb456b65c0a3e1 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 25 Apr 2025 09:29:29 -0400 Subject: [PATCH 3/9] Update protocol version * tidy types.test.ts --- src/types.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.test.ts b/src/types.test.ts index bdb5ffb3..0fbc003d 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -1,4 +1,4 @@ -import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS} from "./types.js"; +import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from "./types.js"; describe("Types", () => { From cada5a4266709351ef4259e8770e9b9df6e6d07f Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 28 Apr 2025 17:58:47 -0400 Subject: [PATCH 4/9] Add client/server version negotiation tests without mocks * In src/client/index.test.ts added tests: - "should connect new client to old, supported server version" - should negotiate version when client is old, and newer server supports its version" - "should throw when client is old, and server doesn't support its version" --- src/client/index.test.ts | 195 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 36dd6518..6e1c442f 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -165,6 +165,201 @@ test("should reject unsupported protocol version", async () => { expect(clientTransport.close).toHaveBeenCalled(); }); +test("should connect new client to old, supported server version", async () => { + const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + }, + ); + + server.setRequestHandler(InitializeRequestSchema, (_request) => ({ + protocolVersion: OLD_VERSION, + capabilities: { + resources: {}, + tools: {}, + }, + serverInfo: { + name: "old server", + version: "1.0", + }, + })); + + server.setRequestHandler(ListResourcesRequestSchema, () => ({ + resources: [], + })); + + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [], + })); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const client = new Client( + { + name: "new client", + version: "1.0", + protocolVersion: LATEST_PROTOCOL_VERSION, + }, + { + capabilities: { + sampling: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + // These should work + // Connection should succeed with the older version + expect(client.getServerVersion()).toEqual({ + name: "old server", + version: "1.0", + }); +}); + +test("should negotiate version when client is old, and newer server supports its version", async () => { + const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; + const server = new Server( + { + name: "new server", + version: "1.0", + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + }, + ); + + server.setRequestHandler(InitializeRequestSchema, (_request) => ({ + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: { + resources: {}, + tools: {}, + }, + serverInfo: { + name: "new server", + version: "1.0", + }, + })); + + server.setRequestHandler(ListResourcesRequestSchema, () => ({ + resources: [], + })); + + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [], + })); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const client = new Client( + { + name: "old client", + version: "1.0", + protocolVersion: OLD_VERSION, + }, + { + capabilities: { + sampling: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + // These should work + // Connection should succeed with the older version + expect(client.getServerVersion()).toEqual({ + name: "new server", + version: "1.0", + }); +}); + +test("should throw when client is old, and server doesn't support its version", async () => { + const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; + const FUTURE_VERSION = "FUTURE_VERSION"; + const server = new Server( + { + name: "new server", + version: "1.0", + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + }, + ); + + server.setRequestHandler(InitializeRequestSchema, (_request) => ({ + protocolVersion: FUTURE_VERSION, + capabilities: { + resources: {}, + tools: {}, + }, + serverInfo: { + name: "new server", + version: "1.0", + }, + })); + + server.setRequestHandler(ListResourcesRequestSchema, () => ({ + resources: [], + })); + + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [], + })); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const client = new Client( + { + name: "old client", + version: "1.0", + protocolVersion: OLD_VERSION, + }, + { + capabilities: { + sampling: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + let closed = false; + clientTransport.onerror = () => {closed = true}; + + await Promise.all([ + expect(client.connect(clientTransport)).rejects.toThrow( + "Server's protocol version is not supported: FUTURE_VERSION" + ), + server.connect(serverTransport), + ]); + +}); + test("should respect server capabilities", async () => { const server = new Server( { From d11673fc2d39743b13719104042500cbb58278c1 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 28 Apr 2025 18:01:14 -0400 Subject: [PATCH 5/9] Add client/server version negotiation tests without mocks * In src/client/index.test.ts added tests: - "should connect new client to old, supported server version" - should negotiate version when client is old, and newer server supports its version" - "should throw when client is old, and server doesn't support its version" --- src/client/index.test.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 6e1c442f..5b4f332f 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -222,8 +222,6 @@ test("should connect new client to old, supported server version", async () => { server.connect(serverTransport), ]); - // These should work - // Connection should succeed with the older version expect(client.getServerVersion()).toEqual({ name: "old server", version: "1.0", @@ -287,8 +285,6 @@ test("should negotiate version when client is old, and newer server supports its server.connect(serverTransport), ]); - // These should work - // Connection should succeed with the older version expect(client.getServerVersion()).toEqual({ name: "new server", version: "1.0", @@ -348,9 +344,6 @@ test("should throw when client is old, and server doesn't support its version", }, ); - let closed = false; - clientTransport.onerror = () => {closed = true}; - await Promise.all([ expect(client.connect(clientTransport)).rejects.toThrow( "Server's protocol version is not supported: FUTURE_VERSION" From def417f2df53d9d54ef6fd7f64be5a96c0930468 Mon Sep 17 00:00:00 2001 From: Henry Wildermuth Date: Wed, 30 Apr 2025 16:46:35 -0700 Subject: [PATCH 6/9] Add stderr access before start --- src/client/stdio.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/client/stdio.ts b/src/client/stdio.ts index b83bf27c..9e35293d 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -1,7 +1,7 @@ import { ChildProcess, IOType } from "node:child_process"; import spawn from "cross-spawn"; import process from "node:process"; -import { Stream } from "node:stream"; +import { Stream, PassThrough } from "node:stream"; import { ReadBuffer, serializeMessage } from "../shared/stdio.js"; import { Transport } from "../shared/transport.js"; import { JSONRPCMessage } from "../types.js"; @@ -93,6 +93,7 @@ export class StdioClientTransport implements Transport { private _abortController: AbortController = new AbortController(); private _readBuffer: ReadBuffer = new ReadBuffer(); private _serverParams: StdioServerParameters; + private _stderrStream: PassThrough | null = null; onclose?: () => void; onerror?: (error: Error) => void; @@ -100,6 +101,9 @@ export class StdioClientTransport implements Transport { constructor(server: StdioServerParameters) { this._serverParams = server; + if (server.stderr === "pipe" || server.stderr === "overlapped") { + this._stderrStream = new PassThrough(); + } } /** @@ -158,15 +162,25 @@ export class StdioClientTransport implements Transport { this._process.stdout?.on("error", (error) => { this.onerror?.(error); }); + + if (this._stderrStream && this._process.stderr) { + this._process.stderr.pipe(this._stderrStream); + } }); } /** * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped". * - * This is only available after the process has been started. + * If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to + * attach listeners before the start method is invoked. This prevents loss of any early + * error output emitted by the child process. */ get stderr(): Stream | null { + if (this._stderrStream) { + return this._stderrStream; + } + return this._process?.stderr ?? null; } From f250ea5f2b37199e9b4a382c98ca161999155176 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 1 May 2025 18:46:30 +0100 Subject: [PATCH 7/9] bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f24053c5..c6a65847 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.10.2", + "version": "1.11.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 52dc7f116080a143ff65a559a4f10a3420ec4d57 Mon Sep 17 00:00:00 2001 From: yuna0x0 Date: Tue, 6 May 2025 01:49:33 +0800 Subject: [PATCH 8/9] Fix isZodRawShape return false on empty object and add test --- src/server/mcp.test.ts | 47 ++++++++++++++++++++++++++++++++++++++++++ src/server/mcp.ts | 7 +++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index c9be5c76..5f50df68 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -622,6 +622,53 @@ describe("tool()", () => { }); }); + test("should register tool with description, empty params, and annotations", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.tool( + "test", + "A tool with everything but empty params", + {}, + { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, + async () => ({ + content: [{ type: "text", text: "Test response" }] + }) + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + const result = await client.request( + { method: "tools/list" }, + ListToolsResultSchema, + ); + + expect(result.tools).toHaveLength(1); + expect(result.tools[0].name).toBe("test"); + expect(result.tools[0].description).toBe("A tool with everything but empty params"); + expect(result.tools[0].inputSchema).toMatchObject({ + type: "object", + properties: {} + }); + expect(result.tools[0].annotations).toEqual({ + title: "Complete Test Tool with empty params", + readOnlyHint: true, + openWorldHint: false + }); + }); + test("should validate tool args", async () => { const mcpServer = new McpServer({ name: "test server", diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 6204eb2a..dccfa04e 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -663,8 +663,11 @@ export class McpServer { // Helper to check if an object is a Zod schema (ZodRawShape) const isZodRawShape = (obj: unknown): obj is ZodRawShape => { if (typeof obj !== "object" || obj === null) return false; - // Check that at least one property is a ZodType instance - return Object.values(obj as object).some(v => v instanceof ZodType); + + const isEmptyObject = z.object({}).strict().safeParse(obj).success; + + // Check if object is empty or at least one property is a ZodType instance + return isEmptyObject || Object.values(obj as object).some(v => v instanceof ZodType); }; let description: string | undefined; From 51ce07fada7d1cdedaf4ca28820a3911ae1329ec Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 8 May 2025 18:05:27 +0100 Subject: [PATCH 9/9] bump version to 1.11.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6a65847..fa6f9545 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.11.0", + "version": "1.11.1", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",