diff --git a/README.md b/README.md index 99356536..9d5017dc 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ Note that the proxy is not a network proxy for intercepting traffic. Instead, it - 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`: diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9c825318..e4f1bc50 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -304,7 +304,14 @@ export function useConnection({ bearerToken || (await serverAuthProvider.tokens())?.access_token; if (token) { const authHeaderName = headerName || "Authorization"; - headers[authHeaderName] = `Bearer ${token}`; + + // 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}`; + } } // Create appropriate transport diff --git a/server/src/index.ts b/server/src/index.ts index c967b60c..ef3061d8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -41,6 +41,44 @@ 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; + const lowerCaseHeaderName = customHeaderName.toLowerCase(); + if (req.headers[lowerCaseHeaderName] !== undefined) { + const value = req.headers[lowerCaseHeaderName]; + headers[customHeaderName] = value as string; + } + } + return headers; +}; + const app = express(); app.use(cors()); app.use((req, res, next) => { @@ -48,7 +86,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; @@ -79,18 +118,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; - } + const headers = getHttpHeaders(req, transportType); console.log(`SSE transport: url=${url}, headers=${Object.keys(headers)}`); @@ -107,18 +136,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; - } + const headers = getHttpHeaders(req, transportType); const transport = new StreamableHTTPClientTransport( new URL(query.url as string), @@ -137,8 +155,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 +177,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 +196,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 +211,7 @@ app.post("/mcp", async (req, res) => { mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); await (webAppTransport as StreamableHTTPServerTransport).handleRequest( @@ -229,10 +246,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 +266,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 +298,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: 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( @@ -305,13 +318,15 @@ app.get("/sse", async (req, res) => { const webAppTransport = new SSEServerTransport("/message", res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); - console.log("Created web app transport"); + console.log("Created client transport"); + serverTransports.set(webAppTransport.sessionId, serverTransport); + console.log("Created server transport"); await webAppTransport.start(); mcpProxy({ transportToClient: webAppTransport, - transportToServer: backingServerTransport, + transportToServer: serverTransport, }); console.log("Set up MCP proxy");