From 70ddef28f2f6255e317cf0c9eeb38842955dcfbd Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Fri, 12 Sep 2025 22:23:32 -0400 Subject: [PATCH 1/4] Fix OAuth redirect URI mismatch in debug flow Register both normal and debug redirect URIs in client metadata to prevent mismatch errors when using the Auth tab's debug mode versus normal connection flow. --- client/src/lib/auth.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index c1505398..0a8d26cf 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -117,9 +117,14 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { return window.location.origin + "/oauth/callback"; } + get debugRedirectUrl() { + return window.location.origin + "/oauth/callback/debug"; + } + get clientMetadata(): OAuthClientMetadata { + // Register both redirect URIs to support both normal and debug flows return { - redirect_uris: [this.redirectUrl], + redirect_uris: [this.redirectUrl, this.debugRedirectUrl], token_endpoint_auth_method: "none", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], @@ -223,11 +228,13 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } } -// Overrides debug URL and allows saving server OAuth metadata to +// Overrides redirect URL to use the debug endpoint and allows saving server OAuth metadata to // display in debug UI. export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider { get redirectUrl(): string { - return `${window.location.origin}/oauth/callback/debug`; + // We can use the debug redirect URL here because it was already registered + // in the parent class's clientMetadata along with the normal redirect URL + return this.debugRedirectUrl; } saveServerMetadata(metadata: OAuthMetadata) { From 51d28cd7b4701d715bf3957239682cae2ac9a945 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 15 Sep 2025 18:09:44 -0400 Subject: [PATCH 2/4] When the client sends a request, do not pass on the mcp-session-id header for the Client <-> Proxy connection. This causes a problem for stateless SSE or StreamableHttp connections. --- server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index af3ea928..7e966d34 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -53,8 +53,8 @@ const getHttpHeaders = (req: express.Request): Record => { lowerKey === "authorization" || lowerKey === "last-event-id" ) { - // Exclude the proxy's own authentication header - if (lowerKey !== "x-mcp-proxy-auth") { + // Exclude the proxy's own authentication header and the Client <-> Proxy session ID header + if (lowerKey !== "x-mcp-proxy-auth" && lowerKey !== "mcp-session-id") { const value = req.headers[key]; if (typeof value === "string") { From 7b338b3ea3fa209f3d8bea94241a4c1694cb5180 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 03:39:40 +0000 Subject: [PATCH 3/4] chore: bump version to 0.16.8 Co-authored-by: Ola Hungerford --- cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 16 ++++++++-------- package.json | 8 ++++---- server/package.json | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/package.json b/cli/package.json index caf21a3e..065f9bd6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.7", + "version": "0.16.8", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/package.json b/client/package.json index db4358fd..7a4c2cf0 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.7", + "version": "0.16.8", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/package-lock.json b/package-lock.json index d422148b..31234e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.7", + "version": "0.16.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.16.7", + "version": "0.16.8", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.16.7", - "@modelcontextprotocol/inspector-client": "^0.16.7", - "@modelcontextprotocol/inspector-server": "^0.16.7", + "@modelcontextprotocol/inspector-cli": "^0.16.8", + "@modelcontextprotocol/inspector-client": "^0.16.8", + "@modelcontextprotocol/inspector-server": "^0.16.8", "@modelcontextprotocol/sdk": "^1.18.0", "concurrently": "^9.2.0", "open": "^10.2.0", @@ -46,7 +46,7 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.7", + "version": "0.16.8", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.18.0", @@ -69,7 +69,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.7", + "version": "0.16.8", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.18.0", @@ -11783,7 +11783,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.7", + "version": "0.16.8", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.18.0", diff --git a/package.json b/package.json index 48609a20..918a2222 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.7", + "version": "0.16.8", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -47,9 +47,9 @@ "check-version": "node scripts/check-version-consistency.js" }, "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.16.7", - "@modelcontextprotocol/inspector-client": "^0.16.7", - "@modelcontextprotocol/inspector-server": "^0.16.7", + "@modelcontextprotocol/inspector-cli": "^0.16.8", + "@modelcontextprotocol/inspector-client": "^0.16.8", + "@modelcontextprotocol/inspector-server": "^0.16.8", "@modelcontextprotocol/sdk": "^1.18.0", "concurrently": "^9.2.0", "open": "^10.2.0", diff --git a/server/package.json b/server/package.json index 528bee70..b6b9a9b2 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.7", + "version": "0.16.8", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From f1e93e0475aeac2ebd2c841dcac42be1f0054abf Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 17 Sep 2025 14:45:58 -0400 Subject: [PATCH 4/4] * In server/index.ts - in createCustomFetch function - convert finalHeaders to headers object for node-fetch compatibility - for SSE streams, convert node stream to response body to web ReadableStream since EventSource polyfill expects web-compatible stream * In package-lock.json and package.json - add node-fetch as a dependency This fixes #600 --- package-lock.json | 92 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + server/src/index.ts | 72 +++++++++++++++++++++++++++++++++-- 3 files changed, 161 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d422148b..92a40ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@modelcontextprotocol/inspector-server": "^0.16.7", "@modelcontextprotocol/sdk": "^1.18.0", "concurrently": "^9.2.0", + "node-fetch": "^3.3.2", "open": "^10.2.0", "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", @@ -5103,6 +5104,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -5970,6 +5980,29 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6154,6 +6187,18 @@ "node": ">= 0.6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8716,6 +8761,44 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11504,6 +11587,15 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 48609a20..a1f704a2 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@modelcontextprotocol/inspector-server": "^0.16.7", "@modelcontextprotocol/sdk": "^1.18.0", "concurrently": "^9.2.0", + "node-fetch": "^3.3.2", "open": "^10.2.0", "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", diff --git a/server/src/index.ts b/server/src/index.ts index 7e966d34..88954ebc 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,6 +3,11 @@ import cors from "cors"; import { parseArgs } from "node:util"; import { parse as shellParseArgs } from "shell-quote"; +import nodeFetch, { Headers as NodeHeaders } from "node-fetch"; + +// Type-compatible wrappers for node-fetch to work with browser-style types +const fetch = nodeFetch; +const Headers = NodeHeaders; import { SSEClientTransport, @@ -231,13 +236,37 @@ const authMiddleware = ( next(); }; +/** + * Converts a Node.js ReadableStream to a web-compatible ReadableStream + * This is necessary for the EventSource polyfill which expects web streams + */ +const createWebReadableStream = (nodeStream: any): ReadableStream => { + return new ReadableStream({ + start(controller) { + nodeStream.on("data", (chunk: any) => { + controller.enqueue(chunk); + }); + nodeStream.on("end", () => { + controller.close(); + }); + nodeStream.on("error", (err: any) => { + controller.error(err); + }); + }, + }); +}; + /** * Creates a `fetch` function that merges dynamic session headers with the * headers from the actual request, ensuring that request-specific headers like - * `Content-Type` are preserved. + * `Content-Type` are preserved. For SSE requests, it also converts Node.js + * streams to web-compatible streams. */ const createCustomFetch = (headerHolder: { headers: HeadersInit }) => { - return (input: RequestInfo | URL, init?: RequestInit): Promise => { + return async ( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise => { // Determine the headers from the original request/init. // The SDK may pass a Request object or a URL and an init object. const originalHeaders = @@ -252,8 +281,43 @@ const createCustomFetch = (headerHolder: { headers: HeadersInit }) => { finalHeaders.set(key, value); }); - // This works for both `fetch(url, init)` and `fetch(request)` style calls. - return fetch(input, { ...init, headers: finalHeaders }); + // Convert Headers to a plain object for node-fetch compatibility + const headersObject: Record = {}; + finalHeaders.forEach((value, key) => { + headersObject[key] = value; + }); + + // Get the response from node-fetch (cast input and init to handle type differences) + const response = await fetch( + input as any, + { ...init, headers: headersObject } as any, + ); + + // Check if this is an SSE request by looking at the Accept header + const acceptHeader = finalHeaders.get("Accept"); + const isSSE = acceptHeader?.includes("text/event-stream"); + + if (isSSE && response.body) { + // For SSE requests, we need to convert the Node.js stream to a web ReadableStream + // because the EventSource polyfill expects web-compatible streams + const webStream = createWebReadableStream(response.body); + + // Create a new response with the web-compatible stream + // Convert node-fetch headers to plain object for web Response compatibility + const responseHeaders: Record = {}; + response.headers.forEach((value: string, key: string) => { + responseHeaders[key] = value; + }); + + return new Response(webStream, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }) as Response; + } + + // For non-SSE requests, return the response as-is (cast to handle type differences) + return response as unknown as Response; }; };