What is the difference between stdio and HTTP in MCP?
MCP defines two transports. Under stdio, the client launches the server as a subprocess and the two exchange newline-delimited JSON-RPC over stdin and stdout. Under Streamable HTTP, the server exposes a single endpoint and every message becomes a POST whose reply is either a JSON object or an SSE stream scoped to that request. Protocol semantics are the same on both1.
That last sentence governs everything that follows. The specification calls a transport a binding: it defines how a message is framed and delivered, how request metadata travels, and how cancellation and termination are signalled. It does not define what the messages mean1.
Transport is the bottom layer under the three roles described in MCP’s architecture. Choosing between the two is almost always the same decision as local versus remote server seen from another angle, and the operational consequences show up in MCP in production.
stdio, one line per message
The client launches the server as a subprocess. The server reads JSON-RPC from
stdin and writes JSON-RPC to stdout, one message per line, and messages must
not contain embedded newlines2.
Three rules on that page account for nearly every stdio server bug you will meet.
The server must not write anything to stdout that is not a valid MCP message.
One forgotten debug print() corrupts the framing and the client stops
understanding the channel. It is the number one cause of “my server won’t connect”,
and it shows up in no log at all, because logging is exactly what broke it.
The server may write UTF-8 strings to stderr for any logging purpose, and the
client should not assume that output on stderr indicates an error
condition2. That is where your print() belongs.
Every message shares a single channel: there are no per-request streams. That has
a consequence for cancellation. With no stream to close, the client cancels by
sending a notifications/cancelled notification carrying the request ID, and the
server must not send any further messages for that request afterwards2.
Shutdown has a defined sequence: the client closes the child’s input stream, waits
for the server to exit, and if it does not exit within a reasonable time, forcibly
terminates the process — on POSIX escalating from SIGTERM to SIGKILL, on
Windows through TerminateProcess or Job Objects2. The server side has a
matching obligation: exit promptly when stdin closes or reads return
end-of-file. That is the portable graceful-shutdown signal, and a server that
ignores it is a server that only ever dies on SIGKILL.
If the process exits on its own, the client should restart it. Here a direct effect
of the protocol going stateless shows up: with no session, in-flight requests are
simply lost and the client can retry them against the fresh process. What has to be
re-established are any active subscriptions/listen streams2.
One point that usually goes unnoticed: stdio’s framing does not depend on the standard streams. One JSON-RPC message per line over a reliable bidirectional byte stream works the same over a Unix socket or TCP, and the specification recommends that custom transports on such channels reuse that framing rather than invent another1.
Streamable HTTP, one POST per message
The server must expose a single HTTP path, the MCP endpoint, that accepts POST.
Every JSON-RPC message the client sends must be a new POST to that endpoint,
and the client must include an Accept header listing both application/json
and text/event-stream3.
The body is a request or a notification, never a response. If it is a notification
and the server accepts it, the reply is 202 Accepted with no body. If it is a
request, the server returns either Content-Type: application/json with a single
object or Content-Type: text/event-stream with a stream, and the client must
support both3.
The SSE stream is what most confuses anyone arriving from the older revision. It is
scoped to the request: it carries notifications relating to that call, such as
notifications/progress, and ends with the final response. The server must not
send independent JSON-RPC requests on it. Long-lived streams do exist, but through
an explicit path: the client sends subscriptions/listen, and the response to that
request is the stream that stays open delivering the notifications it opted
into3.
Two operational recommendations save real pain: include X-Accel-Buffering: no on
SSE responses so reverse proxies stop buffering events, and periodically emit an
SSE comment line as a keep-alive on long-lived streams so intermediaries do not
drop the connection during quiet periods3.
Cancellation here is the mirror of stdio: closing the response stream must be treated by the server as cancellation of that request. Because each request has its own stream, the disconnect is unambiguous3.
The headers the server enforces
Revision 2026-07-28 mirrors body fields into HTTP headers so load balancers and
gateways can route without opening the JSON. This is required, not optional.
Every POST must carry MCP-Protocol-Version, and its value must match
io.modelcontextprotocol/protocolVersion in the body’s _meta. On a mismatch the
server must reject the request with 400 Bad Request and a HeaderMismatch
error, code -32020. Also required are Mcp-Method, carrying the method name, and
Mcp-Name, carrying params.name or params.uri, on tools/call,
resources/read and prompts/get3.
There is also an extension that mirrors tool parameters into headers: an
x-mcp-header property in an argument’s schema makes the client send
Mcp-Param-{Name} with that value. It exists for routing by region or tenant
without body inspection. The spec ships the obvious warning alongside it: do not
mark sensitive parameters this way, because header values are visible to every
intermediary on the network3.
On security, three requirements. The server must validate the Origin header
on all incoming connections and answer 403 Forbidden when it is present and
invalid; it should bind only to 127.0.0.1 when running locally rather than
0.0.0.0; and it should implement proper authentication on all connections.
Without these, an attacker can use DNS rebinding to interact with your local MCP
server from a remote website3.
What changed on 28 July 2026
Revision 2026-07-28 reshaped Streamable HTTP and breaks older clients. The three
removals that matter3:
- The GET stream endpoint is gone. There is no standalone stream opened by GET to receive server-initiated messages.
- Protocol sessions are gone. The
Mcp-Session-Idheader, and the DELETE that terminated a session, are not part of this revision. Last-Event-IDresumability is unsupported. If the connection drops mid-call, the request is lost.
A server implementing only this revision that receives older traffic should answer
405 Method Not Allowed to GET and DELETE on the endpoint, ignore Mcp-Session-Id
without minting or echoing an identifier, and ignore Last-Event-ID3.
SSE did not die, the transport with that name is on its way out
This is where almost every write-up about MCP gets it wrong, and the claim needs a date attached.
As of 30 July 2026, the deprecated features registry lists the HTTP+SSE transport
from revision 2024-11-05 — the two-endpoint one, with a GET that opened the
stream and a separate POST for messages — as deprecated since revision
2025-03-26, reclassified under the formal feature lifecycle policy by SEP-2596,
with earliest removal three months after that policy reaches Final4. New
implementations should not adopt it, and existing ones should migrate.
SSE as a technology remains at the centre of the current transport: whenever a
request needs to emit progress before its result, or when the client opens a
subscriptions/listen, the response is an SSE stream3. Saying “MCP dropped SSE”
is wrong. Saying “the two-endpoint HTTP+SSE transport is deprecated” is right.
The same registry lists the rest of what is on its way out as of 30 July 2026:
roots, sampling, logging and dynamic client registration, all deprecated in
2026-07-28, eligible for removal in the first revision released on or after 28
July 20274.
Which one to pick
Server on the user’s machine, serving that one person: stdio. No network, no authentication, no open port, and the client owns the process life cycle. That is what happens when you add a server to your editor’s configuration file.
Server running locally but reached over HTTP, which is a legitimate setup in some
shops: Streamable HTTP, bound to 127.0.0.1 and validating Origin. Without
those two, any page open in the user’s browser can reach the server.
Server on another machine, with one user or many: Streamable HTTP with authentication. Because there is no protocol session, every request carries its own credential and any replica can answer any request. That is what makes horizontal scaling trivial, and what forces the server to keep no state on the instance.
Where each one breaks
stdio has no isolation. The server runs with the user’s permissions, on the user’s machine, with access to the same filesystem and the same environment variables. There is no sandbox in the protocol. Installing a third-party stdio server is running third-party code.
stdio does not scale past one person. One process per client, on each person’s machine. Ten people using the same server means ten installations to update.
HTTP turned a dropped connection into lost work. With no Last-Event-ID, a tool
POST that has been running for forty seconds and loses its connection loses
everything. The client has to reissue with a fresh id, and the server needs the
operation to be idempotent or reversible — which most servers are not.
Local HTTP without Origin is an open door. It is the one failure mode on this
list the specification names outright, with the vector spelled out: DNS rebinding
from a remote website.
Cancellation has two semantics and code usually has one. On stdio the client sends a notification; on HTTP it closes the stream. A server written assuming one of the two keeps working after a cancellation on the other. A 2025 measurement study of MCP-enabled agents went as far as recommending task abort mechanisms among its most relevant optimizations5.
What it costs
The transport itself costs little next to context, and the proportion is worth keeping in mind. That same 2025 study measured MCP-enabled interactions and showed that token inflation comes from the combination of system prompt, tool definitions and history, not from the channel the messages travel through5.
What the transport charges you is operations. On stdio, one process per server per user, with memory and a life cycle to manage, and updates that depend on each person’s machine. On HTTP, a service to host, authenticate, monitor and rate limit, plus the fixed cost of TLS and one network round trip per message instead of a write to a pipe.
The deciding sum is rarely about latency. It is about who administers the process.
Where to start
- Start with stdio. With no network and no authentication, the development loop is shorter and errors surface in the right place.
- Route all logging to
stderrbefore the first line of logic. In Python,print()withoutfile=sys.stderris the classic bug. - Implement shutdown on closed
stdin. A server that only dies onSIGKILLleaves orphaned processes on the user’s machine. - When you move to HTTP, implement all three required headers at once.
MCP-Protocol-Version,Mcp-MethodandMcp-Name, validated against the body. - Validate
Originand bind to127.0.0.1even in development. The habit is what stops you shipping0.0.0.0to production. - Assume any request may be reissued. With no stream resumption and no session, idempotency stopped being a refinement and became a requirement.
Step 6 is the biggest departure from what tutorials written before July 2026 teach.
Footnotes
-
The transports overview page of revision
2026-07-28is the source for the definition of a transport as a binding, for the list of the two standard transports, and for the recommendation that custom transports reuse stdio’s framing. Verified on 30 July 2026. ↩ ↩2 ↩3 -
The stdio transport page of the same revision is the source for line framing, the
stdoutandstderrrules, cancellation vianotifications/cancelled, the shutdown sequence and the behaviour on unexpected termination. ↩ ↩2 ↩3 ↩4 ↩5 -
The Streamable HTTP page of the same revision is the source for the single endpoint, the
Acceptand response rules, the per-request stream model, the required headers and theHeaderMismatcherror, theOriginand local-bind requirements, and the three removals in this revision. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
The deprecated features registry of revision
2026-07-28is the source for the status of the 2024-11-05 HTTP+SSE transport and for the earliest removal dates of roots, sampling, logging and dynamic client registration. ↩ ↩2 -
Ding et al. (2025) measured MCP-enabled agent interactions and attributed token inflation to the system prompt, tool definitions and history, recommending parallel tool calls and task abort mechanisms. ↩ ↩2
Frequently asked questions
- Which transports does MCP define?
- Two standard ones as of revision 2026-07-28: stdio, where the client launches the server as a subprocess and they exchange newline-delimited messages over the standard streams, and Streamable HTTP, where each message is a POST to a single endpoint. Protocol semantics are identical; only the framing differs.
- What is Streamable HTTP in MCP?
- It is MCP's network transport. The server exposes a single endpoint accepting POST, every JSON-RPC request becomes its own POST, and the reply arrives as a JSON object or as an SSE stream scoped to that request. It replaced the two-endpoint HTTP+SSE transport in revision 2025-03-26.
- Is SSE deprecated in MCP?
- The two-endpoint HTTP+SSE transport from revision 2024-11-05 has been deprecated since 2025-03-26 and appears in the deprecated features registry as of 30 July 2026. SSE itself is still in use: Streamable HTTP answers with an SSE stream whenever a request needs events before its result.
- Which MCP transport should I use?
- If the server runs on the user's machine and serves that one person, stdio. In every other case, Streamable HTTP with authentication. Running HTTP on localhost is legitimate too, but it requires validating the Origin header and binding to 127.0.0.1, or any open browser tab can reach the server.
- Does the Mcp-Session-Id header still exist?
- Not in revision 2026-07-28, which removed protocol sessions along with the GET stream endpoint and Last-Event-ID resumability. A server implementing only that revision should answer 405 to GET and DELETE on the endpoint, ignore Mcp-Session-Id without echoing anything, and ignore Last-Event-ID.
- Why does my stdio server break when I print something?
- Because stdout is the protocol channel. The specification forbids a server from writing anything to stdout that is not a valid MCP message, and one stray print line corrupts the framing. Logging from a stdio server goes to stderr, which the client may capture, forward or ignore.
References
- Agentic AI Foundation. Model Context Protocol specification, revision 2026-07-28 — Transports (2026)
- Agentic AI Foundation. Model Context Protocol specification, revision 2026-07-28 — stdio transport (2026)
- Agentic AI Foundation. Model Context Protocol specification, revision 2026-07-28 — Streamable HTTP transport (2026)
- Agentic AI Foundation. Model Context Protocol — Deprecated features registry, revision 2026-07-28 (2026)
- Ding, Z., Zhu, M., Liu, Y.. Network and Systems Performance Characterization of MCP-Enabled LLM Agents (2025)arXiv:2511.07426