From 4cd5e73a1146834ec3d5822b874ee93e0eea00c0 Mon Sep 17 00:00:00 2001 From: Jun Han Date: Sun, 20 Apr 2025 17:51:46 +0800 Subject: [PATCH 01/16] docs: add error handling when it fails to start HTTP server --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 200cfab6..8ddc117a 100644 --- a/README.md +++ b/README.md @@ -367,7 +367,11 @@ app.delete('/mcp', async (req: Request, res: Response) => { // Start the server const PORT = 3000; setupServer().then(() => { - app.listen(PORT, () => { + app.listen(PORT, (error) => { + if (error) { + console.error('Failed to start server:', error); + process.exit(1); + } console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); }); }).catch(error => { From 59fb4845d6e49bca6b63cb12a617872682d3133a Mon Sep 17 00:00:00 2001 From: Xiaofu Huang Date: Tue, 6 May 2025 17:25:38 +0800 Subject: [PATCH 02/16] fix: Expose the MCP child process PID as an accessible property in StdioClientTransport --- src/client/stdio.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/stdio.ts b/src/client/stdio.ts index 9e35293d..4f7f5f1b 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -184,6 +184,10 @@ export class StdioClientTransport implements Transport { return this._process?.stderr ?? null; } + get pid(): number | undefined { + return this._process?.pid; + } + private processReadBuffer() { while (true) { try { From 7e134f259edbc0e0af897afc4c52154351e208d4 Mon Sep 17 00:00:00 2001 From: Xiaofu Huang Date: Tue, 6 May 2025 17:38:26 +0800 Subject: [PATCH 03/16] test: add ut of chile process pod in StdioClientTransport --- src/client/stdio.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client/stdio.test.ts b/src/client/stdio.test.ts index 646f9ea5..96dd5648 100644 --- a/src/client/stdio.test.ts +++ b/src/client/stdio.test.ts @@ -59,3 +59,12 @@ test("should read messages", async () => { await client.close(); }); + +test("should return child process pid", async () => { + const client = new StdioClientTransport(serverParameters); + + await client.start(); + expect(client.pid).toBeDefined(); + await client.close(); + expect(client.pid).toBeUndefined(); +}); From d154914db74627bb70bbaf1842a9eb937e67a103 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 6 May 2025 10:13:09 +0000 Subject: [PATCH 04/16] fix: change pid return type from undefined to be null --- src/client/stdio.test.ts | 4 ++-- src/client/stdio.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/client/stdio.test.ts b/src/client/stdio.test.ts index 96dd5648..b2132446 100644 --- a/src/client/stdio.test.ts +++ b/src/client/stdio.test.ts @@ -64,7 +64,7 @@ test("should return child process pid", async () => { const client = new StdioClientTransport(serverParameters); await client.start(); - expect(client.pid).toBeDefined(); + expect(client.pid).not.toBeNull(); await client.close(); - expect(client.pid).toBeUndefined(); + expect(client.pid).toBeNull(); }); diff --git a/src/client/stdio.ts b/src/client/stdio.ts index 4f7f5f1b..af29614e 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -184,8 +184,13 @@ export class StdioClientTransport implements Transport { return this._process?.stderr ?? null; } - get pid(): number | undefined { - return this._process?.pid; + /** + * The child process pid spawned by this transport. + * + * This is only available after the transport has been started. + */ + get pid(): number | null { + return this._process?.pid ?? null; } private processReadBuffer() { From fd13cd93694e51f35ed38bee79f953f904bea505 Mon Sep 17 00:00:00 2001 From: Marco Pegoraro Date: Thu, 8 May 2025 13:14:30 +0000 Subject: [PATCH 05/16] doc minimum node version requirment --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c9102260..af3d2e29 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ The Model Context Protocol allows applications to provide context for LLMs in a npm install @modelcontextprotocol/sdk ``` +> ⚠️ MCP requires Node v18.x up to work fine. + ## Quick Start Let's create a simple MCP server that exposes a calculator tool and some data: From 921ce70fa3acf1cb626bc7ff5088f358727be335 Mon Sep 17 00:00:00 2001 From: sinedied Date: Mon, 9 Jun 2025 14:17:53 +0200 Subject: [PATCH 06/16] fix: missing "properties" property in empty schema --- src/server/mcp.test.ts | 1 + src/server/mcp.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 49f852d6..fd71443d 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -265,6 +265,7 @@ describe("tool()", () => { expect(result.tools[0].name).toBe("test"); expect(result.tools[0].inputSchema).toEqual({ type: "object", + properties: {}, }); // Adding the tool before the connection was established means no notification was sent diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 5b864b8b..436a948f 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1018,6 +1018,7 @@ export type RegisteredTool = { const EMPTY_OBJECT_JSON_SCHEMA = { type: "object" as const, + properties: {}, }; // Helper to check if an object is a Zod schema (ZodRawShape) From fc9ca5ff2efc7ee3f54484d2a906d5fdf528bae4 Mon Sep 17 00:00:00 2001 From: sushichan044 <71284054+sushichan044@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:47:53 +0900 Subject: [PATCH 07/16] fix: update ToolCallback type to include output arguments for better type safety --- src/server/mcp.test.ts | 3 +- src/server/mcp.ts | 68 +++++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 50df25b5..7e299bb0 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1248,8 +1248,7 @@ describe("tool()", () => { processedInput: input, resultType: "structured", // Missing required 'timestamp' field - someExtraField: "unexpected" // Extra field not in schema - }, + } as unknown as { processedInput: string; resultType: string; timestamp: string }, // Type assertion to bypass TypeScript validation for testing purposes }) ); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 3d9673da..e48970d6 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -169,7 +169,7 @@ export class McpServer { } const args = parseResult.data; - const cb = tool.callback as ToolCallback; + const cb = tool.callback as ToolCallback; try { result = await Promise.resolve(cb(args, extra)); } catch (error) { @@ -184,7 +184,7 @@ export class McpServer { }; } } else { - const cb = tool.callback as ToolCallback; + const cb = tool.callback as ToolCallback; try { result = await Promise.resolve(cb(extra)); } catch (error) { @@ -760,7 +760,7 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, - callback: ToolCallback + callback: ToolCallback ): RegisteredTool { const registeredTool: RegisteredTool = { title, @@ -917,7 +917,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`); @@ -932,7 +932,7 @@ export class McpServer { inputSchema, outputSchema, annotations, - cb as ToolCallback + cb as ToolCallback ); } @@ -1126,6 +1126,16 @@ export class ResourceTemplate { } } +/** + * Type helper to create a strongly-typed CallToolResult with structuredContent + */ +type TypedCallToolResult = + OutputArgs extends ZodRawShape + ? CallToolResult & { + structuredContent?: z.objectOutputType; + } + : CallToolResult; + /** * Callback for a tool handler registered with Server.tool(). * @@ -1136,13 +1146,21 @@ export class ResourceTemplate { * - `content` if the tool does not have an outputSchema * - Both fields are optional but typically one should be provided */ -export type ToolCallback = - Args extends ZodRawShape +export type ToolCallback< + InputArgs extends undefined | ZodRawShape = undefined, + OutputArgs extends undefined | ZodRawShape = undefined +> = InputArgs extends ZodRawShape ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; + args: z.objectOutputType, + extra: RequestHandlerExtra + ) => + | TypedCallToolResult + | Promise> + : ( + extra: RequestHandlerExtra + ) => + | TypedCallToolResult + | Promise>; export type RegisteredTool = { title?: string; @@ -1150,22 +1168,24 @@ export type RegisteredTool = { inputSchema?: AnyZodObject; outputSchema?: AnyZodObject; annotations?: ToolAnnotations; - callback: ToolCallback; + 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 = { From 08808a45e01d89366548b95af7c1a2159b474cf9 Mon Sep 17 00:00:00 2001 From: sushichan044 <71284054+sushichan044@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:48:05 +0900 Subject: [PATCH 08/16] test: add type assertion to bypass type check --- src/examples/server/mcpServerOutputSchema.ts | 4 ++-- src/server/mcp.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 75bfe690..dce2d42e 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -43,7 +43,7 @@ server.registerTool( void country; // Simulate weather API call const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10; - const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)]; + const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)] as unknown as "sunny" | "cloudy" | "rainy" | "stormy" | "snowy"; const structuredContent = { temperature: { @@ -77,4 +77,4 @@ async function main() { main().catch((error) => { console.error("Server error:", error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 7e299bb0..1a17d789 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1248,6 +1248,7 @@ describe("tool()", () => { processedInput: input, resultType: "structured", // Missing required 'timestamp' field + someExtraField: "unexpected" // Extra field not in schema } as unknown as { processedInput: string; resultType: string; timestamp: string }, // Type assertion to bypass TypeScript validation for testing purposes }) ); From 7a64f974936fd27faf063df8f9e952740bce9ad4 Mon Sep 17 00:00:00 2001 From: Rishi Nandha Vanchi <49914358+RishiNandha@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:08:15 +0530 Subject: [PATCH 09/16] Add: Sampling Example to README --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index aa8f9304..0d8d8f7c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - [Tools](#tools) - [Prompts](#prompts) - [Completions](#completions) + - [Sampling](#sampling) - [Running Your Server](#running-your-server) - [stdio](#stdio) - [Streamable HTTP](#streamable-http) @@ -382,6 +383,37 @@ import { getDisplayName } from "@modelcontextprotocol/sdk/shared/metadataUtils.j const displayName = getDisplayName(tool); ``` +### Sampling + +MCP servers can also request MCP client LLMs for responses. Below is an example of a sampling request sent just after connecting to the Client + +```typescript +// Result sent back from LLM follow the CreateMessageSchema +import {CreateMessageResult} from "@modelcontextprotocol/sdk/types.js"; + +// Async Function to send a sampling request to the LLM at top-level +async function samplingExample(server: McpServer): Promise { + const samplingText = "Text prompt to send to LLM"; + const result = await McpServer.server.createMessage( + { + messages : [{ + role: "user", + content: { + text: samplingText, + type: "text" + } + }], + maxTokens: 1000 + } + ); + return result; +} + +// Sampling request just after connecting to MCP Client +server.connect(transport); +samplingExample(server); +``` + ## Running Your Server MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: From b232faf7987852cd063ec1bda03cba533b5e324e Mon Sep 17 00:00:00 2001 From: Rishi Nandha Vanchi <49914358+RishiNandha@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:09:54 +0530 Subject: [PATCH 10/16] Fixed Tabs --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0d8d8f7c..4f7ed106 100644 --- a/README.md +++ b/README.md @@ -393,20 +393,20 @@ import {CreateMessageResult} from "@modelcontextprotocol/sdk/types.js"; // Async Function to send a sampling request to the LLM at top-level async function samplingExample(server: McpServer): Promise { - const samplingText = "Text prompt to send to LLM"; - const result = await McpServer.server.createMessage( - { - messages : [{ - role: "user", - content: { - text: samplingText, - type: "text" - } - }], - maxTokens: 1000 - } - ); - return result; + const samplingText = "Text prompt to send to LLM"; + const result = await McpServer.server.createMessage( + { + messages : [{ + role: "user", + content: { + text: samplingText, + type: "text" + } + }], + maxTokens: 1000 + } + ); + return result; } // Sampling request just after connecting to MCP Client From ca9924b20b52014d0b0e8a2fed143caf2d2e37c5 Mon Sep 17 00:00:00 2001 From: Rishi Nandha Vanchi <49914358+RishiNandha@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:10:57 +0530 Subject: [PATCH 11/16] Changed example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f7ed106..5e2afd06 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ import {CreateMessageResult} from "@modelcontextprotocol/sdk/types.js"; // Async Function to send a sampling request to the LLM at top-level async function samplingExample(server: McpServer): Promise { - const samplingText = "Text prompt to send to LLM"; + const samplingText = "Example Sampling Prompt"; const result = await McpServer.server.createMessage( { messages : [{ From fb303d748fe7b05b25d5bc938c43c3ae8871e778 Mon Sep 17 00:00:00 2001 From: sushichan044 <71284054+sushichan044@users.noreply.github.com> Date: Sat, 28 Jun 2025 14:56:02 +0900 Subject: [PATCH 12/16] tidying up --- src/examples/server/mcpServerOutputSchema.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index dce2d42e..de3b363e 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -43,7 +43,14 @@ server.registerTool( void country; // Simulate weather API call const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10; - const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)] as unknown as "sunny" | "cloudy" | "rainy" | "stormy" | "snowy"; + const conditionCandidates = [ + "sunny", + "cloudy", + "rainy", + "stormy", + "snowy", + ] as const; + const conditions = conditionCandidates[Math.floor(Math.random() * conditionCandidates.length)]; const structuredContent = { temperature: { From 3d6acd3d9ac50eaed334b5c96e16ee033f2abbd8 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 30 Jun 2025 16:23:19 +0100 Subject: [PATCH 13/16] add an example and update readme --- README.md | 75 +++++++++++++++------ src/examples/server/toolWithSampleServer.ts | 57 ++++++++++++++++ 2 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 src/examples/server/toolWithSampleServer.ts diff --git a/README.md b/README.md index 5e2afd06..f8143501 100644 --- a/README.md +++ b/README.md @@ -385,35 +385,66 @@ const displayName = getDisplayName(tool); ### Sampling -MCP servers can also request MCP client LLMs for responses. Below is an example of a sampling request sent just after connecting to the Client +MCP servers can request LLM completions from connected clients that support sampling. ```typescript -// Result sent back from LLM follow the CreateMessageSchema -import {CreateMessageResult} from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; -// Async Function to send a sampling request to the LLM at top-level -async function samplingExample(server: McpServer): Promise { - const samplingText = "Example Sampling Prompt"; - const result = await McpServer.server.createMessage( - { - messages : [{ - role: "user", - content: { - text: samplingText, - type: "text" - } - }], - maxTokens: 1000 - } - ); - return result; +const mcpServer = new McpServer({ + name: "tools-with-sample-server", + version: "1.0.0", +}); + +// Tool that uses LLM sampling to summarize any text +mcpServer.registerTool( + "summarize", + { + description: "Summarize any text using an LLM", + inputSchema: { + text: z.string().describe("Text to summarize"), + }, + }, + async ({ text }) => { + // Call the LLM through MCP sampling + const response = await mcpServer.server.createMessage({ + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please summarize the following text concisely:\n\n${text}`, + }, + }, + ], + maxTokens: 500, + }); + + return { + content: [ + { + type: "text", + text: response.content.type === "text" ? response.content.text : "Unable to generate summary", + }, + ], + }; + } +); + +async function main() { + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + console.log("MCP server is running..."); } -// Sampling request just after connecting to MCP Client -server.connect(transport); -samplingExample(server); +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); ``` + ## Running Your Server MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: diff --git a/src/examples/server/toolWithSampleServer.ts b/src/examples/server/toolWithSampleServer.ts new file mode 100644 index 00000000..7e92f33b --- /dev/null +++ b/src/examples/server/toolWithSampleServer.ts @@ -0,0 +1,57 @@ + +// Run with: npx tsx src/examples/server/toolWithSampleServer.ts + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +const mcpServer = new McpServer({ + name: "tools-with-sample-server", + version: "1.0.0", +}); + +// Tool that uses LLM sampling to summarize any text +mcpServer.registerTool( + "summarize", + { + description: "Summarize any text using an LLM", + inputSchema: { + text: z.string().describe("Text to summarize"), + }, + }, + async ({ text }) => { + // Call the LLM through MCP sampling + const response = await mcpServer.server.createMessage({ + messages: [ + { + role: "user", + content: { + type: "text", + text: `Please summarize the following text concisely:\n\n${text}`, + }, + }, + ], + maxTokens: 500, + }); + + return { + content: [ + { + type: "text", + text: response.content.type === "text" ? response.content.text : "Unable to generate summary", + }, + ], + }; + } +); + +async function main() { + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + console.log("MCP server is running..."); +} + +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); \ No newline at end of file From a8fa0b342ac0411d0827e48820400e5d048baa45 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Mon, 30 Jun 2025 16:25:26 +0100 Subject: [PATCH 14/16] build --- src/examples/server/toolWithSampleServer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/server/toolWithSampleServer.ts b/src/examples/server/toolWithSampleServer.ts index 7e92f33b..44e5cecb 100644 --- a/src/examples/server/toolWithSampleServer.ts +++ b/src/examples/server/toolWithSampleServer.ts @@ -1,8 +1,8 @@ // Run with: npx tsx src/examples/server/toolWithSampleServer.ts -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { McpServer } from "../../server/mcp.js"; +import { StdioServerTransport } from "../../server/stdio.js"; import { z } from "zod"; const mcpServer = new McpServer({ From 6f5e53b44b7c3a7db80d2f204e713c820e627492 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Wed, 2 Jul 2025 01:24:27 +1000 Subject: [PATCH 15/16] Returning undefined from `discoverOAuthMetadata` for CORS errors (#717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Returning undefined from `discoverOAuthMetadata` for CORS errors This behaviour was already happening for the root URL, but the new `fetchWithCorsRetry` logic differed. The issue was if the server returns a 404 for `/.well-known/oauth-authorization-server/xyz` that didn't have the `access-control-allow-origin`, a TypeError was being thrown. There was logic there already to handle a TypeError for a _preflight_ request (cause by custom headers), but not the fallback. I refactored so all combinations return `undefined`. * Add test for CORS error handling that should return undefined This test covers the scenario where both the initial request with headers and the retry without headers fail with CORS TypeErrors. The desired behavior is to return undefined instead of throwing. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix test comment --------- Co-authored-by: Glen Maddern Co-authored-by: Paul Carleton Co-authored-by: Claude Co-authored-by: Paul Carleton --- src/client/auth.test.ts | 13 ++++++++++++ src/client/auth.ts | 47 +++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 8e77c0a5..8155e134 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -403,6 +403,19 @@ describe("OAuth Authorization", () => { expect(mockFetch).toHaveBeenCalledTimes(2); }); + it("returns undefined when both CORS requests fail in fetchWithCorsRetry", async () => { + // fetchWithCorsRetry tries with headers (fails with CORS), then retries without headers (also fails with CORS) + // simulating a 404 w/o headers set. We want this to return undefined, not throw TypeError + mockFetch.mockImplementation(() => { + // Both the initial request with headers and retry without headers fail with CORS TypeError + return Promise.reject(new TypeError("Failed to fetch")); + }); + + // This should return undefined (the desired behavior after the fix) + const metadata = await discoverOAuthMetadata("https://auth.example.com/path"); + expect(metadata).toBeUndefined(); + }); + it("returns undefined when discovery endpoint returns 404", async () => { mockFetch.mockResolvedValueOnce({ ok: false, diff --git a/src/client/auth.ts b/src/client/auth.ts index 37690574..71101a42 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -292,25 +292,24 @@ export async function discoverOAuthProtectedResourceMetadata( return OAuthProtectedResourceMetadataSchema.parse(await response.json()); } -/** - * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata. - * - * If the server returns a 404 for the well-known endpoint, this function will - * return `undefined`. Any other errors will be thrown as exceptions. - */ /** * Helper function to handle fetch with CORS retry logic */ async function fetchWithCorsRetry( url: URL, - headers: Record, -): Promise { + headers?: Record, +): Promise { try { return await fetch(url, { headers }); } catch (error) { - // CORS errors come back as TypeError, retry without headers if (error instanceof TypeError) { - return await fetch(url); + if (headers) { + // CORS errors come back as TypeError, retry without headers + return fetchWithCorsRetry(url) + } else { + // We're getting CORS errors on retry too, return undefined + return undefined + } } throw error; } @@ -334,7 +333,7 @@ function buildWellKnownPath(pathname: string): string { async function tryMetadataDiscovery( url: URL, protocolVersion: string, -): Promise { +): Promise { const headers = { "MCP-Protocol-Version": protocolVersion }; @@ -344,10 +343,16 @@ async function tryMetadataDiscovery( /** * Determines if fallback to root discovery should be attempted */ -function shouldAttemptFallback(response: Response, pathname: string): boolean { - return response.status === 404 && pathname !== '/'; +function shouldAttemptFallback(response: Response | undefined, pathname: string): boolean { + return !response || response.status === 404 && pathname !== '/'; } +/** + * Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata. + * + * If the server returns a 404 for the well-known endpoint, this function will + * return `undefined`. Any other errors will be thrown as exceptions. + */ export async function discoverOAuthMetadata( authorizationServerUrl: string | URL, opts?: { protocolVersion?: string }, @@ -362,18 +367,10 @@ export async function discoverOAuthMetadata( // If path-aware discovery fails with 404, try fallback to root discovery if (shouldAttemptFallback(response, issuer.pathname)) { - try { - const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer); - response = await tryMetadataDiscovery(rootUrl, protocolVersion); - - if (response.status === 404) { - return undefined; - } - } catch { - // If fallback fails, return undefined - return undefined; - } - } else if (response.status === 404) { + const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer); + response = await tryMetadataDiscovery(rootUrl, protocolVersion); + } + if (!response || response.status === 404) { return undefined; } From cfec7d9b39363495c9d5ca77e1b489f41bb262d3 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Tue, 1 Jul 2025 16:43:50 +0100 Subject: [PATCH 16/16] 1.13.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f7cb0f5..16b90a3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.13.2", + "version": "1.13.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.13.2", + "version": "1.13.3", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index 31bc3562..e5061966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.13.2", + "version": "1.13.3", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",