Agent Protocols Explained
← /learn · 04

Transports

stdio pipes against Streamable HTTP: bytes of framing and time per message, and an SSE stream that breaks mid-call, resumed in one revision and sent again in the next.

Loading the animation…

Concept

The messages of chapter 2 need a way to travel. The specification defines two.

stdio. The host starts the server as a child process and writes JSON-RPC messages to its standard input, one per line; the server answers on its standard output. "Messages are delimited by newlines, and MUST NOT contain embedded newlines", and the server may log to standard error (stdio, 2026-07-28, accessed 2026-10-08). The framing is one byte per message. Nothing crosses a network, so there is nothing to authorise: the server runs as the user.

Streamable HTTP. The server is a web service with one endpoint. Every client message is its own HTTP POST; the server answers a request with one JSON body, or with a Server-Sent Events (SSE) stream that carries notifications (such as progress) before the result (Streamable HTTP, accessed 2026-10-08). In 2025-11-25 the server could also send its own requests on that stream, initialize minted a session ID that every later request carried, and client notifications got 202 Accepted. In 2026-07-28 there are no sessions; instead each POST repeats the method and the tool, resource or prompt name in Mcp-Method and Mcp-Name headers, so a load balancer or gateway can route without reading the body, and the server must reject a request whose headers and body disagree.

Pick a session and switch transports. The JSON is the same; the framing is not. On stdio the short session costs 17 bytes of framing; on Streamable HTTP 2,478 bytes, 40% of everything sent, in 9 POSTs. The same session in 2026-07-28 sends fewer messages (no handshake, no ping), so its HTTP framing is 1,708 bytes, 25%. A session with progress turns its tool call's response into an SSE stream: one header, then one event: message per notification.

When the stream breaks

Loading the animation…

Concept

A long tool call reports progress over an SSE stream, and the connection drops after kk of its nn events. The two revisions disagree about what happens next.

2025-11-25: resume. Disconnection "SHOULD NOT be interpreted as the client cancelling its request", and the server "MAY make the stream resumable" by giving each SSE event an ID (transports, 2025-11-25, accessed 2026-10-08). The animation shows a server that does: it keeps working and keeps the events; the client reconnects with an HTTP GET carrying Last-Event-ID and the session ID, and the server replays what it missed. With n=5n = 5 and a break after event 2, the call finishes at 1.48 s and no work is repeated.

2026-07-28: send again. Resumption is gone: "A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID", and closing the stream is itself the cancellation signal (changelog, accessed 2026-10-08). The server stops; the client sends the call again and the work starts over. The same break now costs 2 repeated steps and the call finishes at 2.48 s. In exchange the server keeps no buffers and no sessions, so any instance can serve any request: the revision traded a rare slow path for simpler servers. For long-running work the same revision moved tasks into an official extension (io.modelcontextprotocol/tasks), which the client polls with tasks/get.

Maths

A message of bpb_p payload bytes with bfb_f bytes of framing, on a link of one-way latency ℓ\ell and rate RR (bits per second), arrives after t=ℓ+8(bp+bf)/Rt = \ell + 8(b_p + b_f)/R. On stdio bf=1b_f = 1 and ℓ\ell is tiny, so the cost is the server's start-up; on HTTP bfb_f is a request line and headers, or an SSE event prefix: 146 bytes per message on average in the short session, comparable to a small message itself, which is why the overhead share ϕ\phi is large for chatty sessions and small for big results.

For the broken stream, with nn steps of ss seconds of work, a break at tbreakt_{\text{break}} after kk events and a reconnect time trt_r: resuming finishes at about max⁡(ns,  tbreak+tr)\max(n s,\; t_{\text{break}} + t_r), because the server never stopped; re-sending finishes at about tbreak+tr+nst_{\text{break}} + t_r + n s and wastes W=ksW = k s of work. The difference grows with how late the break comes.

Code

The headers a client message gets on Streamable HTTP, by era, cut from the vendored engine:

if (era === "handshake") {
  if (version !== null) headers.push(["MCP-Protocol-Version", version]);
  if (session) headers.push(["Mcp-Session-Id", SESSION_ID]);
} else if (k === "request") {
  headers.push(["MCP-Protocol-Version", "2026-07-28"]);
  headers.push(["Mcp-Method", msg.method]);
  const nm = mcpName(msg);
  if (nm !== null) headers.push(["Mcp-Name", nm]);
}