Skip to content

Bridge Internals

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

Bridge Internals

bridge.ts is the heart of the proxy: it forwards each JSON-RPC message from the client to the upstream MCP server over HTTP, parses the response (JSON or SSE), and manages a long-lived server-notification stream. This page documents its behavior in detail. For the surrounding flow, see Architecture.

The message loop

Messages from StdioCodec.messages() are processed through a serialized promise chain:

pending = pending.then(() => handle(message))

This guarantees in-order, one-at-a-time forwarding even though each handle call is async.

Per-message POST

For each message the bridge issues a POST to upstream.url with:

  • Authorization: Bearer <token> (from the TokenManager).
  • Accept: application/json, text/event-stream — the upstream may answer with either.
  • Content-Type: application/json.
  • MCP-Protocol-Version (if protocolVersion is configured).
  • Mcp-Session-Id once captured (see below).

The request is bounded by an AbortController plus undici body/headers timeouts derived from upstream.timeoutMs.

Status-code handling

Status Behavior
200 (JSON) Parse the body as a JSON-RPC response and write it to stdout.
200 (SSE) Parse text/event-stream frames and emit each data: payload to stdout.
202 Accepted Acknowledge; the body is drained. For requests that imply a session, may open the server stream.
401 TokenManager.invalidate(), fetch a fresh token, and retry the request once.
other non-2xx Emit a JSON-RPC error (-32001) carrying the status.

A second consecutive 401 is surfaced to the client rather than retried again.

Session id capture

The first response carrying an Mcp-Session-Id header has that value captured and replayed as a request header on subsequent calls, so the upstream can correlate the session.

Content-type dispatch

The response Content-Type decides parsing:

  • application/json → single JSON-RPC object.
  • text/event-stream → SSE framing (below).

SSE parsing

The proxy parses Server-Sent Events incrementally:

  • Bytes are accumulated into a buffer and split on event boundaries (\n\n or \r\n\r\n).
  • Within an event, only data: lines are collected; the joined data is the JSON-RPC payload emitted to stdout.
  • A buffer guard caps the in-progress buffer at 8 MB (MAX_BUF); this prevents a malicious or broken stream from growing memory unbounded.

Server-notification stream

When upstream.openServerStream is enabled, the bridge maintains a separate, long-lived GET request with Accept: text/event-stream to receive server-initiated notifications:

  • Backoff on reconnect: exponential, base 500 ms, capped at 30 s, with jitter; up to 10 retries (maxRetries).
  • Auto-disable: if the upstream responds 404 or 405 to the stream request, the proxy concludes the upstream doesn't support it and stops trying.
  • The stream reuses the captured session id and Bearer token, and emits notifications to stdout as they arrive.

Timeouts and error mapping

  • Requests use AbortController together with undici bodyTimeout / headersTimeout.
  • Transport errors become JSON-RPC error -32000. Timeouts are detected from undici error codes UND_ERR_BODY_TIMEOUT and UND_ERR_HEADERS_TIMEOUT and reported as timeouts.
  • When stdout is closed (client gone), writes are swallowed so shutdown stays clean.

See OAuth2 Internals for how the Bearer token is produced and refreshed.

Clone this wiki locally