Repository navigation
Inspector V2 Proxy Spec #986
Description
Activity
- added a commit that references this issue
on Jan 20, 2026 Current Proxy Functionality
What the current Express-based MCP Inspector proxy implements
server/src/index.ts— the HTTP “front door” + transport/session orchestrationThis file is the actual proxy server process. It:
-
Bootstraps a CLI-ish runtime configuration (lines
32–48,790–817)- Reads CLI flags via
parseArgs:--env,--args,--command,--transport,--server-url. - Builds
defaultEnvironmentfrom MCP SDK defaults plus optionalMCP_ENV_VARSJSON (lines34–37). - Chooses
PORT(envSERVER_PORTelse6277) andHOST(envHOSTelselocalhost) and starts listening.
- Reads CLI flags via
-
Sets up basic CORS + exposes a response header needed by the client (lines
166–171)cors()middleware.- Adds
Access-Control-Expose-Headers: mcp-session-idso browser clients can read the session header.
-
Maintains proxy session state in-memory (lines
173–175)webAppTransports: Map<sessionId, Transport>: the transport facing the Inspector UI.serverTransports: Map<sessionId, Transport>: the transport facing the actual MCP server (or stdio child process).sessionHeaderHolders: Map<sessionId, { headers: HeadersInit }>: mutable header containers so “dynamic headers” can be updated over time.
-
Implements security controls
4.1 DNS rebinding / origin allowlist guard (lines
182–207)- Checks
Originagainst:ALLOWED_ORIGINSenv (comma-separated), or- default
http://localhost:${CLIENT_PORT || 6274}.
- Rejects with
403and a JSON error if origin is not allowed.
4.2 Per-proxy authentication via bearer token (lines
177–256)- Generates a
sessionToken(random 32 bytes hex) unlessMCP_PROXY_AUTH_TOKENis provided. - Can be disabled with
DANGEROUSLY_OMIT_AUTH. - Expects
x-mcp-proxy-auth: Bearer <token>. - Uses
timingSafeEqualand length checks to avoid timing attacks.
- Checks
-
Normalizes and forwards headers from browser→proxy→MCP server
5.1 Header allowlist extraction (lines
66–139)- Forwards only:
- headers whose name starts with
mcp-, and authorization, andlast-event-id.
- headers whose name starts with
- Explicitly excludes:
x-mcp-proxy-auth(proxy’s own auth),mcp-session-id(client↔proxy session routing header).
- Supports “custom auth header name” patterns:
x-custom-auth-header: "X-Whatever"then forwards that header’s value.x-custom-auth-headers: "[\"X-A\",\"X-B\"]"forwards multiple.
5.2 In-place header mutation (lines
141–164)updateHeadersInPlace()mutates the existing object so SDK transports holding a reference see updates.- Preserves the original
Acceptheader because it’s set at transport creation time.
- Forwards only:
-
Bridges Node fetch / streams vs browser-style fetch expectations
6.1 Node stream → Web
ReadableStreamadapter (lines258–289)- Used for SSE responses because the EventSource polyfill expects web streams.
6.2 Custom
fetchthat merges dynamic session headers + request-specific headers (lines291–354)- Starts with mutable “session headers” (
headerHolder.headers). - Overlays request-specific headers from SDK (preserves
Content-Typeetc.). - For SSE (
Accept: text/event-stream), wrapsresponse.bodywith a web stream.
-
Creates an MCP client transport toward the real MCP server (or stdio) (lines
356–425)- Driven by query param
transportType:
7.1
transportType=stdio(lines367–385)- Reads
command,args,envfrom query. - Uses
shell-quoteto parse args andspawn-rxto locate executable. - Creates
StdioClientTransportwithstderr: "pipe"and starts it.
7.2
transportType=sse(lines386–407)- Creates
SSEClientTransportto an MCP server SSE endpoint. - Sets
Accept: text/event-stream. - Uses
createCustomFetch()so auth headers/etc. can be dynamically updated.
7.3
transportType=streamable-http(lines407–420)- Creates
StreamableHTTPClientTransport. - Sets
Accept: text/event-stream, application/json. - Also uses
createCustomFetch()for dynamic headers.
- Driven by query param
-
Implements the HTTP API surface consumed by the Inspector client
8.1 Streamable HTTP proxy endpoint:
GET /mcp,POST /mcp,DELETE /mcp(lines427–573)-
All three are guarded by origin + auth middleware.
-
GET /mcp(lines427–458)- Requires
mcp-session-idheader. - Updates dynamic headers for that session.
- Delegates to
StreamableHTTPServerTransport.handleRequest(req,res).
- Requires
-
POST /mcphas two modes (lines460–542):- Existing session (when
mcp-session-idheader is present):- Updates dynamic headers.
- Delegates to
StreamableHTTPServerTransport.handleRequest(req,res).
- New session (no
mcp-session-idheader):- Calls
createTransport(req)to create the upstream “server transport”. - Creates a new
StreamableHTTPServerTransportfor the browser client. - On session init:
- stores both transports in the maps,
- stores
headerHolderfor future header updates.
- Wires the two transports together via
mcpProxy({ transportToClient, transportToServer }). - Calls
handleRequest(req,res,req.body)to process the initiating request.
- Calls
- Special error handling for 401 (lines
530–540) usingis401Error().
- Existing session (when
-
DELETE /mcp(lines545–573)- Requires
mcp-session-id. - Terminates and closes the upstream
StreamableHTTPClientTransportsession. - Deletes all session state from the maps.
- Requires
8.2 Legacy/alternate Inspector endpoints (SSE + stdio)
-
GET /stdio(lines576–682)- Creates upstream
stdiotransport viacreateTransport(). - Creates downstream
SSEServerTransportto the browser at${proxyFullAddress}/message. - Stores transports in session maps.
- Starts the SSE server transport.
- Streams
stderrfrom the stdio process and converts lines into JSON-RPCnotifications/messagewith a syslog-ish severity mapping (lines598–664).- Special case: if stderr contains
MODULE_NOT_FOUND, the proxy sends an emergency notification, closes transports, and cleans session state.
- Special case: if stderr contains
- Bridges client/server via
mcpProxy().
- Creates upstream
-
GET /sse(lines684–737)- Marked deprecated (“replaced by StreamableHttp”).
- Creates upstream
SSEClientTransportand downstreamSSEServerTransport. - Stores session/header state.
- Bridges via
mcpProxy(). - Has extra error mapping for SSE:
401,404(“does the MCP server support SSE?”),ECONNREFUSED.
-
POST /message(lines739–767)- Handles the browser client POST side-channel used by
SSEServerTransport. - Session is via query param
sessionId. - Updates dynamic headers.
- Delegates to
SSEServerTransport.handlePostMessage(req,res).
- Handles the browser client POST side-channel used by
8.3 Operational endpoints
GET /health(lines769–773):{ status: "ok" }.GET /config(lines775–788): returns the defaults used by the Inspector UI.
-
-
Logging / observability behaviors
- Logs query parameters for transport creation (line
363). - Logs session IDs for client↔proxy and proxy↔server.
- Logs explicit warnings about deprecated SSE.
- Prints the auth token at startup unless auth is disabled.
- Logs query parameters for transport creation (line
-
HTTP error mapping and response shaping
is401Error()(lines50–64) normalizes different SDK error types to decide when to respond401.- Many route handlers catch and respond
500with the error JSON.
server/src/mcpProxy.ts— the “transport bridge” (proxy core)This file is deliberately small and is essentially a bidirectional message pump:
-
Forwards client → server messages (lines
30–48)transportToClient.onmessage = (message) => transportToServer.send(message).- If
send()fails and the original message was a JSON-RPC request (isJSONRPCRequest) and the client connection is still open, it manufactures a JSON-RPC error response back to the client with code-32001and includes the original error in.error.data.
-
Forwards server → client messages (lines
50–61)- First time only: if
transportToServer.sessionIdexists (typically Streamable HTTP), logs it. - Forwards every incoming message to the inspector client transport.
- First time only: if
-
Symmetric close propagation with loop prevention (lines
63–78)- When one side closes, it closes the other side.
- Uses
transportToClientClosed/transportToServerClosedflags to avoid a close cascade loop.
-
Centralized error logging (lines
4–16,80–81)- Client errors: always logged as “Error from inspector client”.
- Server errors: special cases for
ECONNREFUSEDand404, else generic.
In short:
index.tsis “HTTP server + transport/session lifecycle”, andmcpProxy.tsis “pure message/close/error forwarding between twoTransports”.Refactoring to Hono
What will likely change when rebuilding on Hono instead of Express
Hono is Fetch-API-centric (
Request/Response) and generally pushes you toward smaller, composable handlers. That impacts this proxy in a few concrete places:-
Middleware and request/response types change
- Express:
(req, res, next)with mutablereq/res. - Hono:
async (c, next)wherec.reqwraps a FetchRequestand you return aResponse. - Consequence: places that call SDK helpers expecting Node/Express objects (notably
StreamableHTTPServerTransport.handleRequest(req,res)andSSEServerTransportconstruction) may need:- an adapter layer, or
- to run Hono on a Node adapter that exposes Node
IncomingMessage/ServerResponse, or - to change how you integrate the SDK server transports.
- Express:
-
Streaming/SSE handling is different
- Express exposes
resas a NodeServerResponsewhich is what your currentSSEServerTransport(endpoint, res)uses. - Hono supports streaming responses, but the integration surface is different (often via
stream()/c.body()withReadableStream). - Consequence: the “downstream” transports (
SSEServerTransport,StreamableHTTPServerTransport.handleRequest) may need wrappers.
- Express exposes
-
Body parsing and raw bodies
- Express often needs body-parser middleware; here you rely on SDK
handleRequestto read the body (and you passreq.bodyin one case). - Hono typically uses
await c.req.json()etc. - Consequence: you’ll want to be explicit about:
- which routes must not pre-consume the body before the SDK does,
- how to preserve raw body for the SDK transport handler.
- Express often needs body-parser middleware; here you rely on SDK
-
Header casing/iteration differences
- Express
req.headersis a plain object with lowercased keys; multi-value headers can be arrays. - Fetch
Headersis an iterator; multi-values are combined. - Consequence:
getHttpHeaders()will change implementation:- likely iterate over
c.req.raw.headersorc.req.header(); - decide how to represent repeated headers (if needed).
- likely iterate over
- Express
-
CORS and
Access-Control-Expose-Headers- In Express you do
cors()+ manual header. - In Hono you’d typically use
cors()middleware fromhono/corsand setexposeHeaders: ['mcp-session-id'].
- In Express you do
-
Auth and origin protection remain the same conceptually but become cleaner
- Both are straightforward Hono middleware.
- You’ll likely replace
res.status(403).json(...)withreturn c.json(..., 403).
-
The node-fetch / stream bridging may simplify
- Today you’re forced to adapt node-fetch + Node streams for SSE.
- In a Hono/Fetch-first setup, you may be able to use native
fetchand web streams end-to-end (depending on Node version/runtime), reducingcreateWebReadableStream()and parts ofcreateCustomFetch(). - Caveat: the MCP SDK transports you’re using may still rely on node-fetch behavior; you’d confirm by checking which runtime they target.
Separation of Concerns
Where separation of concerns exists today (and where it’s still tangled)
Even in Express form, you can already see natural boundaries:
-
Security middleware
originValidationMiddleware(lines182–207)authMiddleware(lines209–256)- These can become reusable Hono middlewares.
-
Header-forwarding policy
getHttpHeaders()and “custom auth headers” logic (lines66–139).- This is business logic independent of Express/Hono; only the “read headers from request” part is framework-specific.
-
Dynamic header lifecycle
sessionHeaderHolders+updateHeadersInPlace()+createCustomFetch().- This is “session state + request decorator” logic.
-
Transport construction (“upstream transport factory”)
createTransport()chooses stdio vs sse vs streamable-http (lines356–425).- This can be extracted behind an interface like
TransportFactory.createFromRequest(...).
-
Session registry and cleanup
- The three
Maps plus all theset/deletecalls are currently spread across route handlers. - This is the biggest current cross-cutting concern and a prime candidate to centralize.
- The three
-
Protocol bridging
mcpProxy.tsis already cleanly separated and should remain largely unchanged.
Modularization
A practical modularization target (useful before and during the Hono rewrite)
If your goal is “more modular and extendable”, a good decomposition (independent of Express vs Hono) is:
-
config/- Parse CLI args, build
defaultEnvironment, computeallowedOrigins, auth token config.
- Parse CLI args, build
-
middleware/originGuard(allowedOrigins)proxyAuth({ token, disabled })cors(exposeHeaders: ['mcp-session-id'])
-
headers/extractForwardHeaders(request): Record<string,string>applyDynamicHeaders(holder, request)- Framework adapters:
extractFromExpress(req)vsextractFromHono(c.req).
-
sessions/SessionManager- Own the three maps.
- API like:
createSession({ webTransport, serverTransport, headerHolder? })getSession(sessionId)closeSession(sessionId)updateSessionHeaders(sessionId, newHeaders)
- This removes repeated cleanup logic and makes testing possible.
-
transports/createUpstreamTransport({ type, url, command, args, env, forwardHeaders })createDownstreamTransport(...)(SSE vs StreamableHTTP server side)
-
routes/- Route handlers become thin orchestration layers calling the above modules.
-
proxy/mcpProxy- Keep as-is, or enhance with pluggable error mapping.
This separation also highlights what may need adapters in Hono: the SDK server transports (
StreamableHTTPServerTransport,SSEServerTransport) are currently invoked with Express/Node response objects.Preserve This
Key “behavioral differences” to watch when porting
- Session identification: Streamable HTTP relies on
mcp-session-idheader; SSE/messagerelies on querysessionId. Keep this consistent. - Dynamic auth header forwarding: the
x-custom-auth-header(s)behavior is part of your product surface; preserve it unless you want a breaking change. - Error semantics:
is401Error()is a compatibility layer across SDK transport implementations; keep something equivalent. - Streaming correctness: SSE and Streamable HTTP require correct streaming behavior and no premature body consumption.
-
Note that the V2 TypeScript SDK has middleware for hono, express, and node, so the adapters identified in the above analysis may not be necessary for us to write.
Just adding a note on the MCP Apps use case - may complicate the discussion about 'proxy server' vs 'its just an API for the client.'
Need to consider the separate ports needed for the secure sandboxing. Bob mentioned that he just uses an ephemeral express server on a random point, on-demand (for oauth testing but could apply here too).
Reacted by Cliff HallThis has been addressed by #1027
Inspector V2 Proxy Spec
configEndpointGET
POST
More to come ...