MCP Proxy

repository·main·Indexed 42 days ago

https://github.com/punkpeye/mcp-proxy

A TypeScript SSE proxy for Model Context Protocol (MCP) servers that use stdio transport. It acts as a bridge between web-based clients using HTTP/SSE and local stdio MCP servers, supporting both 2025-era and 2026-07-28 protocol revisions. Key features include CORS support, connection multiplexing, stateless mode, and API key authentication.

Tokens
12K
Snippets
45
Records
48
Agent score
54%

What's inside MCP Proxy

  1. What is MCP Proxy

    main

    MCP Proxy is a TypeScript-based proxy that provides streamable HTTP and SSE (Server-Sent Events) interfaces for Model Context Protocol (MCP) servers that originally use stdio transport. It allows web-based or remote clients to interact with local stdio MCP servers.

    Key features:

    • Protocol Versatility: Serves both 2025-era revisions and the 2026-07-28 revision from the same endpoint.
    • CORS Enabled: CORS is enabled by default and is configurable.
    • Multiplexing: It multiplexes multiple downstream connections onto a single upstream connection.
    • Integration: It is the underlying technology used by FastMCP to enable streamable HTTP and SSE.
  2. Use Stateless Mode

    main

    By default, each client connection maintains a server instance for the life of its session. Enabling --stateless creates a fresh server instance per request. This is ideal for serverless platforms or load-balanced deployments where you want to avoid holding connection state.

    Note: Stateless mode only affects the HTTP streamable transport (/mcp endpoint). SSE transport behavior remains unchanged.

    npx mcp-proxy --port 8080 --stateless -- tsx server.js
    
    # stream-only transport
    npx mcp-proxy --port 8080 --stateless --server stream -- tsx server.js
  3. Understand proxy limitations (What does not cross the proxy)

    main

    Because MCP Proxy multiplexes many downstream connections onto a single upstream connection, certain features are limited or unavailable:

    • Client Input/Sampling: Anything requiring client input (elicitation, sampling, roots) is not currently supported because the proxy cannot reliably attribute a request for input to a specific downstream caller in the 2025-era.
    • Per-request Logging (2026-07-28): The proxy cannot attribute logs to specific requests or callers. 2025-era clients receive unsolicited logs from the upstream, but 2026-07-28 per-request logging is not supported.
    • Upstream logging/setLevel: While the proxy honors log levels per downstream connection, it cannot pass these commands to the upstream. The shared upstream connection maintains a single log level.
    • Client Identity: The upstream server sees mcp-proxy as the client, not the individual downstream callers. This affects any telemetry or identity-based logic on the server.
    • 2026-07-28 over stdio: The startStdioServer command only serves 2025-era clients; modern protocol support is only available via the HTTP endpoints.
  4. How MCP Proxy handles protocol revisions

    main

    MCP Proxy is designed to be backward compatible, serving both 2025-era clients and 2026-07-28 clients simultaneously.

    Revision Detection

    • 2026-07-28 clients: Identified by the presence of a _meta envelope in every request. These use per-request streamable HTTP.
    • 2025-era clients: Identified by the absence of a _meta envelope. These use sessionful streamable HTTP or the /sse (HTTP+SSE) endpoint.

    Upstream Negotiation

    While the proxy serves multiple eras downstream, the connection it spawns to your actual MCP server (the upstream) negotiates its own revision. By default, it uses the 2025 initialize handshake. You can control this behavior using the --upstreamProtocol flag.

    Protocol Support Matrix

    Feature2025-era clients2026-07-28 clients
    /mcp (streamable)Yes (sessionful)Yes (per-request)
    /sse (HTTP+SSE)YesN/A
    stdio (startStdioServer)YesNot yet
    Change notificationsUnsolicitedsubscriptions/listen stream
    # Front a 2026-07-28 stdio server, serving both eras downstream
    npx mcp-proxy --upstreamProtocol auto -- my-modern-mcp-server
  5. Quickstart: Run MCP Proxy via CLI

    main

    You can run mcp-proxy using npx to wrap an existing MCP server.

    Without options: If you don't need to configure the proxy, you can pass the command directly.

    With options: Use the -- separator to distinguish between mcp-proxy flags and the command you want to execute. This is essential if your command also uses flags that might conflict with the proxy's options.

    Example: Spawning a server via tsx and serving it on port 8080 with both SSE and streamable HTTP enabled.

    # no mcp-proxy options
    npx mcp-proxy npx -y @anthropic/mcp-server-filesystem /path
    
    # with mcp-proxy options
    npx mcp-proxy --port 8080 --shell -- tsx server.js
  6. Configure CORS for the Proxy

    main

    The proxy enables CORS by default with a permissive configuration (Origin: *).

    Important for Browser Clients: Because Access-Control-Allow-Credentials is set to true, you cannot use a wildcard * for the origin in a browser. You must specify an explicit origin.

    Adding Custom Headers: If you use --apiKey, you must add X-API-Key to the allowed headers so browser preflights succeed. Use the --corsAddAllowedHeader flag to append to the defaults.

    Programmatic Configuration (SDK): If using the Node.js SDK, pass a CorsOptions object to startHTTPServer to override defaults like origin, methods, allowedHeaders, exposedHeaders, and credentials.

    # Adding a custom header via CLI
    npx mcp-proxy --port 8080 --apiKey secret \
      --corsAddAllowedHeader X-API-Key \
      -- npx -y @modelcontextprotocol/server-filesystem /srv
    import { startHTTPServer, CorsOptions } from "mcp-proxy";
    
    const cors: CorsOptions = {
      origin: ["https://app.example.com"],
      methods: ["GET", "POST", "OPTIONS"],
      allowedHeaders: "*", // Note: This replaces defaults; ensure Mcp-Method/Mcp-Name are included if needed
      exposedHeaders: ["Mcp-Session-Id"],
      credentials: true,
      maxAge: 86400,
    };
    
    await startHTTPServer({ cors, createServer, port: 3000 });
  7. Configure API Key Authentication

    main

    To protect your proxy, you can enable API key authentication. Clients must then provide the key in the X-API-Key header.

    CLI Usage: Pass the --apiKey flag or set the MCP_PROXY_API_KEY environment variable.

    Client Implementation: When using the StreamableHTTPClientTransport, include the key in the headers object.

    npx mcp-proxy --port 8080 --apiKey "your-secret-key" -- tsx server.js
    
    # or via environment
    export MCP_PROXY_API_KEY="your-secret-key"
    npx mcp-proxy --port 8080 -- tsx server.js
    const transport = new StreamableHTTPClientTransport(
      new URL("http://localhost:8080/mcp"),
      { headers: { "X-API-Key": "your-secret-key" } },
    );
  8. Expose a local server via Public Tunnel

    main

    Use the --tunnel flag to expose your local MCP proxy to the public internet. This is useful for testing webhooks or remote access. You can optionally request a specific subdomain using --tunnelSubdomain.

    # Basic tunnel
    npx mcp-proxy --port 8080 --tunnel -- tsx server.js
    
    # Request a specific subdomain
    npx mcp-proxy --port 8080 --tunnel --tunnelSubdomain myapp -- tsx server.js
  9. How upstream notifications and resource leases work

    main

    The mcp-proxy uses an UpstreamBridge to multiplex a single upstream MCP Client connection across multiple downstream connections.

    To prevent one downstream connection from accidentally canceling a resource subscription needed by another, the proxy uses an UpstreamLease model. Each downstream connection receives a lease when it subscribes to the bridge. This lease manages its own set of resource subscriptions. When a lease is released, it only removes the specific URIs it requested, provided no other active leases still require them (using internal reference counting).

    Key components:

    • UpstreamBridge: A shared object per upstream Client that manages the lifecycle of the connection and fans out notifications to all connected sinks.
    • UpstreamLease: A per-connection object used to manage resource subscriptions and determine if a specific URI update should be delivered to that connection.
    • UpstreamNotificationSink: A consumer interface that receives fanned-out notifications (like toolsListChanged or resourceUpdated).
    // Conceptual flow:
    const bridge = getUpstreamBridge({ client });
    const lease = bridge.subscribe(mySink);
    
    // Subscribe to a specific resource via the lease
    await lease.addResourceSubscription("mcp://resource/1");
    
    // When done, release the lease to clean up subscriptions
    lease.release();
  10. Run mcp-proxy to proxy an MCP stdio server over HTTP

    main

    The mcp-proxy CLI allows you to wrap a Model Context Protocol (MCP) server that communicates via stdio and expose it as an HTTP server (supporting SSE and/or streamable HTTP transports). This is useful for connecting web-based clients to local MCP servers.

    You can specify the command to run using two syntaxes:

    1. Using the -- separator: mcp-proxy [options] -- <command> [args...]
    2. Using positional arguments: mcp-proxy <command> [args...]
    # Using the -- separator (recommended for clarity)
    mcp-proxy --port 8080 -- node server.js --port 3000
    
    # Using positional arguments
    mcp-proxy node server.js
  11. Troubleshoot Python stdio servers

    main

    If a Python stdio server times out with the error Expected server to respond to ping, it is likely due to buffered stdout delaying JSON-RPC messages.

    To fix this, run Python in unbuffered mode using the -u flag or the PYTHONUNBUFFERED=1 environment variable.

    npx mcp-proxy -- python -u -m your_package.mcp_server
    # or:
    PYTHONUNBUFFERED=1 npx mcp-proxy -- python -m your_package.mcp_server