From 618f24a9acb25222b01dec19e773362bb36c7abb Mon Sep 17 00:00:00 2001 From: xiaoli Date: Tue, 13 May 2025 23:38:19 +0800 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] =?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 66bb9c896b0f3961d9edc88f845ccdd6b9d39cfb Mon Sep 17 00:00:00 2001 From: David Sanftenberg Date: Wed, 28 May 2025 12:22:22 +0100 Subject: [PATCH 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 b51ee625cddcf76608873c381c069a07195ace73 Mon Sep 17 00:00:00 2001 From: xiaoli Date: Fri, 30 May 2025 10:14:03 +0800 Subject: [PATCH 10/11] 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 11/11] 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; } }