diff --git a/package.json b/package.json index f24053c5..fa6f9545 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.10.2", + "version": "1.11.1", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", 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/client/index.test.ts b/src/client/index.test.ts index 36dd6518..5b4f332f 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -165,6 +165,194 @@ 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), + ]); + + 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), + ]); + + 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, + }, + ); + + 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( { diff --git a/src/client/stdio.ts b/src/client/stdio.ts index cef12fd8..203404ae 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(); + } } /** @@ -162,15 +166,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; } 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; diff --git a/src/types.test.ts b/src/types.test.ts new file mode 100644 index 00000000..0fbc003d --- /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"); + }); + +}); diff --git a/src/types.ts b/src/types.ts index daf9921e..2ee0f752 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()),