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.
This commit is contained in:
Felix Weinberger 2025-06-10 14:05:17 +01:00
commit 111e282386
No known key found for this signature in database
2 changed files with 66 additions and 7 deletions

View file

@ -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:

View file

@ -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<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId
const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // 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<Transport> => {
const query = req.query;
console.log("Query parameters:", JSON.stringify(query));
@ -150,7 +169,7 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
}
};
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`)) {