Protocol revision 2026-07-28
MCP Transports: stdio and Streamable HTTP
The two transports MCP defines, how each frames messages, and what the 2026-07-28 revision removed from the HTTP one: sessions, the GET stream, and resumability.
MCP defines two transports. They carry the same JSON-RPC messages and differ in exactly one interesting way: what happens when the server needs to say something the client did not ask for.
stdio
The client launches the server as a child process and speaks newline-delimited JSON at its standard input. One message per line, no embedded newlines.
client --> server stdin {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{...}}\n
server --> client stdout {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}\n
server --> anywhere stderr free-form logging, which the client may ignoreThe rule that breaks the most hand-written servers is a MUST in the specification: a server must not write anything to stdout that is not a valid MCP message. stderr is explicitly free, and the client is told not to treat output there as an error. So logging goes to stderr, always, and a stray print statement in a tool is a protocol violation rather than a nuisance.
Shutdown is equally plain. The client closes the server's stdin; the server should exit on end-of-file. That is the only portable signal, which is why a server that ignores it ends up being killed.
Streamable HTTP
The server exposes a single endpoint that accepts POST. Every JSON-RPC message is its own request. The server answers either with one JSON object or with an SSE stream scoped to that request, which can carry progress notifications before the final result.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"get_weather",
"arguments":{"location":"Seattle, WA"},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28", ...}}}The headers are the notable part. Since 2026-07-28 the transport mirrors selected body fields into HTTP headers so a gateway can route and rate-limit without parsing JSON: Mcp-Method on every request, and Mcp-Name on calls that name a tool, resource or prompt. A server must reject the request if a header disagrees with the body, with error -32020. That check is not bureaucracy: without it a load balancer routing on the header and a server executing from the body can be made to disagree deliberately.
What the HTTP transport lost
Four mechanisms were removed in this revision, and every one of them appears in tutorials still online:
Sessions. The Mcp-Session-Id header is gone. Servers should ignore it and must not mint one.
The GET stream. Clients used to open a standalone SSE stream with GET to receive server-initiated messages. GET now answers 405.
Resumability. Last-Event-ID is ignored; streams do not resume.
Server-initiated requests. A server may no longer send its own JSON-RPC request down a stream. This is the one that changes how you write code, so it gets its own section.
Multi round-trip requests
When a server needs something from the client mid-call, sampling, a user confirmation, a missing argument, it used to send a request back up the open stream. That required something to be held open. Now it returns a result that says "I need input", and the client calls again with the answers attached.
client POST tools/call (id 1)
server InputRequiredResult { inputRequests: [ elicitation/create ] }
(the call is over; nothing is held open)
client gathers the answer from the user
client POST tools/call (id 2) original params + inputResponses
server final resultThe practical consequence: a tool that asks the user a question is not one long-lived call any more, it is two calls, and your server has to be able to answer the second one without remembering the first. That is statelessness arriving with a bill attached. Anything the server needs across the round trip has to be in the request or in your own storage, not in a connection.
Long-lived notifications
Change notifications, such as a tool list changing or a resource being updated, come from a subscriptions/listen request whose response is itself a stream that stays open. Request-scoped notifications like progress do not travel there; they flow on the response stream of the request they belong to.
Cancellation differs by transport, which is easy to get wrong. On Streamable HTTP, closing the response stream is the cancellation, and no message is sent. On stdio there is only one shared channel and nothing to close, so the client sends notifications/cancelled instead.
The deprecated one
HTTP+SSE, from the 2024-11-05 revision, has been deprecated since 2025-03-26 and is now on a formal phase-out. New implementations should not adopt it. It is worth recognising because a client built for backward compatibility still probes for it: POST first, and only if that fails in a way that is not a recognisable modern error, fall back to opening a GET stream and waiting for an endpoint event.