MusePi

MCP Protocol and Transport Internals

English 中文

This document describes how coding-agent implements MCP JSON-RPC messaging and how protocol concerns are split from transport concerns.

Scope

Covers:

Does not cover extension authoring UX or command UI.

Implementation files

Layer boundaries

Protocol layer (JSON-RPC + MCP methods)

Transport layer (MCPTransport)

MCPTransport abstracts delivery and lifecycle:

Transport implementations own framing and I/O details:

Manager/client wiring

connectToServer() always installs an onRequest handler for standard server-to-client requests. MCPManager installs notification handlers, OAuth refresh hooks for HTTP-like OAuth servers, and onClose reconnect handling for managed connections.

Transport selection

client.ts:createTransport() chooses transport from config:

"sse" uses the legacy HTTP+SSE transport: it opens the configured URL with GET, reads the endpoint event’s plain-text URL/path, POSTs JSON-RPC requests to that endpoint, and receives JSON-RPC responses on the stream.

JSON-RPC message flow and correlation

Request IDs

Each transport generates per-request IDs with Snowflake.next(). IDs are transport-local correlation tokens.

Stdio correlation path

Unknown response IDs are ignored (no rejection, no error callback).

HTTP correlation path

If SSE stream ends before matching response, request fails with No response received for request ID .... After the matching response is captured, the transport drains remaining SSE messages in the background.

Notifications

Client emits JSON-RPC notifications via transport.notify(...).

Server-initiated notifications are surfaced through transport onNotification; MCPManager consumes known MCP list/update notifications and can forward all notifications through its own callback.

Stdio transport internals

Lifecycle and state transitions

If read loop exits unexpectedly, finally triggers #handleClose() which performs the same pending-request rejection and close callback.

Timeout and cancellation

Per request:

Cancellation is local only: transport does not send protocol-level cancellation notification to the server.

Malformed payload handling

In read loop:

If the underlying stream parser throws, onError is invoked (when still connected), then connection closes.

Disconnect/failure behavior

When process exits or stream closes:

Backpressure/streaming notes

Streamable HTTP transport internals

Lifecycle and connection semantics

HTTP transport has logical connection state, but request path is stateless per HTTP call:

So connected means “transport usable”, not “persistent stream established”.

Session header behavior

Timeout, cancellation, and auth refresh

For request():

For notify():

For HTTP-like OAuth configs managed by MCPManager, outbound requests and best-effort server-request responses retry once on HTTP 401/403 if token refresh returns replacement headers.

HTTP error propagation

On non-OK response:

On JSON-RPC error object:

Malformed JSON body (response.json() failure) propagates as parse exception.

SSE behavior and modes

Two SSE paths exist:

  1. Per-request SSE response (#parseSSEResponse)
    • used when POST response content type is text/event-stream
    • consumes stream until matching response id found
    • can process interleaved notifications during same stream
  2. Background SSE listener (startSSEListener())
    • optional GET listener for server-initiated notifications and server-to-client requests
    • connectToServer() starts it for Streamable HTTP transports after initialize and before notifications/initialized
    • listener startup waits up to one second, or less for very small request timeouts; timeout: 0 / OMP_MCP_TIMEOUT_MS=0 disables that startup deadline
    • if GET returns 405, another non-OK status, no body, or times out, listener silently disables itself

Malformed payload and disconnect handling

SSE JSON parsing errors bubble out of readSseJson and reject request/listener.

Legacy HTTP+SSE transport internals

LegacySseTransport implements MCP protocol revision 2024-11-05:

json-rpc.ts utility vs transport abstraction

src/mcp/json-rpc.ts provides callMCP() and parseSSE() helpers for direct HTTP MCP calls (used by Exa integration), not the MCPTransport abstraction used by MCPClient/MCPManager.

Notable differences from HttpTransport:

This path is lightweight but less robust than full transport implementation.

Retry/reconnect responsibilities

Transport-level

Current transport implementations do not:

They fail fast and propagate errors.

Manager/tool-bridge level

MCPManager wires transport.onClose for managed connections and runs reconnectServer(name) when a transport closes unexpectedly. Reconnect tears down the stale connection, re-resolves auth/config values, retries with backoff (500, 1000, 2000, 4000 ms), reloads tools, and preserves stale tools while reconnecting.

MCPTool and DeferredMCPTool also attempt one reconnect + retry for retriable connection errors during a tool call. This is tool availability recovery, not transport-level retry.

Failure scenarios summary

Practical boundary rule

If the concern is message shape, id correlation, or MCP method ordering, it belongs to protocol/client logic.

If the concern is framing (JSONL vs HTTP/SSE), stream parsing, fetch/spawn lifecycle, timeout clocks, or connection teardown, it belongs to transport implementation.