From d16e89df1ecfe0f92741dfcb565b9a30ed2f5e0a Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 29 Apr 2025 21:32:21 -0700 Subject: [PATCH 01/23] Preserve original URL --- cli/src/transport.ts | 4 ++++ client/src/lib/hooks/useConnection.ts | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cli/src/transport.ts b/cli/src/transport.ts index e693f246..5df44f30 100644 --- a/cli/src/transport.ts +++ b/cli/src/transport.ts @@ -15,7 +15,11 @@ export type TransportOptions = { function createSSETransport(options: TransportOptions): Transport { const baseUrl = new URL(options.url ?? ""); + // Create new URL while preserving the port from baseUrl const sseUrl = new URL("/sse", baseUrl); + if (baseUrl.port) { + sseUrl.port = baseUrl.port; + } return new SSEClientTransport(sseUrl); } diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 27bc11a2..98ae0447 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -280,6 +280,8 @@ export function useConnection({ return; } let mcpProxyServerUrl; + const originalUrl = new URL(sseUrl); + switch (transportType) { case "stdio": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`); @@ -290,12 +292,12 @@ export function useConnection({ case "sse": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`); - mcpProxyServerUrl.searchParams.append("url", sseUrl); + mcpProxyServerUrl.searchParams.append("url", originalUrl.toString()); break; case "streamable-http": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`); - mcpProxyServerUrl.searchParams.append("url", sseUrl); + mcpProxyServerUrl.searchParams.append("url", originalUrl.toString()); break; } (mcpProxyServerUrl as URL).searchParams.append( From 0838ffde38098ecd84699d94dfd767caa6af3766 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 30 Apr 2025 05:03:00 -0700 Subject: [PATCH 02/23] Update tests --- .../hooks/__tests__/useConnection.test.tsx | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index e191d6c0..debab46d 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -28,10 +28,18 @@ jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ })); jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: jest.fn(), + SSEClientTransport: jest.fn((url) => ({ + toString: () => url, + })), SseError: jest.fn(), })); +jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + StreamableHTTPClientTransport: jest.fn((url) => ({ + toString: () => url, + })), +})); + jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ auth: jest.fn().mockResolvedValue("AUTHORIZED"), })); @@ -163,4 +171,82 @@ describe("useConnection", () => { result.current.makeRequest(mockRequest, mockSchema), ).rejects.toThrow("MCP client not connected"); }); + + describe("URL Port Handling", () => { + const SSEClientTransport = jest.requireMock("@modelcontextprotocol/sdk/client/sse.js").SSEClientTransport; + const StreamableHTTPClientTransport = jest.requireMock("@modelcontextprotocol/sdk/client/streamableHttp.js").StreamableHTTPClientTransport; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("preserves HTTPS port number when connecting", async () => { + const props = { + ...defaultProps, + sseUrl: "https://example.com:8443/api", + transportType: "sse" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = SSEClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%3A8443%2Fapi"); + }); + + test("preserves HTTP port number when connecting", async () => { + const props = { + ...defaultProps, + sseUrl: "http://localhost:3000/api", + transportType: "sse" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = SSEClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain("url=http%3A%2F%2Flocalhost%3A3000%2Fapi"); + }); + + test("uses default port for HTTPS when not specified", async () => { + const props = { + ...defaultProps, + sseUrl: "https://example.com/api", + transportType: "sse" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = SSEClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%2Fapi"); + expect(call.toString()).not.toContain("%3A443"); + }); + + test("preserves port number in streamable-http transport", async () => { + const props = { + ...defaultProps, + sseUrl: "https://example.com:8443/api", + transportType: "streamable-http" as const, + }; + + const { result } = renderHook(() => useConnection(props)); + + await act(async () => { + await result.current.connect(); + }); + + const call = StreamableHTTPClientTransport.mock.calls[0][0]; + expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%3A8443%2Fapi"); + }); + }); }); From 7bf363cca9ac425a7b301580962bbbefada1b8f3 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 30 Apr 2025 05:05:23 -0700 Subject: [PATCH 03/23] Remove unnecessary comment --- cli/src/transport.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/transport.ts b/cli/src/transport.ts index 5df44f30..f3a1bec9 100644 --- a/cli/src/transport.ts +++ b/cli/src/transport.ts @@ -15,7 +15,6 @@ export type TransportOptions = { function createSSETransport(options: TransportOptions): Transport { const baseUrl = new URL(options.url ?? ""); - // Create new URL while preserving the port from baseUrl const sseUrl = new URL("/sse", baseUrl); if (baseUrl.port) { sseUrl.port = baseUrl.port; From fe827426ae748b3c5cc816d5c28bd3309e72a1a4 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 30 Apr 2025 05:16:00 -0700 Subject: [PATCH 04/23] Revert re-parsing url --- client/src/lib/hooks/useConnection.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 98ae0447..34388b8e 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -280,7 +280,6 @@ export function useConnection({ return; } let mcpProxyServerUrl; - const originalUrl = new URL(sseUrl); switch (transportType) { case "stdio": @@ -292,12 +291,12 @@ export function useConnection({ case "sse": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`); - mcpProxyServerUrl.searchParams.append("url", originalUrl.toString()); + mcpProxyServerUrl.searchParams.append("url", sseUrl); break; case "streamable-http": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`); - mcpProxyServerUrl.searchParams.append("url", originalUrl.toString()); + mcpProxyServerUrl.searchParams.append("url", sseUrl); break; } (mcpProxyServerUrl as URL).searchParams.append( From f641aa047099eae4cd1d5b7d6c77b36dbffedf0a Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 30 Apr 2025 05:16:42 -0700 Subject: [PATCH 05/23] Revert re-parsing url --- client/src/lib/hooks/useConnection.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 34388b8e..27bc11a2 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -280,7 +280,6 @@ export function useConnection({ return; } let mcpProxyServerUrl; - switch (transportType) { case "stdio": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`); From 24e1aaefd170572031bb6b4c23c840395e8d27cb Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 30 Apr 2025 06:09:35 -0700 Subject: [PATCH 06/23] Remove unnecessary explicit port setting --- cli/src/transport.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/cli/src/transport.ts b/cli/src/transport.ts index f3a1bec9..e693f246 100644 --- a/cli/src/transport.ts +++ b/cli/src/transport.ts @@ -16,9 +16,6 @@ export type TransportOptions = { function createSSETransport(options: TransportOptions): Transport { const baseUrl = new URL(options.url ?? ""); const sseUrl = new URL("/sse", baseUrl); - if (baseUrl.port) { - sseUrl.port = baseUrl.port; - } return new SSEClientTransport(sseUrl); } From 7611227fd4be86e9fb78342e8640740d17b5e009 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 30 Apr 2025 06:19:44 -0700 Subject: [PATCH 07/23] Fix formatting --- .../hooks/__tests__/useConnection.test.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index debab46d..d1dae99d 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -173,8 +173,12 @@ describe("useConnection", () => { }); describe("URL Port Handling", () => { - const SSEClientTransport = jest.requireMock("@modelcontextprotocol/sdk/client/sse.js").SSEClientTransport; - const StreamableHTTPClientTransport = jest.requireMock("@modelcontextprotocol/sdk/client/streamableHttp.js").StreamableHTTPClientTransport; + const SSEClientTransport = jest.requireMock( + "@modelcontextprotocol/sdk/client/sse.js", + ).SSEClientTransport; + const StreamableHTTPClientTransport = jest.requireMock( + "@modelcontextprotocol/sdk/client/streamableHttp.js", + ).StreamableHTTPClientTransport; beforeEach(() => { jest.clearAllMocks(); @@ -194,7 +198,9 @@ describe("useConnection", () => { }); const call = SSEClientTransport.mock.calls[0][0]; - expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%3A8443%2Fapi"); + expect(call.toString()).toContain( + "url=https%3A%2F%2Fexample.com%3A8443%2Fapi", + ); }); test("preserves HTTP port number when connecting", async () => { @@ -211,7 +217,9 @@ describe("useConnection", () => { }); const call = SSEClientTransport.mock.calls[0][0]; - expect(call.toString()).toContain("url=http%3A%2F%2Flocalhost%3A3000%2Fapi"); + expect(call.toString()).toContain( + "url=http%3A%2F%2Flocalhost%3A3000%2Fapi", + ); }); test("uses default port for HTTPS when not specified", async () => { @@ -246,7 +254,9 @@ describe("useConnection", () => { }); const call = StreamableHTTPClientTransport.mock.calls[0][0]; - expect(call.toString()).toContain("url=https%3A%2F%2Fexample.com%3A8443%2Fapi"); + expect(call.toString()).toContain( + "url=https%3A%2F%2Fexample.com%3A8443%2Fapi", + ); }); }); }); From 89ab90df81c4886c02cf3e853aa540760c6fa473 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:58:43 +0000 Subject: [PATCH 08/23] Bump vite in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 6.3.0 to 6.3.4 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.3.4 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a10da69..57863e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7768,13 +7768,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -7785,9 +7785,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8225,18 +8225,18 @@ } }, "node_modules/vite": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.0.tgz", - "integrity": "sha512-9aC0n4pr6hIbvi1YOpFjwQ+QOTGssvbJKoeYkuHHGWwlXfdxQlI8L2qNMo9awEEcCPSiS+5mJZk5jH1PAqoDeQ==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", - "tinyglobby": "^0.2.12" + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -8300,9 +8300,9 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { From 618f24a9acb25222b01dec19e773362bb36c7abb Mon Sep 17 00:00:00 2001 From: xiaoli Date: Tue, 13 May 2025 23:38:19 +0800 Subject: [PATCH 09/23] feat: support custom authorization headers, fix #395 --- client/src/lib/hooks/useConnection.ts | 7 ++++++- server/src/index.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 4ced90e4..501c8359 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -297,7 +297,12 @@ export function useConnection({ bearerToken || (await serverAuthProvider.tokens())?.access_token; if (token) { const authHeaderName = headerName || "Authorization"; - headers[authHeaderName] = `Bearer ${token}`; + headers[authHeaderName] = token; + + // Add custom header name as a special request header to let the server know which header to pass through + if (headerName && headerName.toLowerCase() !== "authorization") { + headers["x-custom-auth-header"] = headerName; + } } // Create appropriate transport diff --git a/server/src/index.ts b/server/src/index.ts index c967b60c..766d2df4 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -91,6 +91,15 @@ const createTransport = async (req: express.Request): Promise => { const value = req.headers[key]; headers[key] = Array.isArray(value) ? value[value.length - 1] : value; } + + // If the header "x-custom-auth-header" is present, use its value as the custom header name. + if (req.headers["x-custom-auth-header"] !== undefined) { + const customHeaderName = req.headers["x-custom-auth-header"] as string; + if (req.headers[customHeaderName.toLowerCase()] !== undefined) { + const value = req.headers[customHeaderName.toLowerCase()]; + headers[customHeaderName] = Array.isArray(value) ? value[value.length - 1] : value as string; + } + } console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`); @@ -119,6 +128,15 @@ const createTransport = async (req: express.Request): Promise => { const value = req.headers[key]; headers[key] = Array.isArray(value) ? value[value.length - 1] : value; } + + // If the header "x-custom-auth-header" is present, use its value as the custom header name. + if (req.headers["x-custom-auth-header"] !== undefined) { + const customHeaderName = req.headers["x-custom-auth-header"] as string; + if (req.headers[customHeaderName.toLowerCase()] !== undefined) { + const value = req.headers[customHeaderName.toLowerCase()]; + headers[customHeaderName] = Array.isArray(value) ? value[value.length - 1] : value as string; + } + } const transport = new StreamableHTTPClientTransport( new URL(query.url as string), From 13f736033a98b4502af2b7d47cf630fde1e72b3a Mon Sep 17 00:00:00 2001 From: xiaoli Date: Tue, 20 May 2025 13:10:55 +0800 Subject: [PATCH 10/23] fix: fix review comments - Avoid calling .lowerCase twice - Keep 'Bearer ' prefix when header equals 'Authorization' - Remove unnecessary Array type checks --- client/src/lib/hooks/useConnection.ts | 10 +++++++--- server/src/index.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 501c8359..f1f7ab9e 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -297,11 +297,15 @@ export function useConnection({ bearerToken || (await serverAuthProvider.tokens())?.access_token; if (token) { const authHeaderName = headerName || "Authorization"; - headers[authHeaderName] = token; // Add custom header name as a special request header to let the server know which header to pass through - if (headerName && headerName.toLowerCase() !== "authorization") { - headers["x-custom-auth-header"] = headerName; + if (authHeaderName.toLowerCase() !== "authorization") { + headers[authHeaderName] = token; + if (headerName) { + headers["x-custom-auth-header"] = headerName; + } + } else { + headers[authHeaderName] = `Bearer ${token}`; } } diff --git a/server/src/index.ts b/server/src/index.ts index 766d2df4..fd28a807 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -95,9 +95,9 @@ const createTransport = async (req: express.Request): Promise => { // If the header "x-custom-auth-header" is present, use its value as the custom header name. if (req.headers["x-custom-auth-header"] !== undefined) { const customHeaderName = req.headers["x-custom-auth-header"] as string; - if (req.headers[customHeaderName.toLowerCase()] !== undefined) { - const value = req.headers[customHeaderName.toLowerCase()]; - headers[customHeaderName] = Array.isArray(value) ? value[value.length - 1] : value as string; + if (req.headers[customHeaderName] !== undefined) { + const value = req.headers[customHeaderName]; + headers[customHeaderName] = value as string; } } @@ -132,9 +132,9 @@ const createTransport = async (req: express.Request): Promise => { // If the header "x-custom-auth-header" is present, use its value as the custom header name. if (req.headers["x-custom-auth-header"] !== undefined) { const customHeaderName = req.headers["x-custom-auth-header"] as string; - if (req.headers[customHeaderName.toLowerCase()] !== undefined) { - const value = req.headers[customHeaderName.toLowerCase()]; - headers[customHeaderName] = Array.isArray(value) ? value[value.length - 1] : value as string; + if (req.headers[customHeaderName] !== undefined) { + const value = req.headers[customHeaderName]; + headers[customHeaderName] = value as string; } } From f04470ace7080f53233d1387e2dbb4d232bd5f7d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 21 May 2025 17:45:19 -0400 Subject: [PATCH 11/23] Allow multiple client connections to the MCP server, by making a unique server tranpsort for each web app transport. * In server/src/index.ts - add serverTransports map - remove backingServerTransport var - in /mcp POST handler, when a new connection is being made - create a server transport - map the server transport using the session id from the web app connection - pass serverTransport to the mcpProxy instead of backingServerTransport - in /stdio GET handler, when a new connection is being made - create a server transport - map the server transport using the session id from the web app connection - pass serverTransport to the mcpProxy instead of backingServerTransport - in /sse GET handler, when a new connection is being made - create a server transport - map the server transport using the session id from the web app connection - pass serverTransport to the mcpProxy instead of backingServerTransport --- server/src/index.ts | 57 +++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index c967b60c..8bc8a53c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -48,7 +48,8 @@ app.use((req, res, next) => { next(); }); -const webAppTransports: Map = new Map(); // Transports by sessionId +const webAppTransports: Map = new Map(); // Web app transports by web app sessionId +const serverTransports: Map = new Map(); // Server Transports by web app sessionId const createTransport = async (req: express.Request): Promise => { const query = req.query; @@ -137,8 +138,6 @@ const createTransport = async (req: express.Request): Promise => { } }; -let backingServerTransport: Transport | undefined; - app.get("/mcp", async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string; console.log(`Received GET message for sessionId ${sessionId}`); @@ -161,12 +160,12 @@ app.get("/mcp", async (req, res) => { app.post("/mcp", async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; console.log(`Received POST message for sessionId ${sessionId}`); + let serverTransport: Transport | undefined; if (!sessionId) { try { console.log("New streamable-http connection"); try { - await backingServerTransport?.close(); - backingServerTransport = await createTransport(req); + serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -180,12 +179,13 @@ app.post("/mcp", async (req, res) => { throw error; } - console.log("Connected MCP client to backing server transport"); + console.log("Connected MCP client to server transport"); const webAppTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID, onsessioninitialized: (sessionId) => { webAppTransports.set(sessionId, webAppTransport); + serverTransports.set(sessionId, serverTransport!); console.log("Created streamable web app transport " + sessionId); }, }); @@ -194,7 +194,7 @@ app.post("/mcp", async (req, res) => { mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); await (webAppTransport as StreamableHTTPServerTransport).handleRequest( @@ -229,10 +229,9 @@ app.post("/mcp", async (req, res) => { app.get("/stdio", async (req, res) => { try { console.log("New connection"); - + let serverTransport: Transport | undefined; try { - await backingServerTransport?.close(); - backingServerTransport = await createTransport(req); + serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -250,26 +249,24 @@ app.get("/stdio", async (req, res) => { const webAppTransport = new SSEServerTransport("/message", res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); - - console.log("Created web app transport"); + serverTransports.set(webAppTransport.sessionId, serverTransport); + console.log("Created client/server transports"); await webAppTransport.start(); - (backingServerTransport as StdioClientTransport).stderr!.on( - "data", - (chunk) => { - webAppTransport.send({ - jsonrpc: "2.0", - method: "notifications/stderr", - params: { - content: chunk.toString(), - }, - }); - }, - ); + + (serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => { + webAppTransport.send({ + jsonrpc: "2.0", + method: "notifications/stderr", + params: { + content: chunk.toString(), + }, + }); + }); mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); console.log("Set up MCP proxy"); @@ -284,10 +281,9 @@ app.get("/sse", async (req, res) => { console.log( "New SSE connection. NOTE: The sse transport is deprecated and has been replaced by streamable-http", ); - + let serverTransport: SSEServerTransport | undefined; try { - await backingServerTransport?.close(); - backingServerTransport = await createTransport(req); + serverTransport = (await createTransport(req)) as SSEServerTransport; } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -305,13 +301,14 @@ app.get("/sse", async (req, res) => { const webAppTransport = new SSEServerTransport("/message", res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); - console.log("Created web app transport"); + serverTransports.set(webAppTransport.sessionId, serverTransport); + console.log("Created client/server transports"); await webAppTransport.start(); mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); console.log("Set up MCP proxy"); From 2870441fea8206625142f9d6b15ffe9904b82377 Mon Sep 17 00:00:00 2001 From: xiaoli Date: Sun, 25 May 2025 11:17:37 +0800 Subject: [PATCH 12/23] =?UTF-8?q?fix:=20fix=20review=20comments,=20again?= =?UTF-8?q?=F0=9F=98=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unnecessary auth header name check - Extract operations on headers into a function --- client/src/lib/hooks/useConnection.ts | 3 - server/src/index.ts | 80 +++++++++++++-------------- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index f1f7ab9e..84956942 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -301,9 +301,6 @@ export function useConnection({ // Add custom header name as a special request header to let the server know which header to pass through if (authHeaderName.toLowerCase() !== "authorization") { headers[authHeaderName] = token; - if (headerName) { - headers["x-custom-auth-header"] = headerName; - } } else { headers[authHeaderName] = `Bearer ${token}`; } diff --git a/server/src/index.ts b/server/src/index.ts index fd28a807..de14e286 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -41,6 +41,43 @@ const { values } = parseArgs({ }, }); +// Function to get HTTP headers. +// Supports only "sse" and "streamable-http" transport types. +const getHttpHeaders = ( + req: express.Request, + transportType: string, +): HeadersInit => { + const headers: HeadersInit = { + Accept: + transportType === "sse" + ? "text/event-stream" + : "text/event-stream, application/json", + }; + const defaultHeaders = + transportType === "sse" + ? SSE_HEADERS_PASSTHROUGH + : STREAMABLE_HTTP_HEADERS_PASSTHROUGH; + + for (const key of defaultHeaders) { + if (req.headers[key] === undefined) { + continue; + } + + const value = req.headers[key]; + headers[key] = Array.isArray(value) ? value[value.length - 1] : value; + } + + // If the header "x-custom-auth-header" is present, use its value as the custom header name. + if (req.headers["x-custom-auth-header"] !== undefined) { + const customHeaderName = req.headers["x-custom-auth-header"] as string; + if (req.headers[customHeaderName] !== undefined) { + const value = req.headers[customHeaderName]; + headers[customHeaderName] = value as string; + } + } + return headers; +}; + const app = express(); app.use(cors()); app.use((req, res, next) => { @@ -79,27 +116,8 @@ const createTransport = async (req: express.Request): Promise => { return transport; } else if (transportType === "sse") { const url = query.url as string; - const headers: HeadersInit = { - Accept: "text/event-stream", - }; - for (const key of SSE_HEADERS_PASSTHROUGH) { - if (req.headers[key] === undefined) { - continue; - } - - const value = req.headers[key]; - headers[key] = Array.isArray(value) ? value[value.length - 1] : value; - } - - // If the header "x-custom-auth-header" is present, use its value as the custom header name. - if (req.headers["x-custom-auth-header"] !== undefined) { - const customHeaderName = req.headers["x-custom-auth-header"] as string; - if (req.headers[customHeaderName] !== undefined) { - const value = req.headers[customHeaderName]; - headers[customHeaderName] = value as string; - } - } + const headers = getHttpHeaders(req, transportType); console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`); @@ -116,27 +134,7 @@ const createTransport = async (req: express.Request): Promise => { console.log("Connected to SSE transport"); return transport; } else if (transportType === "streamable-http") { - const headers: HeadersInit = { - Accept: "text/event-stream, application/json", - }; - - for (const key of STREAMABLE_HTTP_HEADERS_PASSTHROUGH) { - if (req.headers[key] === undefined) { - continue; - } - - const value = req.headers[key]; - headers[key] = Array.isArray(value) ? value[value.length - 1] : value; - } - - // If the header "x-custom-auth-header" is present, use its value as the custom header name. - if (req.headers["x-custom-auth-header"] !== undefined) { - const customHeaderName = req.headers["x-custom-auth-header"] as string; - if (req.headers[customHeaderName] !== undefined) { - const value = req.headers[customHeaderName]; - headers[customHeaderName] = value as string; - } - } + const headers = getHttpHeaders(req, transportType); const transport = new StreamableHTTPClientTransport( new URL(query.url as string), From 21833f91022539ce6093a112cbe5b204f67ccd9a Mon Sep 17 00:00:00 2001 From: Balaj Marius Date: Tue, 27 May 2025 11:48:03 +0300 Subject: [PATCH 13/23] feat: improve color contrast and readability in dark mode --- client/src/App.tsx | 8 ++++---- client/src/components/History.tsx | 12 ++++++++---- client/src/components/ListPane.tsx | 6 +++--- client/src/components/PromptsTab.tsx | 6 +++--- client/src/components/ResourcesTab.tsx | 6 +++--- client/src/components/Sidebar.tsx | 4 ++-- client/src/components/ToolsTab.tsx | 8 ++++---- client/src/index.css | 4 ++-- 8 files changed, 29 insertions(+), 25 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 68065e24..be4347ca 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -608,7 +608,7 @@ const App = () => { className="w-full p-4" onValueChange={(value) => (window.location.hash = value)} > - + { !serverCapabilities?.tools ? ( <>
-

+

The connected server does not support any MCP capabilities

@@ -811,7 +811,7 @@ const App = () => { ) : (
-

+

Connect to an MCP server to start inspecting

@@ -836,7 +836,7 @@ const App = () => { }} >
diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index 0b05b55d..78394dea 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -29,7 +29,9 @@ const HistoryAndNotifications = ({

History

{requestHistory.length === 0 ? ( -

No history yet

+

+ No history yet +

) : (
    {requestHistory @@ -38,7 +40,7 @@ const HistoryAndNotifications = ({ .map((request, index) => (
  • Server Notifications

    {serverNotifications.length === 0 ? ( -

    No notifications yet

    +

    + No notifications yet +

    ) : (
      {serverNotifications @@ -102,7 +106,7 @@ const HistoryAndNotifications = ({ .map((notification, index) => (
    • ({ buttonText, isButtonDisabled, }: ListPaneProps) => ( -
      -
      +
      +

      {title}

      @@ -46,7 +46,7 @@ const ListPane = ({ {items.map((item, index) => (
      setSelectedItem(item)} > {renderItem(item)} diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index 5fdad823..66c15d8f 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -110,8 +110,8 @@ const PromptsTab = ({ isButtonDisabled={!nextCursor && prompts.length > 0} /> -
      -
      +
      +

      {selectedPrompt ? selectedPrompt.name : "Select a prompt"}

      @@ -126,7 +126,7 @@ const PromptsTab = ({ ) : selectedPrompt ? (
      {selectedPrompt.description && ( -

      +

      {selectedPrompt.description}

      )} diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index c0f8203f..35742799 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -173,8 +173,8 @@ const ResourcesTab = ({ isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} /> -
      -
      +
      +

      ) : selectedTemplate ? (
      -

      +

      {selectedTemplate.description}

      {selectedTemplate.uriTemplate diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 516f48d5..eee8eeaa 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -215,7 +215,7 @@ const Sidebar = ({ return (
      -
      +

      MCP Inspector v{version} @@ -646,7 +646,7 @@ const Sidebar = ({ } })()}`} /> - + {(() => { switch (connectionStatus) { case "connected": diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index aa67bfcf..16d53848 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -145,8 +145,8 @@ const ToolsTab = ({ isButtonDisabled={!nextCursor && tools.length > 0} /> -
      -
      +
      +

      {selectedTool ? selectedTool.name : "Select a tool"}

      @@ -154,7 +154,7 @@ const ToolsTab = ({
      {selectedTool ? (
      -

      +

      {selectedTool.description}

      {Object.entries(selectedTool.inputSchema.properties ?? []).map( @@ -164,7 +164,7 @@ const ToolsTab = ({
      diff --git a/client/src/index.css b/client/src/index.css index 11c6f23d..c3f30b97 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -93,8 +93,8 @@ h1 { --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; + --border: 217.2, 24%, 24%; + --input: 217.2 24% 24%; --ring: 212.7 26.8% 83.9%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; From 66bb9c896b0f3961d9edc88f845ccdd6b9d39cfb Mon Sep 17 00:00:00 2001 From: David Sanftenberg Date: Wed, 28 May 2025 12:22:22 +0100 Subject: [PATCH 14/23] Add quick start example --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index d7c34b97..8d0de6f8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,16 @@ The MCP inspector is a developer tool for testing and debugging MCP servers. - Node.js: ^22.7.5 +### Quick Start (UI mode) + +To get up and running right away with the UI, just execute the following: + +```bash +npx @modelcontextprotocol/inspector +``` + +The server will start up and the UI will be accessible at `http://localhost:6274`. + ### From an MCP server repository To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`: From bc90f11dc88ae15de0adc059164ea35ed7f77d48 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 28 May 2025 13:44:55 -0400 Subject: [PATCH 15/23] Address [comment](https://github.com/modelcontextprotocol/inspector/pull/428/files/f04470ace7080f53233d1387e2dbb4d232bd5f7d#r2109509222) by @olaservo --- 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 8bc8a53c..c4345ae6 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -281,9 +281,9 @@ app.get("/sse", async (req, res) => { console.log( "New SSE connection. NOTE: The sse transport is deprecated and has been replaced by streamable-http", ); - let serverTransport: SSEServerTransport | undefined; + let serverTransport: Transport | undefined; try { - serverTransport = (await createTransport(req)) as SSEServerTransport; + serverTransport = (await createTransport(req)); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( From 9bc6bd725ee5e974047d2949644355dac5615828 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 28 May 2025 13:46:58 -0400 Subject: [PATCH 16/23] Address [comment](https://github.com/modelcontextprotocol/inspector/pull/428/files/f04470ace7080f53233d1387e2dbb4d232bd5f7d#r2109509222) [by](https://github.com/modelcontextprotocol/inspector/pull/428/files/f04470ace7080f53233d1387e2dbb4d232bd5f7d#r2108137150) @olaservo --- server/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index c4345ae6..0b2aeec9 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -301,8 +301,9 @@ app.get("/sse", async (req, res) => { const webAppTransport = new SSEServerTransport("/message", res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); + console.log("Created client transport"); serverTransports.set(webAppTransport.sessionId, serverTransport); - console.log("Created client/server transports"); + console.log("Created server transport"); await webAppTransport.start(); From 0c1962c2d06e11f7dab395a38c7104a9c0121640 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 28 May 2025 15:46:07 -0400 Subject: [PATCH 17/23] prettier --- server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index 0b2aeec9..73288f67 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -283,7 +283,7 @@ app.get("/sse", async (req, res) => { ); let serverTransport: Transport | undefined; try { - serverTransport = (await createTransport(req)); + serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( From 10e3202832c51add7e0fdb8ab16cecde2646c47e Mon Sep 17 00:00:00 2001 From: xiaoli Date: Thu, 29 May 2025 11:49:57 +0800 Subject: [PATCH 18/23] fix: Fix the issue of accidentally losing 'x-custom-auth-header' --- client/src/lib/hooks/useConnection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 84956942..f1c29b2d 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -301,6 +301,7 @@ export function useConnection({ // Add custom header name as a special request header to let the server know which header to pass through if (authHeaderName.toLowerCase() !== "authorization") { headers[authHeaderName] = token; + headers["x-custom-auth-header"] = authHeaderName; } else { headers[authHeaderName] = `Bearer ${token}`; } From 2e5cc87659ea77bb26db342af2097abb7d95d20b Mon Sep 17 00:00:00 2001 From: Balaj Marius Date: Thu, 29 May 2025 18:30:13 +0300 Subject: [PATCH 19/23] Update index.css Co-authored-by: Ola Hungerford --- client/src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/index.css b/client/src/index.css index c3f30b97..858f9e7f 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -93,7 +93,7 @@ h1 { --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2, 24%, 24%; + --border: 217.2 24% 24%; --input: 217.2 24% 24%; --ring: 212.7 26.8% 83.9%; --chart-1: 220 70% 50%; From 02aacb46a2851cf6c5066659f1bc3b56a44d3c76 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 29 May 2025 19:54:36 -0400 Subject: [PATCH 20/23] Send HTTP/DELETE when disconnecting client * In server/src/index.ts - add delete handler for /mcp endpoint - gets the server transport for the sessionId - calls terminateSession on it - removes the webapp and server transports for the sessionId from the maps - returns status 200 to the client. * In client/src/lib/hooks/useConnection.ts - import Transport - add useState for clientTransport, type Transport or null initialized to null - in connect() function - move creation of client transport down a ways, just before calling client.connect with it - immendiately efter calling client.connect, call setClientTransport with the connected transport (has to happen after, so that what's saved has the abort controller, etc. otherwise it doesn't work to save it prior. - in disconnect() function - immediately call clientTransport.terminateSession if transportType is "streamable-http" - setClientTransport to null --- client/src/lib/hooks/useConnection.ts | 32 +++++++++++++++++++-------- server/src/index.ts | 27 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9c825318..39da55cb 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -45,6 +45,7 @@ import { } from "@/utils/configUtils"; import { getMCPServerRequestTimeout } from "@/utils/configUtils"; import { InspectorConfig } from "../configurationTypes"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; interface UseConnectionOptions { transportType: "stdio" | "sse" | "streamable-http"; @@ -83,6 +84,9 @@ export function useConnection({ const [serverCapabilities, setServerCapabilities] = useState(null); const [mcpClient, setMcpClient] = useState(null); + const [clientTransport, setClientTransport] = useState( + null, + ); const [requestHistory, setRequestHistory] = useState< { request: string; response?: string }[] >([]); @@ -377,14 +381,6 @@ export function useConnection({ transportType, ); - const clientTransport = - transportType === "streamable-http" - ? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, { - sessionId: undefined, - ...transportOptions, - }) - : new SSEClientTransport(mcpProxyServerUrl as URL, transportOptions); - if (onNotification) { [ CancelledNotificationSchema, @@ -414,7 +410,20 @@ export function useConnection({ let capabilities; try { - await client.connect(clientTransport); + const transport = + transportType === "streamable-http" + ? new StreamableHTTPClientTransport(mcpProxyServerUrl as URL, { + sessionId: undefined, + ...transportOptions, + }) + : new SSEClientTransport( + mcpProxyServerUrl as URL, + transportOptions, + ); + + await client.connect(transport as Transport); + + setClientTransport(transport); capabilities = client.getServerCapabilities(); const initializeRequest = { @@ -468,10 +477,15 @@ export function useConnection({ }; const disconnect = async () => { + if (transportType === "streamable-http") + await ( + clientTransport as StreamableHTTPClientTransport + ).terminateSession(); await mcpClient?.close(); const authProvider = new InspectorOAuthClientProvider(sseUrl); authProvider.clear(); setMcpClient(null); + setClientTransport(null); setConnectionStatus("disconnected"); setCompletionsSupported(false); setServerCapabilities(null); diff --git a/server/src/index.ts b/server/src/index.ts index 73288f67..1586d0a0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -226,6 +226,33 @@ app.post("/mcp", async (req, res) => { } }); +app.delete("/mcp", async (req, res) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + console.log(`Received DELETE message for sessionId ${sessionId}`); + let serverTransport: Transport | undefined; + if (sessionId) { + try { + serverTransport = serverTransports.get( + sessionId, + ) as StreamableHTTPClientTransport; + if (!serverTransport) { + res.status(404).end("Transport not found for sessionId " + sessionId); + } else { + await ( + serverTransport as StreamableHTTPClientTransport + ).terminateSession(); + webAppTransports.delete(sessionId); + serverTransports.delete(sessionId); + console.log(`Transports removed for sessionId ${sessionId}`); + } + res.status(200).end(); + } catch (error) { + console.error("Error in /mcp route:", error); + res.status(500).json(error); + } + } +}); + app.get("/stdio", async (req, res) => { try { console.log("New connection"); From b51ee625cddcf76608873c381c069a07195ace73 Mon Sep 17 00:00:00 2001 From: xiaoli Date: Fri, 30 May 2025 10:14:03 +0800 Subject: [PATCH 21/23] style: fix code style to pass CI checks --- client/src/lib/hooks/useConnection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index fae904da..e4f1bc50 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -304,8 +304,8 @@ export function useConnection({ bearerToken || (await serverAuthProvider.tokens())?.access_token; if (token) { const authHeaderName = headerName || "Authorization"; - - // Add custom header name as a special request header to let the server know which header to pass through + + // Add custom header name as a special request header to let the server know which header to pass through if (authHeaderName.toLowerCase() !== "authorization") { headers[authHeaderName] = token; headers["x-custom-auth-header"] = authHeaderName; From 3510e1a56128a094638290531fec59fc820ffa2e Mon Sep 17 00:00:00 2001 From: xiaoli Date: Fri, 30 May 2025 14:43:09 +0800 Subject: [PATCH 22/23] fix: preserve capitalization in custom headers Express converts all header names to lowercase, which prevents custom headers with uppercase characters from being properly passed to the target service. This fix ensures that custom headers preserve their original capitalization when forwarded through the proxy. --- server/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index de14e286..962c90e8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -70,8 +70,9 @@ const getHttpHeaders = ( // If the header "x-custom-auth-header" is present, use its value as the custom header name. if (req.headers["x-custom-auth-header"] !== undefined) { const customHeaderName = req.headers["x-custom-auth-header"] as string; - if (req.headers[customHeaderName] !== undefined) { - const value = req.headers[customHeaderName]; + const lowerCaseHeaderName = customHeaderName.toLowerCase(); + if (req.headers[lowerCaseHeaderName] !== undefined) { + const value = req.headers[lowerCaseHeaderName]; headers[customHeaderName] = value as string; } } From 11757ef6154283a0e5bacf04b61dfeda5d9e4cfe Mon Sep 17 00:00:00 2001 From: olaservo Date: Fri, 30 May 2025 07:46:28 -0700 Subject: [PATCH 23/23] Add simple architecture overview --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index d7c34b97..99356536 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ The MCP inspector is a developer tool for testing and debugging MCP servers. ![MCP Inspector Screenshot](https://raw.githubusercontent.com/modelcontextprotocol/inspector/main/mcp-inspector.png) +## Architecture Overview + +The MCP Inspector consists of two main components that work together: + +- **MCP Inspector Client (MCPI)**: A React-based web UI that provides an interactive interface for testing and debugging MCP servers +- **MCP Proxy (MCPP)**: A Node.js server that acts as a protocol bridge, connecting the web UI to MCP servers via various transport methods (stdio, SSE, streamable-http) + +Note that the proxy is not a network proxy for intercepting traffic. Instead, it functions as both an MCP client (connecting to your MCP server) and an HTTP server (serving the web UI), enabling browser-based interaction with MCP servers that use different transport protocols. + ## Running the Inspector ### Requirements