Skip to content

Inspector V2 Proxy Spec #986

Description

@cliffhall

Inspector V2 Proxy Spec

config Endpoint

GET

  • Serves a JSON config shape
    • Minimizes environment variable config for client config
    • Use a validatable JSON config file

POST

  • Can also write the same validated shape
    • e.g., The client can add a new server configuration or store startup preferences

More to come ...

Activity

  1. added theissue type on Dec 18, 2025
  2. cliffhall commented on Jan 24, 2026

    @cliffhall
    MemberAuthor

    Current Proxy Functionality

    What the current Express-based MCP Inspector proxy implements

    server/src/index.ts — the HTTP “front door” + transport/session orchestration

    This file is the actual proxy server process. It:

    1. Bootstraps a CLI-ish runtime configuration (lines 32–48, 790–817)

      • Reads CLI flags via parseArgs: --env, --args, --command, --transport, --server-url.
      • Builds defaultEnvironment from MCP SDK defaults plus optional MCP_ENV_VARS JSON (lines 34–37).
      • Chooses PORT (env SERVER_PORT else 6277) and HOST (env HOST else localhost) and starts listening.
    2. Sets up basic CORS + exposes a response header needed by the client (lines 166–171)

      • cors() middleware.
      • Adds Access-Control-Expose-Headers: mcp-session-id so browser clients can read the session header.
    3. 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.
    4. Implements security controls

      4.1 DNS rebinding / origin allowlist guard (lines 182–207)

      • Checks Origin against:
        • ALLOWED_ORIGINS env (comma-separated), or
        • default http://localhost:${CLIENT_PORT || 6274}.
      • Rejects with 403 and 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) unless MCP_PROXY_AUTH_TOKEN is provided.
      • Can be disabled with DANGEROUSLY_OMIT_AUTH.
      • Expects x-mcp-proxy-auth: Bearer <token>.
      • Uses timingSafeEqual and length checks to avoid timing attacks.
    5. 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, and
        • last-event-id.
      • 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 Accept header because it’s set at transport creation time.
    6. Bridges Node fetch / streams vs browser-style fetch expectations

      6.1 Node stream → Web ReadableStream adapter (lines 258–289)

      • Used for SSE responses because the EventSource polyfill expects web streams.

      6.2 Custom fetch that merges dynamic session headers + request-specific headers (lines 291–354)

      • Starts with mutable “session headers” (headerHolder.headers).
      • Overlays request-specific headers from SDK (preserves Content-Type etc.).
      • For SSE (Accept: text/event-stream), wraps response.body with a web stream.
    7. Creates an MCP client transport toward the real MCP server (or stdio) (lines 356–425)

      • Driven by query param transportType:

      7.1 transportType=stdio (lines 367–385)

      • Reads command, args, env from query.
      • Uses shell-quote to parse args and spawn-rx to locate executable.
      • Creates StdioClientTransport with stderr: "pipe" and starts it.

      7.2 transportType=sse (lines 386–407)

      • Creates SSEClientTransport to 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 (lines 407–420)

      • Creates StreamableHTTPClientTransport.
      • Sets Accept: text/event-stream, application/json.
      • Also uses createCustomFetch() for dynamic headers.
    8. Implements the HTTP API surface consumed by the Inspector client

      8.1 Streamable HTTP proxy endpoint: GET /mcp, POST /mcp, DELETE /mcp (lines 427–573)

      • All three are guarded by origin + auth middleware.

      • GET /mcp (lines 427–458)

        • Requires mcp-session-id header.
        • Updates dynamic headers for that session.
        • Delegates to StreamableHTTPServerTransport.handleRequest(req,res).
      • POST /mcp has two modes (lines 460–542):

        1. Existing session (when mcp-session-id header is present):
          • Updates dynamic headers.
          • Delegates to StreamableHTTPServerTransport.handleRequest(req,res).
        2. New session (no mcp-session-id header):
          • Calls createTransport(req) to create the upstream “server transport”.
          • Creates a new StreamableHTTPServerTransport for the browser client.
          • On session init:
            • stores both transports in the maps,
            • stores headerHolder for future header updates.
          • Wires the two transports together via mcpProxy({ transportToClient, transportToServer }).
          • Calls handleRequest(req,res,req.body) to process the initiating request.
        • Special error handling for 401 (lines 530–540) using is401Error().
      • DELETE /mcp (lines 545–573)

        • Requires mcp-session-id.
        • Terminates and closes the upstream StreamableHTTPClientTransport session.
        • Deletes all session state from the maps.

      8.2 Legacy/alternate Inspector endpoints (SSE + stdio)

      • GET /stdio (lines 576–682)

        • Creates upstream stdio transport via createTransport().
        • Creates downstream SSEServerTransport to the browser at ${proxyFullAddress}/message.
        • Stores transports in session maps.
        • Starts the SSE server transport.
        • Streams stderr from the stdio process and converts lines into JSON-RPC notifications/message with a syslog-ish severity mapping (lines 598–664).
          • Special case: if stderr contains MODULE_NOT_FOUND, the proxy sends an emergency notification, closes transports, and cleans session state.
        • Bridges client/server via mcpProxy().
      • GET /sse (lines 684–737)

        • Marked deprecated (“replaced by StreamableHttp”).
        • Creates upstream SSEClientTransport and downstream SSEServerTransport.
        • Stores session/header state.
        • Bridges via mcpProxy().
        • Has extra error mapping for SSE: 401, 404 (“does the MCP server support SSE?”), ECONNREFUSED.
      • POST /message (lines 739–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).

      8.3 Operational endpoints

      • GET /health (lines 769–773): { status: "ok" }.
      • GET /config (lines 775–788): returns the defaults used by the Inspector UI.
    9. 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.
    10. HTTP error mapping and response shaping

    • is401Error() (lines 50–64) normalizes different SDK error types to decide when to respond 401.
    • Many route handlers catch and respond 500 with the error JSON.

    server/src/mcpProxy.ts — the “transport bridge” (proxy core)

    This file is deliberately small and is essentially a bidirectional message pump:

    1. 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 -32001 and includes the original error in .error.data.
    2. Forwards server → client messages (lines 50–61)

      • First time only: if transportToServer.sessionId exists (typically Streamable HTTP), logs it.
      • Forwards every incoming message to the inspector client transport.
    3. Symmetric close propagation with loop prevention (lines 63–78)

      • When one side closes, it closes the other side.
      • Uses transportToClientClosed / transportToServerClosed flags to avoid a close cascade loop.
    4. Centralized error logging (lines 4–16, 80–81)

      • Client errors: always logged as “Error from inspector client”.
      • Server errors: special cases for ECONNREFUSED and 404, else generic.

    In short: index.ts is “HTTP server + transport/session lifecycle”, and mcpProxy.ts is “pure message/close/error forwarding between two Transports”.

    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:

    1. Middleware and request/response types change

      • Express: (req, res, next) with mutable req/res.
      • Hono: async (c, next) where c.req wraps a Fetch Request and you return a Response.
      • Consequence: places that call SDK helpers expecting Node/Express objects (notably StreamableHTTPServerTransport.handleRequest(req,res) and SSEServerTransport construction) 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.
    2. Streaming/SSE handling is different

      • Express exposes res as a Node ServerResponse which is what your current SSEServerTransport(endpoint, res) uses.
      • Hono supports streaming responses, but the integration surface is different (often via stream() / c.body() with ReadableStream).
      • Consequence: the “downstream” transports (SSEServerTransport, StreamableHTTPServerTransport.handleRequest) may need wrappers.
    3. Body parsing and raw bodies

      • Express often needs body-parser middleware; here you rely on SDK handleRequest to read the body (and you pass req.body in 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.
    4. Header casing/iteration differences

      • Express req.headers is a plain object with lowercased keys; multi-value headers can be arrays.
      • Fetch Headers is an iterator; multi-values are combined.
      • Consequence: getHttpHeaders() will change implementation:
        • likely iterate over c.req.raw.headers or c.req.header();
        • decide how to represent repeated headers (if needed).
    5. CORS and Access-Control-Expose-Headers

      • In Express you do cors() + manual header.
      • In Hono you’d typically use cors() middleware from hono/cors and set exposeHeaders: ['mcp-session-id'].
    6. Auth and origin protection remain the same conceptually but become cleaner

      • Both are straightforward Hono middleware.
      • You’ll likely replace res.status(403).json(...) with return c.json(..., 403).
    7. 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 fetch and web streams end-to-end (depending on Node version/runtime), reducing createWebReadableStream() and parts of createCustomFetch().
      • 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:

    1. Security middleware

      • originValidationMiddleware (lines 182–207)
      • authMiddleware (lines 209–256)
      • These can become reusable Hono middlewares.
    2. Header-forwarding policy

      • getHttpHeaders() and “custom auth headers” logic (lines 66–139).
      • This is business logic independent of Express/Hono; only the “read headers from request” part is framework-specific.
    3. Dynamic header lifecycle

      • sessionHeaderHolders + updateHeadersInPlace() + createCustomFetch().
      • This is “session state + request decorator” logic.
    4. Transport construction (“upstream transport factory”)

      • createTransport() chooses stdio vs sse vs streamable-http (lines 356–425).
      • This can be extracted behind an interface like TransportFactory.createFromRequest(...).
    5. Session registry and cleanup

      • The three Maps plus all the set/delete calls are currently spread across route handlers.
      • This is the biggest current cross-cutting concern and a prime candidate to centralize.
    6. Protocol bridging

      • mcpProxy.ts is 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:

    1. config/

      • Parse CLI args, build defaultEnvironment, compute allowedOrigins, auth token config.
    2. middleware/

      • originGuard(allowedOrigins)
      • proxyAuth({ token, disabled })
      • cors(exposeHeaders: ['mcp-session-id'])
    3. headers/

      • extractForwardHeaders(request): Record<string,string>
      • applyDynamicHeaders(holder, request)
      • Framework adapters: extractFromExpress(req) vs extractFromHono(c.req).
    4. 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.
    5. transports/

      • createUpstreamTransport({ type, url, command, args, env, forwardHeaders })
      • createDownstreamTransport(...) (SSE vs StreamableHTTP server side)
    6. routes/

      • Route handlers become thin orchestration layers calling the above modules.
    7. 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-id header; SSE /message relies on query sessionId. 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.
  3. cliffhall commented on Jan 24, 2026

    @cliffhall
    MemberAuthor

    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.

  4. olaservo commented on Feb 4, 2026

    @olaservo
    Member
  5. olaservo commented on Feb 4, 2026

    @olaservo
    Member

    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).

  6. cliffhall commented on Mar 7, 2026

    @cliffhall
    MemberAuthor

    This has been addressed by #1027

  7. added this to the v2.0.0 milestone on Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions