From e8e990987c544f9e9c2603acbf990866f9c91891 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 10 Jun 2025 13:56:09 +0100 Subject: [PATCH 1/9] fix: bind to localhost by default to prevent DNS rebinding attacks - Change default binding from 0.0.0.0 (all interfaces) to 127.0.0.1 (localhost only) - Add HOST environment variable to override binding address when needed - Update console output to show actual binding address - Document security change in README with warnings about binding to all interfaces This prevents remote attackers from accessing the MCP Inspector proxy server, which has the ability to execute local processes. The proxy should only be accessible from the local machine unless explicitly configured otherwise. --- README.md | 10 ++++++++++ server/src/index.ts | 7 ++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d5017dc..fbda060e 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,16 @@ The inspector supports bearer token authentication for SSE connections. Enter yo The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. +#### Local-only Binding + +By default, the MCP Inspector proxy server binds only to `127.0.0.1` (localhost) to prevent network access. This ensures the server is not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable: + +```bash +HOST=0.0.0.0 npm start +``` + +**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes. + ### Configuration The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI: diff --git a/server/src/index.ts b/server/src/index.ts index 37327fa2..c67f2936 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -415,11 +415,12 @@ app.get("/config", (req, res) => { } }); -const PORT = process.env.PORT || 6277; +const PORT = parseInt(process.env.PORT || '6277', 10); +const HOST = process.env.HOST || '127.0.0.1'; -const server = app.listen(PORT); +const server = app.listen(PORT, HOST); server.on("listening", () => { - console.log(`āš™ļø Proxy server listening on port ${PORT}`); + console.log(`āš™ļø Proxy server listening on ${HOST}:${PORT}`); }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { From 111e2823860ea00b74d32cf97fa76b64ff791be7 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 10 Jun 2025 14:05:17 +0100 Subject: [PATCH 2/9] feat: add session token authentication to proxy server - Generate random session token on server startup using crypto.randomBytes - Add authentication middleware requiring Bearer token for all sensitive endpoints - Apply auth to /mcp, /stdio, /sse, and /message endpoints - Display session token in console on startup - Add DANGEROUSLY_OMIT_AUTH environment variable to disable auth for development - Update README with authentication documentation This prevents unauthorized access to the proxy server's ability to execute local processes and connect to MCP servers. --- README.md | 29 +++++++++++++++++++++++++++++ server/src/index.ts | 44 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fbda060e..42829645 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,35 @@ The inspector supports bearer token authentication for SSE connections. Enter yo The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. +#### Authentication + +The MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console: + +``` +šŸ”‘ Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4 + +šŸ”— Open inspector with token pre-filled: + http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4 +``` + +This token must be included as a Bearer token in the Authorization header for all requests to the server. + +**Option 1: Use the pre-filled URL** - Click the link shown in the console to open the inspector with the token already configured. + +**Option 2: Manual configuration** - If you already have the inspector open: + +1. Click the "Configuration" button in the sidebar +2. Find "Proxy Session Token" and enter the token displayed in the proxy console +3. Click "Save" to apply the configuration + +The token will be saved in your browser's local storage for future use. + +If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable: + +```bash +DANGEROUSLY_OMIT_AUTH=true npm start +``` + #### Local-only Binding By default, the MCP Inspector proxy server binds only to `127.0.0.1` (localhost) to prevent network access. This ensures the server is not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable: diff --git a/server/src/index.ts b/server/src/index.ts index c67f2936..5ba4788f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -19,7 +19,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import express from "express"; import { findActualExecutable } from "spawn-rx"; import mcpProxy from "./mcpProxy.js"; -import { randomUUID } from "node:crypto"; +import { randomUUID, randomBytes } from "node:crypto"; const SSE_HEADERS_PASSTHROUGH = ["authorization"]; const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [ @@ -89,6 +89,25 @@ app.use((req, res, next) => { const webAppTransports: Map = new Map(); // Web app transports by web app sessionId const serverTransports: Map = new Map(); // Server Transports by web app sessionId +const sessionToken = randomBytes(32).toString('hex'); +const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; + +const authMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (authDisabled) { + return next(); + } + + const authHeader = req.headers.authorization; + if (!authHeader || authHeader !== `Bearer ${sessionToken}`) { + res.status(401).json({ + error: "Unauthorized", + message: "Authentication required. Use the session token shown in the console when starting the server." + }); + return; + } + next(); +}; + const createTransport = async (req: express.Request): Promise => { const query = req.query; console.log("Query parameters:", JSON.stringify(query)); @@ -150,7 +169,7 @@ const createTransport = async (req: express.Request): Promise => { } }; -app.get("/mcp", async (req, res) => { +app.get("/mcp", authMiddleware, async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string; console.log(`Received GET message for sessionId ${sessionId}`); try { @@ -169,7 +188,7 @@ app.get("/mcp", async (req, res) => { } }); -app.post("/mcp", async (req, res) => { +app.post("/mcp", authMiddleware, async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; let serverTransport: Transport | undefined; if (!sessionId) { @@ -239,7 +258,7 @@ app.post("/mcp", async (req, res) => { } }); -app.delete("/mcp", async (req, res) => { +app.delete("/mcp", authMiddleware, 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; @@ -266,7 +285,7 @@ app.delete("/mcp", async (req, res) => { } }); -app.get("/stdio", async (req, res) => { +app.get("/stdio", authMiddleware, async (req, res) => { try { console.log("New STDIO connection request"); let serverTransport: Transport | undefined; @@ -328,7 +347,7 @@ app.get("/stdio", async (req, res) => { } }); -app.get("/sse", async (req, res) => { +app.get("/sse", authMiddleware, async (req, res) => { try { console.log( "New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp", @@ -377,7 +396,7 @@ app.get("/sse", async (req, res) => { } }); -app.post("/message", async (req, res) => { +app.post("/message", authMiddleware, async (req, res) => { try { const sessionId = req.query.sessionId; console.log(`Received POST message for sessionId ${sessionId}`); @@ -421,6 +440,17 @@ const HOST = process.env.HOST || '127.0.0.1'; const server = app.listen(PORT, HOST); server.on("listening", () => { console.log(`āš™ļø Proxy server listening on ${HOST}:${PORT}`); + if (!authDisabled) { + console.log(`šŸ”‘ Session token: ${sessionToken}`); + console.log(`Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`); + + // Display clickable URL with pre-filled token + const clientPort = process.env.CLIENT_PORT || '6274'; + const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; + console.log(`\nšŸ”— Open inspector with token pre-filled:\n ${clientUrl}`); + } else { + console.log(`āš ļø WARNING: Authentication is disabled. This is not recommended.`); + } }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { From 908d0ee36ae3e78374ad8e15ef458c9d4928b7b2 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 10 Jun 2025 14:12:54 +0100 Subject: [PATCH 3/9] feat: add client support for proxy authentication - Add MCP_PROXY_AUTH_TOKEN configuration field to store session token - Update useConnection hook to include proxy auth token in all requests - Add separate proxy auth headers to avoid conflicts with MCP server auth - Add specific error handling for proxy authentication failures - Show helpful toast message directing users to Configuration settings - Update README with instructions for configuring token in UI The client now properly authenticates with the proxy server using the session token displayed in the proxy console. --- client/src/lib/configurationTypes.ts | 5 +++ client/src/lib/constants.ts | 6 ++++ client/src/lib/hooks/useConnection.ts | 45 ++++++++++++++++++++++----- client/src/utils/configUtils.ts | 4 +++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/client/src/lib/configurationTypes.ts b/client/src/lib/configurationTypes.ts index 7c74bb71..c2024e51 100644 --- a/client/src/lib/configurationTypes.ts +++ b/client/src/lib/configurationTypes.ts @@ -33,4 +33,9 @@ export type InspectorConfig = { * The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577 */ MCP_PROXY_FULL_ADDRESS: ConfigItem; + + /** + * Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup. + */ + MCP_PROXY_AUTH_TOKEN: ConfigItem; }; diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index 3acbda91..d1ca972a 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -54,4 +54,10 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = { "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577", value: "", }, + MCP_PROXY_AUTH_TOKEN: { + label: "Proxy Session Token", + description: + "Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)", + value: "", + }, } as const; diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index d42b8d38..c2eccb72 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -42,6 +42,7 @@ import { getMCPProxyAddress, getMCPServerRequestMaxTotalTimeout, resetRequestTimeoutOnProgress, + getMCPProxyAuthToken, } from "@/utils/configUtils"; import { getMCPServerRequestTimeout } from "@/utils/configUtils"; import { InspectorConfig } from "../configurationTypes"; @@ -242,7 +243,12 @@ export function useConnection({ const checkProxyHealth = async () => { try { const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`); - const proxyHealthResponse = await fetch(proxyHealthUrl); + const proxyAuthToken = getMCPProxyAuthToken(config); + const headers: HeadersInit = {}; + if (proxyAuthToken) { + headers['Authorization'] = `Bearer ${proxyAuthToken}`; + } + const proxyHealthResponse = await fetch(proxyHealthUrl, { headers }); const proxyHealth = await proxyHealthResponse.json(); if (proxyHealth?.status !== "ok") { throw new Error("MCP Proxy Server is not healthy"); @@ -261,6 +267,13 @@ export function useConnection({ ); }; + const isProxyAuthError = (error: unknown): boolean => { + return ( + error instanceof Error && + error.message.includes("Authentication required. Use the session token") + ); + }; + const handleAuthError = async (error: unknown) => { if (is401Error(error)) { const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); @@ -318,6 +331,13 @@ export function useConnection({ } } + // Add proxy authentication + const proxyAuthToken = getMCPProxyAuthToken(config); + const proxyHeaders: HeadersInit = {}; + if (proxyAuthToken) { + proxyHeaders['Authorization'] = `Bearer ${proxyAuthToken}`; + } + // Create appropriate transport let transportOptions: | StreamableHTTPClientTransportOptions @@ -336,10 +356,10 @@ export function useConnection({ fetch: ( url: string | URL | globalThis.Request, init: RequestInit | undefined, - ) => fetch(url, { ...init, headers }), + ) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }), }, requestInit: { - headers, + headers: { ...headers, ...proxyHeaders }, }, }; break; @@ -352,10 +372,10 @@ export function useConnection({ fetch: ( url: string | URL | globalThis.Request, init: RequestInit | undefined, - ) => fetch(url, { ...init, headers }), + ) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }), }, requestInit: { - headers, + headers: { ...headers, ...proxyHeaders }, }, }; break; @@ -368,10 +388,10 @@ export function useConnection({ fetch: ( url: string | URL | globalThis.Request, init: RequestInit | undefined, - ) => fetch(url, { ...init, headers }), + ) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }), }, requestInit: { - headers, + headers: { ...headers, ...proxyHeaders }, }, // TODO these should be configurable... reconnectionOptions: { @@ -447,6 +467,17 @@ export function useConnection({ error, ); + // Check if it's a proxy auth error + if (isProxyAuthError(error)) { + toast({ + title: "Proxy Authentication Required", + description: "Please enter the session token from the proxy server console in the Configuration settings.", + variant: "destructive", + }); + setConnectionStatus("error"); + return; + } + const shouldRetry = await handleAuthError(error); if (shouldRetry) { return connect(undefined, retryCount + 1); diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index ae404d65..55b29a17 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -28,6 +28,10 @@ export const getMCPServerRequestMaxTotalTimeout = ( return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number; }; +export const getMCPProxyAuthToken = (config: InspectorConfig): string => { + return config.MCP_PROXY_AUTH_TOKEN.value as string; +}; + const getSearchParam = (key: string): string | null => { try { const url = new URL(window.location.href); From 15ecb59a965d0de66358af22c8f2d33e88e0b1bd Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 10 Jun 2025 14:16:46 +0100 Subject: [PATCH 4/9] feat: add origin validation to prevent DNS rebinding attacks - Add origin validation middleware to check Origin header - Default allowed origins respect CLIENT_PORT environment variable - Support ALLOWED_ORIGINS environment variable for additional origins - Apply validation to all protected endpoints before auth check - Return 403 Forbidden with clear message for invalid origins - Add DNS Rebinding Protection section to README This completes the security hardening by preventing malicious websites from making requests to the local proxy server. --- README.md | 8 ++++++++ server/src/index.ts | 35 +++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 42829645..122e86de 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,14 @@ HOST=0.0.0.0 npm start **Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes. +#### DNS Rebinding Protection + +To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list): + +```bash +ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://localhost:8000 npm start +``` + ### Configuration The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI: diff --git a/server/src/index.ts b/server/src/index.ts index 5ba4788f..5b3b0c50 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -92,6 +92,29 @@ const serverTransports: Map = new Map(); / const sessionToken = randomBytes(32).toString('hex'); const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; +// Origin validation middleware to prevent DNS rebinding attacks +const originValidationMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { + const origin = req.headers.origin; + + // Default origins based on CLIENT_PORT or use environment variable + const clientPort = process.env.CLIENT_PORT || '6274'; + const defaultOrigins = [ + `http://localhost:${clientPort}`, + `http://127.0.0.1:${clientPort}` + ]; + const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || defaultOrigins; + + if (origin && !allowedOrigins.includes(origin)) { + console.error(`Invalid origin: ${origin}`); + res.status(403).json({ + error: 'Forbidden - invalid origin', + message: 'Request blocked to prevent DNS rebinding attacks. Configure allowed origins via environment variable.' + }); + return; + } + next(); +}; + const authMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { if (authDisabled) { return next(); @@ -169,7 +192,7 @@ const createTransport = async (req: express.Request): Promise => { } }; -app.get("/mcp", authMiddleware, async (req, res) => { +app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string; console.log(`Received GET message for sessionId ${sessionId}`); try { @@ -188,7 +211,7 @@ app.get("/mcp", authMiddleware, async (req, res) => { } }); -app.post("/mcp", authMiddleware, async (req, res) => { +app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; let serverTransport: Transport | undefined; if (!sessionId) { @@ -258,7 +281,7 @@ app.post("/mcp", authMiddleware, async (req, res) => { } }); -app.delete("/mcp", authMiddleware, async (req, res) => { +app.delete("/mcp", originValidationMiddleware, authMiddleware, 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; @@ -285,7 +308,7 @@ app.delete("/mcp", authMiddleware, async (req, res) => { } }); -app.get("/stdio", authMiddleware, async (req, res) => { +app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) => { try { console.log("New STDIO connection request"); let serverTransport: Transport | undefined; @@ -347,7 +370,7 @@ app.get("/stdio", authMiddleware, async (req, res) => { } }); -app.get("/sse", authMiddleware, async (req, res) => { +app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => { try { console.log( "New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp", @@ -396,7 +419,7 @@ app.get("/sse", authMiddleware, async (req, res) => { } }); -app.post("/message", authMiddleware, async (req, res) => { +app.post("/message", originValidationMiddleware, authMiddleware, async (req, res) => { try { const sessionId = req.query.sessionId; console.log(`Received POST message for sessionId ${sessionId}`); From 9dbd17343cdeef02fbc2c6f1cadb8c26f165755e Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 10 Jun 2025 16:12:52 +0100 Subject: [PATCH 5/9] feat: disable auto-open when authentication is enabled - Auto-open browser only works when DANGEROUSLY_OMIT_AUTH=true - When auth is enabled (default), users must use the secure URL with token - Server displays clear message that auto-open is disabled with auth - Updates documentation to reflect this behavior This improves UX by preventing the browser from opening without auth token, forcing users to use the secure pre-filled URL instead. --- README.md | 6 +++--- client/bin/start.js | 4 +++- server/src/index.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 122e86de..ae82a695 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,11 @@ The MCP Inspector proxy server requires authentication by default. When starting http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4 ``` -This token must be included as a Bearer token in the Authorization header for all requests to the server. +This token must be included as a Bearer token in the Authorization header for all requests to the server. When authentication is enabled, auto-open is disabled by default to ensure you use the secure URL. -**Option 1: Use the pre-filled URL** - Click the link shown in the console to open the inspector with the token already configured. +**Recommended: Use the pre-filled URL** - Click or copy the link shown in the console to open the inspector with the token already configured. -**Option 2: Manual configuration** - If you already have the inspector open: +**Alternative: Manual configuration** - If you already have the inspector open: 1. Click the "Configuration" button in the sidebar 2. Find "Proxy Session Token" and enter the token displayed in the proxy console diff --git a/client/bin/start.js b/client/bin/start.js index 8e383b38..ccce7ade 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -100,7 +100,9 @@ async function main() { if (serverOk) { try { - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { + // Only auto-open when auth is disabled + const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; + if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && authDisabled) { open(`http://127.0.0.1:${CLIENT_PORT}`); } await spawnPromise("node", [inspectorClientPath], { diff --git a/server/src/index.ts b/server/src/index.ts index 5b3b0c50..c82227ac 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -470,7 +470,7 @@ server.on("listening", () => { // Display clickable URL with pre-filled token const clientPort = process.env.CLIENT_PORT || '6274'; const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; - console.log(`\nšŸ”— Open inspector with token pre-filled:\n ${clientUrl}`); + console.log(`\nšŸ”— Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`); } else { console.log(`āš ļø WARNING: Authentication is disabled. This is not recommended.`); } From 32e6bdd13c79531afe9d1d0ab7a280a19e43cc9f Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 10 Jun 2025 17:33:29 +0100 Subject: [PATCH 6/9] feat: improve error messaging for proxy authentication failures - Update connection error message to guide users about proxy token configuration - Add specific message when proxy token is missing directing to Configuration section - Help users quickly identify and resolve authentication issues --- client/src/components/Sidebar.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 3985eb48..938e5b5a 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -666,8 +666,13 @@ const Sidebar = ({ switch (connectionStatus) { case "connected": return "Connected"; - case "error": - return "Connection Error, is your MCP server running?"; + case "error": { + const hasProxyToken = config.MCP_PROXY_AUTH_TOKEN?.value; + if (!hasProxyToken) { + return "Connection Error - Did you add the proxy session token in Configuration?"; + } + return "Connection Error - Check if your MCP server is running and proxy token is correct"; + } case "error-connecting-to-proxy": return "Error Connecting to MCP Inspector Proxy - Check Console logs"; default: From ab062d8f9d8f4f6deb6a7ecd72df07b38b7d3afc Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 11 Jun 2025 11:58:21 +0100 Subject: [PATCH 7/9] refactor: implement split storage for inspector config - Add is_session_item field to ConfigItem type to control storage location - Store auth tokens in sessionStorage (cleared on browser close) - Store other settings in localStorage (persist between sessions) - Maintain backward compatibility with existing config structure This provides better security for auth tokens while preserving user preferences across browser sessions. --- client/src/App.tsx | 3 +- client/src/lib/configurationTypes.ts | 1 + client/src/lib/constants.ts | 5 ++ client/src/utils/configUtils.ts | 76 +++++++++++++++++++++------- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index d264a8fa..c44a0e5f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -68,6 +68,7 @@ import { getInitialCommand, getInitialArgs, initializeInspectorConfig, + saveInspectorConfig, } from "./utils/configUtils"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -226,7 +227,7 @@ const App = () => { }, [headerName]); useEffect(() => { - localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); + saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); // Auto-connect to previously saved serverURL after OAuth callback diff --git a/client/src/lib/configurationTypes.ts b/client/src/lib/configurationTypes.ts index c2024e51..263b2cf2 100644 --- a/client/src/lib/configurationTypes.ts +++ b/client/src/lib/configurationTypes.ts @@ -2,6 +2,7 @@ export type ConfigItem = { label: string; description: string; value: string | number | boolean; + is_session_item: boolean; }; /** diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index d1ca972a..922f1943 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -36,28 +36,33 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = { label: "Request Timeout", description: "Timeout for requests to the MCP server (ms)", value: 10000, + is_session_item: false, }, MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: { label: "Reset Timeout on Progress", description: "Reset timeout on progress notifications", value: true, + is_session_item: false, }, MCP_REQUEST_MAX_TOTAL_TIMEOUT: { label: "Maximum Total Timeout", description: "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)", value: 60000, + is_session_item: false, }, MCP_PROXY_FULL_ADDRESS: { label: "Inspector Proxy Address", description: "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577", value: "", + is_session_item: false, }, MCP_PROXY_AUTH_TOKEN: { label: "Proxy Session Token", description: "Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)", value: "", + is_session_item: true, }, } as const; diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index 55b29a17..3e4d3e9b 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -104,27 +104,67 @@ export const getConfigOverridesFromQueryParams = ( export const initializeInspectorConfig = ( localStorageKey: string, ): InspectorConfig => { - const savedConfig = localStorage.getItem(localStorageKey); - let baseConfig: InspectorConfig; - if (savedConfig) { - // merge default config with saved config - const mergedConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - ...JSON.parse(savedConfig), - } as InspectorConfig; + // Read persistent config from localStorage + const savedPersistentConfig = localStorage.getItem(localStorageKey); + // Read ephemeral config from sessionStorage + const savedEphemeralConfig = sessionStorage.getItem( + `${localStorageKey}_ephemeral`, + ); - // update description of keys to match the new description (in case of any updates to the default config description) - for (const [key, value] of Object.entries(mergedConfig)) { - mergedConfig[key as keyof InspectorConfig] = { - ...value, - label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, - }; - } - baseConfig = mergedConfig; - } else { - baseConfig = DEFAULT_INSPECTOR_CONFIG; + // Start with default config + let baseConfig = { ...DEFAULT_INSPECTOR_CONFIG }; + + // Apply saved persistent config + if (savedPersistentConfig) { + const parsedPersistentConfig = JSON.parse(savedPersistentConfig); + baseConfig = { ...baseConfig, ...parsedPersistentConfig }; } + + // Apply saved ephemeral config + if (savedEphemeralConfig) { + const parsedEphemeralConfig = JSON.parse(savedEphemeralConfig); + baseConfig = { ...baseConfig, ...parsedEphemeralConfig }; + } + + // Ensure all config items have the latest labels/descriptions from defaults + for (const [key, value] of Object.entries(baseConfig)) { + baseConfig[key as keyof InspectorConfig] = { + ...value, + label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, + description: + DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].description, + is_session_item: + DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].is_session_item, + }; + } + // Apply query param overrides const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG); return { ...baseConfig, ...overrides }; }; + +export const saveInspectorConfig = ( + localStorageKey: string, + config: InspectorConfig, +): void => { + const persistentConfig: Partial = {}; + const ephemeralConfig: Partial = {}; + + // Split config based on is_session_item flag + for (const [key, value] of Object.entries(config)) { + if (value.is_session_item) { + ephemeralConfig[key as keyof InspectorConfig] = value; + } else { + persistentConfig[key as keyof InspectorConfig] = value; + } + } + + // Save persistent config to localStorage + localStorage.setItem(localStorageKey, JSON.stringify(persistentConfig)); + + // Save ephemeral config to sessionStorage + sessionStorage.setItem( + `${localStorageKey}_ephemeral`, + JSON.stringify(ephemeralConfig), + ); +}; From 17740daaf5c04c2a0271de9e89931a210886a3a7 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 12 Jun 2025 17:57:41 +0100 Subject: [PATCH 8/9] fix: address @jenn-newton comments - Add authentication and origin validation middleware to /config endpoint - Implement timing-safe token comparison using crypto.timingSafeEqual() - Add config read to initial env setup s.t. /config call is authenticated --- client/src/App.tsx | 12 +++++++++--- server/src/index.ts | 32 ++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index c44a0e5f..8aca8697 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -63,6 +63,7 @@ import ToolsTab from "./components/ToolsTab"; import { InspectorConfig } from "./lib/configurationTypes"; import { getMCPProxyAddress, + getMCPProxyAuthToken, getInitialSseUrl, getInitialTransportType, getInitialCommand, @@ -345,7 +346,13 @@ const App = () => { }, [sseUrl]); useEffect(() => { - fetch(`${getMCPProxyAddress(config)}/config`) + const headers: HeadersInit = {}; + const proxyAuthToken = getMCPProxyAuthToken(config); + if (proxyAuthToken) { + headers['Authorization'] = `Bearer ${proxyAuthToken}`; + } + + fetch(`${getMCPProxyAddress(config)}/config`, { headers }) .then((response) => response.json()) .then((data) => { setEnv(data.defaultEnvironment); @@ -359,8 +366,7 @@ const App = () => { .catch((error) => console.error("Error fetching default environment:", error), ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [config]); useEffect(() => { rootsRef.current = roots; diff --git a/server/src/index.ts b/server/src/index.ts index c82227ac..a6ca2f42 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -19,7 +19,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import express from "express"; import { findActualExecutable } from "spawn-rx"; import mcpProxy from "./mcpProxy.js"; -import { randomUUID, randomBytes } from "node:crypto"; +import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto"; const SSE_HEADERS_PASSTHROUGH = ["authorization"]; const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [ @@ -120,14 +120,38 @@ const authMiddleware = (req: express.Request, res: express.Response, next: expre return next(); } - const authHeader = req.headers.authorization; - if (!authHeader || authHeader !== `Bearer ${sessionToken}`) { + const sendUnauthorized = () => { res.status(401).json({ error: "Unauthorized", message: "Authentication required. Use the session token shown in the console when starting the server." }); + }; + + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + sendUnauthorized(); return; } + + const providedToken = authHeader.substring(7); // Remove 'Bearer ' prefix + const expectedToken = sessionToken; + + // Convert to buffers for timing-safe comparison + const providedBuffer = Buffer.from(providedToken); + const expectedBuffer = Buffer.from(expectedToken); + + // Check length first to prevent timing attacks + if (providedBuffer.length !== expectedBuffer.length) { + sendUnauthorized(); + return; + } + + // Perform timing-safe comparison + if (!timingSafeEqual(providedBuffer, expectedBuffer)) { + sendUnauthorized(); + return; + } + next(); }; @@ -444,7 +468,7 @@ app.get("/health", (req, res) => { }); }); -app.get("/config", (req, res) => { +app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => { try { res.json({ defaultEnvironment, From 4d336af7e61395f7dece56c90d6850fc5a08c414 Mon Sep 17 00:00:00 2001 From: Jerome Date: Fri, 13 Jun 2025 15:40:31 +0100 Subject: [PATCH 9/9] Bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8742cc8b..cf66aba9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.14.0", + "version": "0.14.1", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",