#!/usr/bin/env node import cors from "cors"; import { parseArgs } from "node:util"; import { parse as shellParseArgs } from "shell-quote"; import { SSEClientTransport, SseError, } from "@modelcontextprotocol/sdk/client/sse.js"; import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 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, timingSafeEqual } from "node:crypto"; const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277"; const SSE_HEADERS_PASSTHROUGH = ["authorization"]; const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [ "authorization", "mcp-session-id", "last-event-id", ]; const defaultEnvironment = { ...getDefaultEnvironment(), ...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}), }; const { values } = parseArgs({ args: process.argv.slice(2), options: { env: { type: "string", default: "" }, args: { type: "string", default: "" }, command: { type: "string", default: "" }, transport: { type: "string", default: "" }, "server-url": { type: "string", default: "" }, }, }); // 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) => { res.header("Access-Control-Expose-Headers", "mcp-session-id"); next(); }); const webAppTransports: Map = new Map(); // Web app transports by web app sessionId const serverTransports: Map = new Map(); // Server Transports by web app sessionId // Use provided token from environment or generate a new one const sessionToken = process.env.MCP_PROXY_AUTH_TOKEN || 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 defaultOrigin = `http://localhost:${clientPort}`; const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [ defaultOrigin, ]; 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(); } 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["x-mcp-proxy-auth"]; const authHeaderValue = Array.isArray(authHeader) ? authHeader[0] : authHeader; if (!authHeaderValue || !authHeaderValue.startsWith("Bearer ")) { sendUnauthorized(); return; } const providedToken = authHeaderValue.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(); }; const createTransport = async (req: express.Request): Promise => { const query = req.query; console.log("Query parameters:", JSON.stringify(query)); const transportType = query.transportType as string; if (transportType === "stdio") { const command = (query.command as string).trim(); const origArgs = shellParseArgs(query.args as string) as string[]; const queryEnv = query.env ? JSON.parse(query.env as string) : {}; const env = { ...defaultEnvironment, ...process.env, ...queryEnv }; const { cmd, args } = findActualExecutable(command, origArgs); console.log(`STDIO transport: command=${cmd}, args=${args}`); const transport = new StdioClientTransport({ command: cmd, args, env, stderr: "pipe", }); await transport.start(); return transport; } else if (transportType === "sse") { const url = query.url as string; const headers = getHttpHeaders(req, transportType); console.log( `SSE transport: url=${url}, headers=${JSON.stringify(headers)}`, ); const transport = new SSEClientTransport(new URL(url), { eventSourceInit: { fetch: (url, init) => fetch(url, { ...init, headers }), }, requestInit: { headers, }, }); await transport.start(); return transport; } else if (transportType === "streamable-http") { const headers = getHttpHeaders(req, transportType); const transport = new StreamableHTTPClientTransport( new URL(query.url as string), { requestInit: { headers, }, }, ); await transport.start(); return transport; } else { console.error(`Invalid transport type: ${transportType}`); throw new Error("Invalid transport type specified"); } }; 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 { const transport = webAppTransports.get( sessionId, ) as StreamableHTTPServerTransport; if (!transport) { res.status(404).end("Session not found"); return; } else { await transport.handleRequest(req, res); } } catch (error) { console.error("Error in /mcp route:", error); res.status(500).json(error); } }, ); app.post( "/mcp", originValidationMiddleware, authMiddleware, async (req, res) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; let serverTransport: Transport | undefined; if (!sessionId) { try { console.log("New StreamableHttp connection request"); try { serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( "Received 401 Unauthorized from MCP server:", error.message, ); res.status(401).json(error); return; } throw error; } console.log("Created StreamableHttp server transport"); const webAppTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID, onsessioninitialized: (sessionId) => { webAppTransports.set(sessionId, webAppTransport); serverTransports.set(sessionId, serverTransport!); console.log("Client <-> Proxy sessionId: " + sessionId); }, }); console.log("Created StreamableHttp client transport"); await webAppTransport.start(); mcpProxy({ transportToClient: webAppTransport, transportToServer: serverTransport, }); await (webAppTransport as StreamableHTTPServerTransport).handleRequest( req, res, req.body, ); } catch (error) { console.error("Error in /mcp POST route:", error); res.status(500).json(error); } } else { console.log(`Received POST message for sessionId ${sessionId}`); try { const transport = webAppTransports.get( sessionId, ) as StreamableHTTPServerTransport; if (!transport) { res.status(404).end("Transport not found for sessionId " + sessionId); } else { await (transport as StreamableHTTPServerTransport).handleRequest( req, res, ); } } catch (error) { console.error("Error in /mcp route:", error); res.status(500).json(error); } } }, ); 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; if (sessionId) { try { serverTransport = serverTransports.get( sessionId, ) as StreamableHTTPClientTransport; if (!serverTransport) { res.status(404).end("Transport not found for sessionId " + sessionId); } else { await ( serverTransport as StreamableHTTPClientTransport ).terminateSession(); webAppTransports.delete(sessionId); serverTransports.delete(sessionId); console.log(`Transports removed for sessionId ${sessionId}`); } res.status(200).end(); } catch (error) { console.error("Error in /mcp route:", error); res.status(500).json(error); } } }, ); app.get( "/stdio", originValidationMiddleware, authMiddleware, async (req, res) => { try { console.log("New STDIO connection request"); let serverTransport: Transport | undefined; try { serverTransport = await createTransport(req); console.log("Created server transport"); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( "Received 401 Unauthorized from MCP server. Authentication failure.", ); res.status(401).json(error); return; } throw error; } const proxyFullAddress = (req.query.proxyFullAddress as string) || ""; const prefix = proxyFullAddress || ""; const endpoint = `${prefix}/message`; const webAppTransport = new SSEServerTransport(endpoint, res); console.log("Created client transport"); webAppTransports.set(webAppTransport.sessionId, webAppTransport); serverTransports.set(webAppTransport.sessionId, serverTransport); await webAppTransport.start(); (serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => { if (chunk.toString().includes("MODULE_NOT_FOUND")) { // Server command not found, remove transports const message = "Command not found, transports removed"; webAppTransport.send({ jsonrpc: "2.0", method: "notifications/message", params: { level: "emergency", logger: "proxy", data: { message, }, }, }); webAppTransport.close(); serverTransport.close(); webAppTransports.delete(webAppTransport.sessionId); serverTransports.delete(webAppTransport.sessionId); console.error(message); } else { // Inspect message and attempt to assign a RFC 5424 Syslog Protocol level let level; let message = chunk.toString().trim(); let ucMsg = chunk.toString().toUpperCase(); if (ucMsg.includes("DEBUG")) { level = "debug"; } else if (ucMsg.includes("INFO")) { level = "info"; } else if (ucMsg.includes("NOTICE")) { level = "notice"; } else if (ucMsg.includes("WARN")) { level = "warning"; } else if (ucMsg.includes("ERROR")) { level = "error"; } else if (ucMsg.includes("CRITICAL")) { level = "critical"; } else if (ucMsg.includes("ALERT")) { level = "alert"; } else if (ucMsg.includes("EMERGENCY")) { level = "emergency"; } else if (ucMsg.includes("SIGINT")) { message = "SIGINT received. Server shutdown."; level = "emergency"; } else if (ucMsg.includes("SIGHUP")) { message = "SIGHUP received. Server shutdown."; level = "emergency"; } else if (ucMsg.includes("SIGTERM")) { message = "SIGTERM received. Server shutdown."; level = "emergency"; } else { level = "info"; } webAppTransport.send({ jsonrpc: "2.0", method: "notifications/message", params: { level, logger: "stdio", data: { message, }, }, }); } }); mcpProxy({ transportToClient: webAppTransport, transportToServer: serverTransport, }); } catch (error) { console.error("Error in /stdio route:", error); res.status(500).json(error); } }, ); 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", ); let serverTransport: Transport | undefined; try { serverTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( "Received 401 Unauthorized from MCP server. Authentication failure.", ); res.status(401).json(error); return; } else if (error instanceof SseError && error.code === 404) { console.error( "Received 404 not found from MCP server. Does the MCP server support SSE?", ); res.status(404).json(error); return; } else if (JSON.stringify(error).includes("ECONNREFUSED")) { console.error("Connection refused. Is the MCP server running?"); res.status(500).json(error); } else { throw error; } } if (serverTransport) { const proxyFullAddress = (req.query.proxyFullAddress as string) || ""; const prefix = proxyFullAddress || ""; const endpoint = `${prefix}/message`; const webAppTransport = new SSEServerTransport(endpoint, res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); console.log("Created client transport"); serverTransports.set(webAppTransport.sessionId, serverTransport!); console.log("Created server transport"); await webAppTransport.start(); mcpProxy({ transportToClient: webAppTransport, transportToServer: serverTransport, }); } } catch (error) { console.error("Error in /sse route:", error); res.status(500).json(error); } }, ); app.post( "/message", originValidationMiddleware, authMiddleware, async (req, res) => { try { const sessionId = req.query.sessionId; console.log(`Received POST message for sessionId ${sessionId}`); const transport = webAppTransports.get( sessionId as string, ) as SSEServerTransport; if (!transport) { res.status(404).end("Session not found"); return; } await transport.handlePostMessage(req, res); } catch (error) { console.error("Error in /message route:", error); res.status(500).json(error); } }, ); app.get("/health", (req, res) => { res.json({ status: "ok", }); }); app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => { try { res.json({ defaultEnvironment, defaultCommand: values.command, defaultArgs: values.args, defaultTransport: values.transport, defaultServerUrl: values["server-url"], }); } catch (error) { console.error("Error in /config route:", error); res.status(500).json(error); } }); const PORT = parseInt( process.env.SERVER_PORT || DEFAULT_MCP_PROXY_LISTEN_PORT, 10, ); const HOST = process.env.HOST || "localhost"; const server = app.listen(PORT, HOST); server.on("listening", () => { console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`); if (!authDisabled) { console.log( `🔑 Session token: ${sessionToken}\n ` + `Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`, ); } else { console.log( `⚠️ WARNING: Authentication is disabled. This is not recommended.`, ); } }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { console.error(`❌ Proxy Server PORT IS IN USE at port ${PORT} ❌ `); } else { console.error(err.message); } process.exit(1); });