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`)) {