MCP Transports: stdio vs Streamable HTTP, and What Happened to SSE
The current MCP specification has two standard transports, stdio and Streamable HTTP. The old HTTP with SSE transport is deprecated, yet SSE still appears inside Streamable HTTP. What each one is, when to use which, what the requests look like, and how to move off the old one.
7 min read
MCP has two standard transports today. stdio is for a server running on the same machine as the client, which starts it as a child process and exchanges messages over its standard input and output. Streamable HTTP is for a server anyone can reach at a URL: every message is an HTTP POST to one endpoint, and each reply is either a single JSON object or a short Server-Sent Events (SSE) stream for that one request. “SSE” as a transport of its own means the older HTTP with SSE transport from the 2024-11-05 revision, which used two endpoints and one long-lived stream. It has been deprecated since 2025-03-26, it is eligible for removal, and nothing new should be built on it. So in Streamable HTTP vs SSE, the answer is Streamable HTTP, which still uses SSE as a response format when it needs to.
This page is about the current revision, 2026-07-28, and the practical choice. The history of every revision is in MCP specification changes; if MCP itself is new, start with what MCP is.
What a transport is, and is not
In the transports overview (opens in a new tab) a transport is a binding: it defines how messages are framed and delivered, how request metadata travels, and how cancellation works. It does not change what the messages mean. Both transports carry the same JSON-RPC messages, UTF-8 encoded, and in 2026-07-28 every request carries its own protocol version and client capabilities in _meta. Your tools do not know or care which transport called them. That is why most SDKs let you switch with one argument.
stdio
- The client launches the server as a subprocess. The server reads JSON-RPC messages from standard input and writes them to standard output, one per line, with no embedded newlines.
- Standard output is for protocol messages only. Logs go to standard error, which the client may capture, forward or ignore.
- To cancel a request, the client sends
notifications/cancelled. To shut down, it closes the server’s input and waits; a server should exit when its input closes. - If the process dies, the client should restart it. Because the protocol is now stateless, lost requests are simply retried against the new process.
Use stdio for tools that belong to one person on one machine: a server that reads local files, wraps a command-line tool, or talks to a database on your laptop. There is no port and no sign-in, because only the client that started the process can talk to it. The cost is distribution: every user installs the server and its runtime, and any credential it needs sits in that machine’s configuration.
Streamable HTTP
The Streamable HTTP binding (opens in a new tab) gives the server a single endpoint, such as https://example.com/mcp, that accepts POST. The rules that matter most:
- Every client message is its own POST, with an
Acceptheader listing bothapplication/jsonandtext/event-stream. - The server chooses per request: a single JSON object, or an SSE stream scoped to that request that carries progress or log notifications and ends with the response. Clients must handle both.
- Every request POST carries
MCP-Protocol-Version, plusMcp-Methodand, for tool calls, resource reads and prompt gets,Mcp-Name. They mirror the body so gateways can route without parsing it, and a mismatch is rejected with400. - Servers must validate the
Originheader and answer403to an invalid one, should bind to127.0.0.1when running locally, and should authenticate every connection. - Since 2026-07-28 there is no GET stream, no
Mcp-Session-Id, and no resuming a broken stream. Long-lived change notifications come on the response stream of asubscriptions/listenrequest instead.
Use Streamable HTTP for anything shared: a product’s own MCP server, a team server, anything a chat assistant in a browser has to reach. It is also the transport that carries MCP’s OAuth sign-in, so it is how a person grants an assistant scoped access without handing over a key. How MCP sign-in works covers that from the user’s side.
What the requests look like
These are real exchanges with a small Python server built on version 2.2.0 of the official SDK and started with uv run mcp run server.py --transport streamable-http. First the discovery request a modern client sends, then a tool call.
curl -si http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: add_task" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
"name":"add_task","arguments":{"title":"Tag the release"},
"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}'
HTTP/1.1 200 OK
content-type: application/json
{"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"Added T-2: Tag the release","type":"text"}],
"isError":false,"resultType":"complete","structuredContent":{"result":"Added T-2: Tag the release"}}}The same server answered server/discover with supportedVersions: ["2026-07-28"], its capabilities and its name, and it answered a GET to /mcp from a 2026-07-28 client with 405 Method Not Allowed, as the specification asks. The MCP Inspector connecting in its default legacy mode still worked, through initialize, because the SDK serves both eras from one endpoint. The MCP Inspector shows all of this in its Network tab if you would rather not use curl.
What the old SSE transport was
The 2024-11-05 transport (opens in a new tab) called HTTP with SSE needed two endpoints. The client opened a GET to the SSE endpoint and kept it open for the whole connection. The server’s first event, endpoint, told the client where to POST its messages, and every server message, including every response, came back as a message event on that one long-lived stream. Requests went one way and answers came back another, over a connection that had to stay up for as long as the client was connected.
Streamable HTTP replaced it in 2025-03-26. The 2026-07-28 revision formally classifies HTTP with SSE as deprecated, and its deprecated features list (opens in a new tab) sets the earliest removal at three months after the proposal that reclassified it reaches Final. New implementations should not adopt it and existing ones should migrate.
Which one to use
- A tool for yourself, on your machine: stdio.
- A server other people or other machines will use: Streamable HTTP, with authentication from the start.
- Replies that are quick and need no progress updates: let the server answer with plain JSON. It is simpler to proxy, log and cache.
- Long-running tools that report progress: an SSE response stream for that request. Behind nginx or a similar proxy, send
X-Accel-Buffering: noso events are not held back. - A remote server your stdio-only client needs: a small local bridge that speaks stdio to the client and HTTP to the server.
- The old SSE transport: only to keep existing clients working while you migrate.
Migrating a server off SSE
- Add one MCP endpoint, conventionally
/mcp, that accepts POST and answers each request with JSON or a request-scoped SSE stream. In most SDKs this is a change of transport name, not of tools: in the Python SDK,mcp.run(transport="streamable-http"), whose running guide (opens in a new tab) says plainly not to build anything new on SSE. - Keep the old SSE and message endpoints running beside it while clients move. The specification allows this, and notes that merging the old POST endpoint into the new one is possible but may add complexity you do not need.
- Decide your era. If you support only 2026-07-28, answer GET or DELETE on the endpoint with
405, ignore anyMcp-Session-Idheader and do not mint one, and ignoreLast-Event-ID. If you support both eras, legacy clients keep their sessions. - Move anything that relied on the server sending requests over the stream, such as elicitation or sampling, to the new pattern where a result asks the client for input and the client retries.
- Check
Originvalidation, localhost binding for local servers, and authentication, then change the URL in your documentation from…/sseto…/mcp.
Migrating a client
A client that must reach both kinds of server tries a POST first. If it gets 400, 404 or 405 without a recognised modern error in the body, it falls back to a GET and waits for an endpoint event, which means it has found an old SSE server. Claude Code already does this: according to its MCP documentation (opens in a new tab), claude mcp add --transport http tries HTTP first and switches to SSE when the server does not accept it, from v2.1.265, and --transport sse remains for connecting over SSE directly.
How fenbs does it
fenbs, a task board where AI assistants are members with roles, runs a Streamable HTTP server at https://fenbs.ai/api/mcp. Every POST gets a single JSON reply, because no fenbs tool needs to stream. A GET asking for an event stream gets 405, which tells the client to carry on without one, and a call with no credential gets 401 with a pointer to the sign-in metadata, so the client can open the browser. A client that cannot open a browser sends a token issued by hand under Settings, with a name, scopes and an optional expiry, as an Authorization: Bearer header instead. A client that speaks only stdio runs the fenbs-mcp bridge (npx -y fenbs-mcp), which relays stdin and stdout to this endpoint with that token in the FENBS_TOKEN environment variable. Setup for each assistant is in the MCP docs.
Related
Watching the traffic yourself: the MCP Inspector. A server in Python, on either transport: building an MCP server in Python. Serving a TypeScript server over HTTP: how to build an MCP server. Adding sign-in: how to add authentication to an MCP server.