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 of its 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 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 payload bytes with bytes of framing, on a link of one-way latency and rate (bits per second), arrives after . On stdio and is tiny, so the cost is the server's start-up; on HTTP 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 is large for chatty sessions and small for big results.
For the broken stream, with steps of seconds of work, a break at after events and a reconnect time : resuming finishes at about , because the server never stopped; re-sending finishes at about and wastes 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]);
}