From 44c9bffa8511372b760dc5edb78650563053b18f Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Sun, 23 Feb 2025 12:39:15 -0500 Subject: [PATCH 01/14] added session id to request handler extra data --- src/server/sse.ts | 2 -- src/shared/protocol.ts | 13 ++++++++++++- src/shared/transport.ts | 2 ++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/server/sse.ts b/src/server/sse.ts index 84c1cbb9..1bc304a8 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -136,8 +136,6 @@ export class SSEServerTransport implements Transport { /** * Returns the session ID for this transport. - * - * This can be used to route incoming POST requests. */ get sessionId(): string { return this._sessionId; diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index a4f211c6..ef303a39 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -74,6 +74,11 @@ export type RequestHandlerExtra = { * An abort signal used to communicate if the request was cancelled from the sender's side. */ signal: AbortSignal; + + /** + * The session ID from the transport, if available. + */ + sessionId?: string; }; /** @@ -239,9 +244,15 @@ export abstract class Protocol< const abortController = new AbortController(); this._requestHandlerAbortControllers.set(request.id, abortController); + // Create extra object with both abort signal and sessionId from transport + const extra: RequestHandlerExtra = { + signal: abortController.signal, + sessionId: this._transport?.sessionId, + }; + // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() - .then(() => handler(request, { signal: abortController.signal })) + .then(() => handler(request, extra)) .then( (result) => { if (abortController.signal.aborted) { diff --git a/src/shared/transport.ts b/src/shared/transport.ts index 5843cf00..bcadcf4d 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -41,4 +41,6 @@ export interface Transport { * Callback for when a message (request or response) is received over the connection. */ onmessage?: (message: JSONRPCMessage) => void; + + sessionId?: string; } From 0408fdbb6284d4609703fcbc374082f653200ae7 Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Mon, 24 Feb 2025 11:06:34 -0500 Subject: [PATCH 02/14] put missing comment back --- src/server/sse.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/sse.ts b/src/server/sse.ts index 1bc304a8..84c1cbb9 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -136,6 +136,8 @@ export class SSEServerTransport implements Transport { /** * Returns the session ID for this transport. + * + * This can be used to route incoming POST requests. */ get sessionId(): string { return this._sessionId; From 92da5861f4fded8f21315cb18dbe620535214000 Mon Sep 17 00:00:00 2001 From: Tuomas Date: Tue, 25 Feb 2025 11:30:50 -0600 Subject: [PATCH 03/14] Fix resetTimeoutOnProgress option not being checked --- src/shared/protocol.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 97213bf0..ccfcdbc1 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -98,6 +98,7 @@ type TimeoutInfo = { startTime: number; timeout: number; maxTotalTimeout?: number; + resetTimeoutOnProgress: boolean; onTimeout: () => void; }; @@ -179,13 +180,15 @@ export abstract class Protocol< messageId: number, timeout: number, maxTotalTimeout: number | undefined, - onTimeout: () => void + onTimeout: () => void, + resetTimeoutOnProgress: boolean = false ) { this._timeoutInfo.set(messageId, { timeoutId: setTimeout(onTimeout, timeout), startTime: Date.now(), timeout, maxTotalTimeout, + resetTimeoutOnProgress, onTimeout }); } @@ -358,7 +361,9 @@ export abstract class Protocol< } const responseHandler = this._responseHandlers.get(messageId); - if (this._timeoutInfo.has(messageId) && responseHandler) { + const timeoutInfo = this._timeoutInfo.get(messageId); + + if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) { try { this._resetTimeout(messageId); } catch (error) { @@ -520,7 +525,7 @@ export abstract class Protocol< { timeout } )); - this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler); + this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false); this._transport.send(jsonrpcRequest).catch((error) => { this._cleanupTimeout(messageId); From d63324e34e07cd32feeb919bf3a9565e2051cf90 Mon Sep 17 00:00:00 2001 From: Tuomas Date: Tue, 25 Feb 2025 11:32:24 -0600 Subject: [PATCH 04/14] Fix resetTimeoutOnProgress option not being checked, add test --- src/shared/protocol.test.ts | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/shared/protocol.test.ts b/src/shared/protocol.test.ts index 1d037b98..fb5ecd13 100644 --- a/src/shared/protocol.test.ts +++ b/src/shared/protocol.test.ts @@ -71,6 +71,44 @@ describe("protocol tests", () => { jest.useRealTimers(); }); + test("should not reset timeout when resetTimeoutOnProgress is false", async () => { + await protocol.connect(transport); + const request = { method: "example", params: {} }; + const mockSchema: ZodType<{ result: string }> = z.object({ + result: z.string(), + }); + const onProgressMock = jest.fn(); + const requestPromise = protocol.request(request, mockSchema, { + timeout: 1000, + resetTimeoutOnProgress: false, + onprogress: onProgressMock, + }); + + jest.advanceTimersByTime(800); + + if (transport.onmessage) { + transport.onmessage({ + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: 0, + progress: 50, + total: 100, + }, + }); + } + await Promise.resolve(); + + expect(onProgressMock).toHaveBeenCalledWith({ + progress: 50, + total: 100, + }); + + jest.advanceTimersByTime(201); + + await expect(requestPromise).rejects.toThrow("Request timed out"); + }); + test("should reset timeout when progress notification is received", async () => { await protocol.connect(transport); const request = { method: "example", params: {} }; From 57f3b07bb9a827ab97089bcbeeca89f56b0c009c Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Tue, 25 Feb 2025 17:48:14 -0500 Subject: [PATCH 05/14] docstring --- src/shared/transport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/transport.ts b/src/shared/transport.ts index bcadcf4d..b80e2a51 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -42,5 +42,8 @@ export interface Transport { */ onmessage?: (message: JSONRPCMessage) => void; + /** + * The session ID generated for this connection. + */ sessionId?: string; } From c396b4af1daea1afd144514348d75d76c93a4d86 Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Tue, 25 Feb 2025 17:57:45 -0500 Subject: [PATCH 06/14] added test for session id using in memory transport --- src/inMemory.ts | 1 + src/server/mcp.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/inMemory.ts b/src/inMemory.ts index 2763f38c..106a9e7e 100644 --- a/src/inMemory.ts +++ b/src/inMemory.ts @@ -11,6 +11,7 @@ export class InMemoryTransport implements Transport { onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; + sessionId?: string; /** * Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a Client and one to a Server. diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 8f9bfa77..2e91a568 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -323,6 +323,59 @@ describe("tool()", () => { mcpServer.tool("tool2", () => ({ content: [] })); }); + test("should pass sessionId to tool callback via RequestHandlerExtra", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + let receivedSessionId: string | undefined; + mcpServer.tool("test-tool", async (extra) => { + receivedSessionId = extra.sessionId; + return { + content: [ + { + type: "text", + text: "Test response", + }, + ], + }; + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + // Set a test sessionId on the server transport + serverTransport.sessionId = "test-session-123"; + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + await client.request( + { + method: "tools/call", + params: { + name: "test-tool", + }, + }, + CallToolResultSchema, + ); + + expect(receivedSessionId).toBe("test-session-123"); + }); + test("should allow client to call server tools", async () => { const mcpServer = new McpServer({ name: "test server", From 0fa2397174eba309b54575294d56754c52b13a65 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Tue, 11 Mar 2025 10:54:21 +0000 Subject: [PATCH 07/14] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81104cb3..b7641c07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.6.1", + "version": "1.7.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 3c0081f2ecd8bb9b117330783ad25c2ec127ef60 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 15 Mar 2025 13:22:15 +0000 Subject: [PATCH 08/14] add improve transport in SSE server example --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fe4caa3f..a959ae3d 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ await server.connect(transport); For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to: ```typescript -import express from "express"; +import express, { Request, Response } from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; @@ -224,16 +224,27 @@ const server = new McpServer({ const app = express(); -app.get("/sse", async (req, res) => { - const transport = new SSEServerTransport("/messages", res); +// to support multiple simultaneous connections we have a lookup object from +// sessionId to transport +const transports: {[sessionId: string]: SSEServerTransport} = {}; + +app.get("/sse", async (_: Request, res: Response) => { + const transport = new SSEServerTransport('/messages', res); + transports[transport.sessionId] = transport; + res.on("close", () => { + delete transports[transport.sessionId]; + }); await server.connect(transport); }); -app.post("/messages", async (req, res) => { - // Note: to support multiple simultaneous connections, these messages will - // need to be routed to a specific matching transport. (This logic isn't - // implemented here, for simplicity.) - await transport.handlePostMessage(req, res); +app.post("/messages", async (req: Request, res: Response) => { + const sessionId = req.query.sessionId as string; + const transport = transports[sessionId]; + if (transport) { + await transport.handlePostMessage(req, res); + } else { + res.status(400).send('No transport found for sessionId'); + } }); app.listen(3001); From d4bf73b06e404d75c974d5d526cb0acf4e9a56a4 Mon Sep 17 00:00:00 2001 From: zhibisora Date: Sun, 16 Mar 2025 05:00:36 +0800 Subject: [PATCH 09/14] Replace native spawn with cross-spawn for improved cross-platform compatibility --- package.json | 1 + src/client/cross-spawn.test.ts | 112 +++++++++++++++++++++++++++++++++ src/client/stdio.ts | 3 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/client/cross-spawn.test.ts diff --git a/package.json b/package.json index b7641c07..51c4e0cd 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", diff --git a/src/client/cross-spawn.test.ts b/src/client/cross-spawn.test.ts new file mode 100644 index 00000000..d898aeb6 --- /dev/null +++ b/src/client/cross-spawn.test.ts @@ -0,0 +1,112 @@ +import { StdioClientTransport } from "./stdio.js"; +import spawn from "cross-spawn"; + +// mock cross-spawn +jest.mock("cross-spawn"); +const mockSpawn = spawn as jest.MockedFunction; + +describe("StdioClientTransport using cross-spawn", () => { + beforeEach(() => { + // mock cross-spawn's return value + mockSpawn.mockImplementation(() => { + const mockProcess = { + on: jest.fn((event: string, callback: Function) => { + if (event === "spawn") { + callback(); + } + return mockProcess; + }), + stdin: { + on: jest.fn(), + write: jest.fn().mockReturnValue(true) + }, + stdout: { + on: jest.fn() + }, + stderr: null + }; + return mockProcess as any; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should call cross-spawn correctly", async () => { + const transport = new StdioClientTransport({ + command: "test-command", + args: ["arg1", "arg2"] + }); + + await transport.start(); + + // verify spawn is called correctly + expect(mockSpawn).toHaveBeenCalledWith( + "test-command", + ["arg1", "arg2"], + expect.objectContaining({ + shell: false + }) + ); + }); + + test("should pass environment variables correctly", async () => { + const customEnv = { TEST_VAR: "test-value" }; + const transport = new StdioClientTransport({ + command: "test-command", + env: customEnv + }); + + await transport.start(); + + // verify environment variables are passed correctly + expect(mockSpawn).toHaveBeenCalledWith( + "test-command", + [], + expect.objectContaining({ + env: customEnv + }) + ); + }); + + test("should send messages correctly", async () => { + const transport = new StdioClientTransport({ + command: "test-command" + }); + + // get the mock process object + const mockProcess = { + on: jest.fn((event, callback) => { + if (event === "spawn") { + callback(); + } + return mockProcess; + }), + stdin: { + on: jest.fn(), + write: jest.fn().mockReturnValue(true), + once: jest.fn() + }, + stdout: { + on: jest.fn() + }, + stderr: null + }; + + mockSpawn.mockReturnValue(mockProcess as any); + + await transport.start(); + + const message = { + jsonrpc: "2.0", + id: "test-id", + method: "test-method" + }; + + await transport.send(message); + + // verify message is sent correctly + expect(mockProcess.stdin.write).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/client/stdio.ts b/src/client/stdio.ts index 07e9a196..b83bf27c 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -1,4 +1,5 @@ -import { ChildProcess, IOType, spawn } from "node:child_process"; +import { ChildProcess, IOType } from "node:child_process"; +import spawn from "cross-spawn"; import process from "node:process"; import { Stream } from "node:stream"; import { ReadBuffer, serializeMessage } from "../shared/stdio.js"; From 46ae2a17af4d331d3076071ed0033bfac0fbb9ef Mon Sep 17 00:00:00 2001 From: zhibisora Date: Sun, 16 Mar 2025 05:15:50 +0800 Subject: [PATCH 10/14] Fix test for cross-spawn --- package-lock.json | 24 +++++++++++++++--------- package.json | 1 + src/client/cross-spawn.test.ts | 28 +++++++++++++++++++++++----- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfd8a622..73f1cbba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.5.0", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.5.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", @@ -24,6 +25,7 @@ "@jest-mock/express": "^3.0.0", "@types/content-type": "^1.1.8", "@types/cors": "^2.8.17", + "@types/cross-spawn": "^6.0.6", "@types/eslint__js": "^8.42.3", "@types/eventsource": "^1.1.15", "@types/express": "^5.0.0", @@ -1693,6 +1695,16 @@ "@types/node": "*" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2809,7 +2821,6 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4156,8 +4167,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -5359,7 +5369,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5871,7 +5880,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5883,7 +5891,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -6480,7 +6487,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index 51c4e0cd..7302af05 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@jest-mock/express": "^3.0.0", "@types/content-type": "^1.1.8", "@types/cors": "^2.8.17", + "@types/cross-spawn": "^6.0.6", "@types/eslint__js": "^8.42.3", "@types/eventsource": "^1.1.15", "@types/express": "^5.0.0", diff --git a/src/client/cross-spawn.test.ts b/src/client/cross-spawn.test.ts index d898aeb6..ae0a4fd6 100644 --- a/src/client/cross-spawn.test.ts +++ b/src/client/cross-spawn.test.ts @@ -1,5 +1,6 @@ import { StdioClientTransport } from "./stdio.js"; import spawn from "cross-spawn"; +import { JSONRPCMessage } from "../types.js"; // mock cross-spawn jest.mock("cross-spawn"); @@ -9,8 +10,13 @@ describe("StdioClientTransport using cross-spawn", () => { beforeEach(() => { // mock cross-spawn's return value mockSpawn.mockImplementation(() => { - const mockProcess = { - on: jest.fn((event: string, callback: Function) => { + const mockProcess: { + on: jest.Mock; + stdin?: { on: jest.Mock; write: jest.Mock }; + stdout?: { on: jest.Mock }; + stderr?: null; + } = { + on: jest.fn((event: string, callback: () => void) => { if (event === "spawn") { callback(); } @@ -76,8 +82,19 @@ describe("StdioClientTransport using cross-spawn", () => { }); // get the mock process object - const mockProcess = { - on: jest.fn((event, callback) => { + const mockProcess: { + on: jest.Mock; + stdin: { + on: jest.Mock; + write: jest.Mock; + once: jest.Mock; + }; + stdout: { + on: jest.Mock; + }; + stderr: null; + } = { + on: jest.fn((event: string, callback: () => void) => { if (event === "spawn") { callback(); } @@ -98,7 +115,8 @@ describe("StdioClientTransport using cross-spawn", () => { await transport.start(); - const message = { + // 关键修复:确保 jsonrpc 是字面量 "2.0" + const message: JSONRPCMessage = { jsonrpc: "2.0", id: "test-id", method: "test-method" From 2a2392be2608a50a8773ca0ffea202fadfa6669e Mon Sep 17 00:00:00 2001 From: Peter Raboud Date: Tue, 25 Mar 2025 13:26:29 -0700 Subject: [PATCH 11/14] Correctly pass redirect_uri to tokens call --- src/client/auth.test.ts | 18 +++++++++++------- src/client/auth.ts | 4 ++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 02026c4f..cc4717ec 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -46,11 +46,11 @@ describe("OAuth Authorization", () => { it("returns metadata when first fetch fails but second without MCP header succeeds", async () => { // Set up a counter to control behavior let callCount = 0; - + // Mock implementation that changes behavior based on call count mockFetch.mockImplementation((_url, _options) => { callCount++; - + if (callCount === 1) { // First call with MCP header - fail with TypeError (simulating CORS error) // We need to use TypeError specifically because that's what the implementation checks for @@ -68,10 +68,10 @@ describe("OAuth Authorization", () => { // Should succeed with the second call const metadata = await discoverOAuthMetadata("https://auth.example.com"); expect(metadata).toEqual(validMetadata); - + // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); - + // Verify first call had MCP header expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version"); }); @@ -79,11 +79,11 @@ describe("OAuth Authorization", () => { it("throws an error when all fetch attempts fail", async () => { // Set up a counter to control behavior let callCount = 0; - + // Mock implementation that changes behavior based on call count mockFetch.mockImplementation((_url, _options) => { callCount++; - + if (callCount === 1) { // First call - fail with TypeError return Promise.reject(new TypeError("First failure")); @@ -96,7 +96,7 @@ describe("OAuth Authorization", () => { // Should fail with the second error await expect(discoverOAuthMetadata("https://auth.example.com")) .rejects.toThrow("Second failure"); - + // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); }); @@ -250,6 +250,7 @@ describe("OAuth Authorization", () => { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", }); expect(tokens).toEqual(validTokens); @@ -271,6 +272,7 @@ describe("OAuth Authorization", () => { expect(body.get("code_verifier")).toBe("verifier123"); expect(body.get("client_id")).toBe("client123"); expect(body.get("client_secret")).toBe("secret123"); + expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback"); }); it("validates token response schema", async () => { @@ -288,6 +290,7 @@ describe("OAuth Authorization", () => { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", }) ).rejects.toThrow(); }); @@ -303,6 +306,7 @@ describe("OAuth Authorization", () => { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", }) ).rejects.toThrow("Token exchange failed"); }); diff --git a/src/client/auth.ts b/src/client/auth.ts index c7799429..a279121b 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -115,6 +115,7 @@ export async function auth( clientInformation, authorizationCode, codeVerifier, + redirectUri: provider.redirectUrl, }); await provider.saveTokens(tokens); @@ -259,11 +260,13 @@ export async function exchangeAuthorization( clientInformation, authorizationCode, codeVerifier, + redirectUri, }: { metadata?: OAuthMetadata; clientInformation: OAuthClientInformation; authorizationCode: string; codeVerifier: string; + redirectUri: string | URL; }, ): Promise { const grantType = "authorization_code"; @@ -290,6 +293,7 @@ export async function exchangeAuthorization( client_id: clientInformation.client_id, code: authorizationCode, code_verifier: codeVerifier, + redirect_uri: String(redirectUri), }); if (clientInformation.client_secret) { From c6635d71d6685a92617ae1b7d7520e56868b2e72 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Wed, 26 Mar 2025 10:12:19 +0000 Subject: [PATCH 12/14] Fix lints --- src/client/cross-spawn.test.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/client/cross-spawn.test.ts b/src/client/cross-spawn.test.ts index ae0a4fd6..11e81bf6 100644 --- a/src/client/cross-spawn.test.ts +++ b/src/client/cross-spawn.test.ts @@ -1,6 +1,7 @@ import { StdioClientTransport } from "./stdio.js"; import spawn from "cross-spawn"; import { JSONRPCMessage } from "../types.js"; +import { ChildProcess } from "node:child_process"; // mock cross-spawn jest.mock("cross-spawn"); @@ -31,7 +32,7 @@ describe("StdioClientTransport using cross-spawn", () => { }, stderr: null }; - return mockProcess as any; + return mockProcess as unknown as ChildProcess; }); }); @@ -44,9 +45,9 @@ describe("StdioClientTransport using cross-spawn", () => { command: "test-command", args: ["arg1", "arg2"] }); - + await transport.start(); - + // verify spawn is called correctly expect(mockSpawn).toHaveBeenCalledWith( "test-command", @@ -63,9 +64,9 @@ describe("StdioClientTransport using cross-spawn", () => { command: "test-command", env: customEnv }); - + await transport.start(); - + // verify environment variables are passed correctly expect(mockSpawn).toHaveBeenCalledWith( "test-command", @@ -80,7 +81,7 @@ describe("StdioClientTransport using cross-spawn", () => { const transport = new StdioClientTransport({ command: "test-command" }); - + // get the mock process object const mockProcess: { on: jest.Mock; @@ -110,20 +111,20 @@ describe("StdioClientTransport using cross-spawn", () => { }, stderr: null }; - - mockSpawn.mockReturnValue(mockProcess as any); - + + mockSpawn.mockReturnValue(mockProcess as unknown as ChildProcess); + await transport.start(); - + // 关键修复:确保 jsonrpc 是字面量 "2.0" const message: JSONRPCMessage = { jsonrpc: "2.0", id: "test-id", method: "test-method" }; - + await transport.send(message); - + // verify message is sent correctly expect(mockProcess.stdin.write).toHaveBeenCalled(); }); From 784012946dbc28f69708061863da6733f91c6696 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Wed, 26 Mar 2025 10:15:46 +0000 Subject: [PATCH 13/14] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7302af05..e2d8b3d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.7.0", + "version": "1.8.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 9f5b4bf00d506eb5914e001773143e5408a35133 Mon Sep 17 00:00:00 2001 From: Nathan Arseneau Date: Tue, 1 Apr 2025 19:01:22 -0400 Subject: [PATCH 14/14] Add audio content --- src/types.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index d051d8f7..b9c0ab37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -666,6 +666,23 @@ export const ImageContentSchema = z }) .passthrough(); +/** + * An Audio provided to or from an LLM. + */ +export const AudioContentSchema = z + .object({ + type: z.literal("audio"), + /** + * The base64-encoded audio data. + */ + data: z.string().base64(), + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: z.string(), + }) + .passthrough(); + /** * The contents of a resource, embedded into a prompt or tool call result. */ @@ -685,6 +702,7 @@ export const PromptMessageSchema = z content: z.union([ TextContentSchema, ImageContentSchema, + AudioContentSchema, EmbeddedResourceSchema, ]), }) @@ -753,7 +771,7 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({ */ export const CallToolResultSchema = ResultSchema.extend({ content: z.array( - z.union([TextContentSchema, ImageContentSchema, EmbeddedResourceSchema]), + z.union([TextContentSchema, ImageContentSchema, AudioContentSchema, EmbeddedResourceSchema]), ), isError: z.boolean().default(false).optional(), }); @@ -877,7 +895,7 @@ export const ModelPreferencesSchema = z export const SamplingMessageSchema = z .object({ role: z.enum(["user", "assistant"]), - content: z.union([TextContentSchema, ImageContentSchema]), + content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), }) .passthrough(); @@ -931,6 +949,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({ content: z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, + AudioContentSchema ]), }); @@ -1195,6 +1214,7 @@ export type ListPromptsResult = Infer; export type GetPromptRequest = Infer; export type TextContent = Infer; export type ImageContent = Infer; +export type AudioContent = Infer; export type EmbeddedResource = Infer; export type PromptMessage = Infer; export type GetPromptResult = Infer;