Our own search data has been telling me to write this for months and I ignored it. Thirty-one different spellings of the same question, things like "stdio vs http mcp" and "mcp server stdio vs http", nearly three hundred impressions, several of them sitting on page one. Zero clicks, because we rank for it by accident out of other articles and have never had a page that actually answers it.
So here is the page. And writing it turned up something I did not expect: most of what you will find on this question, including the top results, describes a version of the protocol that has been replaced.
The Short Answer#
If a single client launches the server on the same machine and it touches local files, local databases or local tools, use stdio. If the server has to be reachable over a network, serve more than one client, or run somewhere you deploy rather than somewhere you sit, use Streamable HTTP.
If you are about to build on HTTP+SSE, the two-endpoint transport from the 2024-11-05 revision, do not. It has been deprecated since 2025-03-26, new implementations should not adopt it, and it is eligible for removal in a future revision.
That covers most decisions. The rest of this article is about the parts that bite after you have chosen.
stdio: One Pipe, and One Bug Everybody Writes#
The client launches your server as a subprocess and talks to it over standard streams. The server reads JSON-RPC from stdin and writes JSON-RPC to stdout, one message per line, newline-delimited, and messages must not contain embedded newlines.
Now the rule that breaks more stdio servers than anything else, and it is worth quoting because people skim past it: the server MUST NOT write anything to its stdout that is not a valid MCP message.
That means every print(), every console.log(), every stray debug line from a library you imported goes straight into the message channel and corrupts the protocol. The client sees malformed JSON where it expected a response. Symptoms range from a tool that never returns to a server that appears to connect and then dies on the first call.
The fix is in the same paragraph of the spec: stderr is yours. The server may write UTF-8 to stderr for any logging purpose, and the client may capture, forward, or ignore it. The spec is explicit that the client should not treat output on stderr as an error condition. So route your logging there and leave stdout alone.
Two more things worth knowing. Shutdown is initiated by the client closing your input stream, so a server should exit promptly when stdin reaches end of file. That is the primary graceful shutdown signal and, per the spec, the only portable one. And if your process dies unexpectedly, the client should restart it. Because the protocol is stateless, in-flight requests are simply lost, and the client can retry them against the fresh process. Note the wording: they are lost, not automatically replayed. Whether a retry happens is your caller's decision, not something the transport does for you.
Streamable HTTP: One Endpoint, One POST per Message#
The server exposes a single HTTP endpoint that accepts POST. Every JSON-RPC request or notification is its own POST. For a request, the server answers with either a single JSON object or an SSE stream scoped to that request, carrying progress notifications and then the final response. For a notification there is no JSON-RPC answer at all: an accepted notification gets HTTP 202 Accepted with no body.
Three requirements on the client side are easy to miss. The Accept header must list both application/json and text/event-stream, because the server chooses per request which one it sends and the client must handle both. The body must be a single JSON-RPC request or notification, never a response. And the request metadata headers, which I come to next, are mandatory.
For long-lived server-to-client notifications there is now a dedicated mechanism: you send a subscriptions/listen request, and its response stream stays open carrying only the notification types you opted into. Request-scoped notifications like progress do not travel on that stream; they flow on the response stream of the request they belong to.
What Changed in the 2026-07-28 Revision#
This is the part that makes most existing write-ups wrong, including a fair number of tutorials published this year.
Protocol-level sessions are gone. Earlier revisions let the server assign a session through an Mcp-Session-Id header, terminated with an HTTP DELETE. That mechanism is not part of the current revision. A server implementing only the new revision should ignore an incoming Mcp-Session-Id header entirely and must not mint or echo session ids.
The standalone GET stream is gone. Clients used to open a separate SSE stream with a GET request to receive server-initiated messages. Removed. A server that supports only this revision should answer GET or DELETE on the MCP endpoint with 405 Method Not Allowed; servers that still speak the older revisions alongside it obviously keep handling them.
Resumable streams are gone. Last-Event-ID is not supported. A server built only for this revision should ignore that header, along with any incoming Mcp-Session-Id.
Servers no longer send their own requests. When a server needs something from the client, sampling, elicitation, or roots, it used to send a JSON-RPC request down an SSE stream. Now it returns an InputRequiredResult and the client retries the original call with the answers attached. This is called Multi Round-Trip Requests, and it is a real change in control flow, not a rename.
And the initialize handshake itself is now the legacy path. Modern revisions carry the protocol version, client capabilities and client identity per request, in _meta.io.modelcontextprotocol/* fields, rather than establishing them once in a connection-scoped handshake.
If you have read any of this in a guide that still shows Mcp-Session-Id bookkeeping and a GET stream, that guide is describing the 2025-03-26 through 2025-11-25 shape. That shape is not wrong history, and plenty of deployed servers still speak it, but it is not what you should be building against now.
Three Headers That Are Now Mandatory#
Streamable HTTP mirrors selected body fields into HTTP headers so that load balancers, gateways and observability tooling can route and inspect requests without parsing the body.
Every POST to the MCP endpoint must carry MCP-Protocol-Version, for example MCP-Protocol-Version: 2026-07-28. Mcp-Method is required on all requests, and Mcp-Name on tools/call, resources/read and prompts/get, carrying the tool name, the resource URI or the prompt name respectively.
Here is the trap: the header value must match the corresponding value in the body. If it does not, the server must reject the request with 400 Bad Request and JSON-RPC error -32020, named HeaderMismatch. The reasoning is a genuine security concern rather than pedantry. If a load balancer routes on the header while the server executes on the body, and the two disagree, you have a request that goes to one tenant's infrastructure and runs another tenant's call.
Values that cannot be safely written as plain ASCII, anything non-ASCII, control characters, or leading and trailing whitespace, must be Base64-encoded with the sentinel format =?base64?VALUE?=. Servers decode that before comparing to the body.
The Security Rules for HTTP, Which Are Not Optional#
Three requirements, and the first one is the one people skip.
Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks, and must answer an invalid Origin with 403 Forbidden. Without it, a website your user visits in a browser can reach a local MCP server on their machine and drive it. That attack works precisely because the server is local and trusted.
When running locally, servers SHOULD bind only to 127.0.0.1 rather than 0.0.0.0. Binding to all interfaces on a laptop puts your tool server on every network that laptop joins, including hotel wifi.
And servers SHOULD implement proper authentication on all connections. The transport binding itself does not prescribe a mechanism, so which one you use is a decision you make deliberately rather than one the transport makes for you. In practice that means the authorization the MCP spec defines separately, or a bearer credential you control.
None of these apply to stdio, which is a large part of why stdio remains the right choice for local work. There is no port, no origin, and no listening network surface, so the process boundary is the transport's security boundary.
One thing stdio does not give you, and this is worth saying because it is widely assumed: it is not a guarantee that your data stays on the machine. The transport is local, the server's behaviour is not. A stdio server is an ordinary process that can open any outbound connection it likes, and plenty of them exist precisely to call a remote API on your behalf. stdio tells you how the client talks to the server. It tells you nothing about where the server sends things afterwards.
Choosing, in Table Form#
| stdio | Streamable HTTP | |
|---|---|---|
| Who starts the server | the client, as a subprocess | you, independently |
| Reachable by | that one client | anything that can reach the URL |
| Network surface | none | a port, with all that follows |
| Cancellation | notifications/cancelled | close the request's stream |
| Auth | process boundary | OAuth 2.1 or bearer, plus Origin checks |
| Scheduled and headless use | needs a process to launch it | natural fit |
| Typical case | local files, local database, IDE tooling | hosted API, team server, SaaS |
The decision is rarely close in practice. What is genuinely worth thinking about is the case where a vendor offers both, which is increasingly common: a local package you install and a hosted endpoint you point at. There the deciding question is not technical elegance but who holds the credential and what the server does with it. A local package keeps the credential on your machine, which is a real difference. It does not, as the previous section says, keep your data there. If that distinction matters for your use case, the answer is in the server's behaviour and its privacy terms, not in its transport.
Two Things I Would Check Before Building#
Check which revision your client actually speaks, not which one the docs describe. The spec carries an explicit version-negotiation and fallback procedure precisely because the field is split across revisions right now, and it recommends probing before the first real request even for clients that only support modern versions. The payoff is that a mismatch fails predictably instead of a legacy server quietly processing your call under different semantics.
And if you are writing a stdio server, put a lint rule or a test on it that fails when anything reaches stdout outside the message writer. It is the single cheapest guard against the one bug that costs everyone an afternoon, and it takes less time to add than the first debugging session it prevents.
The transports themselves are not complicated. What makes this topic confusing is that the ground moved recently, and most of the writing about it has not caught up. If you take one thing from this article, take the habit of checking the revision date on whatever you are reading.
