Skip to content

Architecture

Chenglei Yuan edited this page Jun 4, 2026 · 1 revision

Architecture

A source-grounded tour of how the proxy is put together. For deep dives see Bridge Internals and OAuth2 Internals.

Component map

            ┌────────────────────────── mcp-oauth2-proxy ──────────────────────────┐
            │                                                                        │
 MCP client │   stdio.ts            bridge.ts                  oauth2/               │   upstream
   (parent) │  ┌─────────┐        ┌───────────┐   getToken()  ┌────────────┐        │   MCP server
  ──stdin──▶│  │StdioCodec│──msg──▶│   Bridge  │──────────────▶│TokenManager│        │
  ◀─stdout──│  │ (JSON-RPC)│◀─resp─│ POST/SSE  │◀──Bearer──────│ + Grant    │──token─┼──▶ IdP token
            │  └─────────┘        └─────┬─────┘                └────────────┘        │     endpoint
            │       ▲                   │ HTTP + SSE (undici)                        │
            │       │                   └────────────────────────────────────────────┼──▶ upstream /mcp
            │   log.ts (pino → stderr)                                                │
            └────────────────────────────────────────────────────────────────────────┘

Module responsibilities

Module Responsibility
index.ts Process entry point and startup orchestration.
config.ts zod schema, env-var overrides, load-time security validation.
security.ts isLoopbackHost, assertSecureUrl (cleartext-HTTP policy).
log.ts pino logger to stderr with secret redaction.
stdio.ts Newline-delimited JSON-RPC codec; request/notification/response helpers.
bridge.ts The message loop: forward each JSON-RPC message to the upstream over HTTP/SSE.
oauth2/tokenManager.ts Access-token cache, proactive refresh, in-flight dedup, invalidate.
oauth2/factory.ts Build the right Grant; wire the refresh-token cache for authorization_code.
oauth2/grants/* Grant interface + client_credentials / authorization_code / refresh_token.
oauth2/discovery.ts RFC 9728 → RFC 8414 / OIDC endpoint discovery.
oauth2/interactive.ts PKCE, browser open, local callback server, DNS-rebinding/CSRF defense.
oauth2/tokenCache.ts AES-256-GCM refresh-token cache on disk.
oauth2/http.ts undici-backed token-endpoint client; client-auth + token-response parsing.

Startup sequence (index.ts)

  1. loadConfig() — read MCP_PROXY_CONFIG (if set), apply env overrides, validate with zod, run assertSecureUrl on upstream/token/authorization URLs.
  2. createLogger() — pino to stderr at the configured level.
  3. If discovery.enabled, discoverFromUpstream() fills in missing endpoints.
  4. buildGrant() — construct the configured grant (requires a token URL).
  5. new TokenManager(grant, …) and prefetch a token via getToken(). On failure the process exits with code 2 (fail fast, before touching stdio).
  6. new StdioCodec(process.stdin, process.stdout) and new Bridge(...).
  7. Install SIGINT / SIGTERM handlers for graceful shutdown.
  8. bridge.run() — enter the message loop. A warning is logged if stdin is a TTY (the proxy expects to be driven by an MCP client, not a human).

Request lifecycle

client → stdin → StdioCodec.messages() → Bridge
   Bridge: token = TokenManager.getToken()
           POST upstream  (Authorization: Bearer, Accept: json + event-stream)
             ├─ 200 application/json   → write JSON-RPC response to stdout
             ├─ 200 text/event-stream  → parse SSE frames, emit data: payloads
             ├─ 202 Accepted           → ack; maybe open server stream
             ├─ 401                     → invalidate token, refresh, retry once
             └─ other                   → JSON-RPC error to stdout

Concurrency model

  • Serialized message loop. The bridge chains processing with pending = pending.then(...), so messages from the client are forwarded in order, one at a time.
  • Async server stream. Independently, a long-lived GET text/event-stream channel (when openServerStream is on) delivers server-initiated notifications, with bounded exponential backoff on reconnect. See Bridge Internals.
  • Single token authority. All token access funnels through one TokenManager, which de-duplicates concurrent refreshes.

Error and shutdown handling

  • Upstream transport errors become JSON-RPC errors (-32000), with timeouts detected from undici's UND_ERR_BODY_TIMEOUT / UND_ERR_HEADERS_TIMEOUT.
  • Non-2xx upstream responses become JSON-RPC error -32001.
  • SIGINT/SIGTERM stop the loop and let in-flight work unwind.

Clone this wiki locally