Engineering 1 Sep 2026 · 4 min read

MCP transports explained: stdio, Streamable HTTP, and the SSE it replaced

Three ways the same JSON-RPC travels, one of them deprecated, and a client that only speaks one of them. Which to build, which to install, and how to tell them apart from a config file.

MR mcprush team · The people who run the gateway

The protocol is the same in every case: a client sends initialize, then tools/list, then tools/call, each a JSON-RPC message with an id and an answer. The transport decides whether those messages travel down a pipe to a child process, or across HTTP to an address — and that decision, made once in a config file, is most of the difference between "runs on my laptop" and "runs for my team".

The same call, both shapes. The dashed path is stdio: no hop, no address, nowhere for a log or a ceiling to live.The same call, both shapes. The dashed path is stdio: no hop, no address, nowhere for a log or a ceiling to live.
The same call, both shapes. The dashed path is stdio: no hop, no address, nowhere for a log or a ceiling to live.

stdio: the client runs the server

The client starts your executable as a child process and writes JSON-RPC to its stdin, one message per line; the server answers on stdout the same way. Stderr is yours for logs. There is no network, no port, no authentication, and no way for anybody but that client to reach the server — which is both the whole appeal and the whole limitation.

  • Credentials live in the environment. The config entry carries an env block; the server reads the key from it. That is also where the key sits on disk, in every copy of that file.
  • One byte on stdout that is not protocol breaks the session. A print statement, a deprecation warning from a dependency, a progress bar — the symptom is always "server disconnected".
  • Nothing can be metered, logged or revoked by anybody but the person whose machine it is. A subscription to a stdio server is a promise with no admission point behind it.
  • It runs as you. Your files, your environment, your network. A local server with a file-system tool reaches everything you can reach.
{ "mcpServers": { "issues": { "command": "npx", "args": ["-y", "@you/issues-mcp"], "env": { "ISSUES_TOKEN": "…" } } } }
A stdio entry: a command, its arguments, and the environment the process starts with. If there is a "command" key, it is stdio.

Streamable HTTP: one endpoint, and a stream when it is worth one

Introduced in the 2025-03-26 revision, this is the transport for a server that lives at an address. The client POSTs every message to one URL. The server answers each POST either with a single JSON response or, when it has more to say than one answer — progress, log lines, a long-running call — with a stream of server-sent events on that same response. A separate GET on the same URL opens a standing stream for anything the server wants to send unprompted, and a server that has nothing to send answers that GET with 405.

Two headers carry the state. Mcp-Session-Id is issued by the server on initialize if it wants a session at all — a stateless server simply never issues one, and every POST stands alone. MCP-Protocol-Version, required on every HTTP request since the 2025-06-18 revision, says which revision the client is speaking, so a server can refuse one it does not.

POST /mcp HTTP/1.1 Authorization: Bearer <key> Content-Type: application/json Accept: application/json, text/event-stream MCP-Protocol-Version: 2025-06-18 {"jsonrpc":"2.0","id":1,"method":"tools/list"} HTTP/1.1 200 OK Content-Type: application/json {"jsonrpc":"2.0","id":1,"result":{"tools":[…]}}
One request, one answer. The Accept header says the client can take a stream instead, and the server picks.
  • Stateless is the default worth choosing. No session id, no server memory between calls, any replica can answer any POST. Sessions buy you a standing stream and cost you affinity.
  • Notifications get a 202 and no body. A server that answers notifications/initialized with a JSON body is a server that a strict client will complain about.
  • Resumability is optional and rarely used. A stream can carry event ids and a client can ask to resume from one; almost nobody does, and a server that skips it loses nothing today.

HTTP+SSE: the one being retired

The original remote transport, from the 2024-11-05 revision, used two endpoints: the client opened a long-lived GET to an SSE endpoint, the server told it which URL to POST messages to, and every answer came back on the standing stream. It works, plenty of servers still speak it, and it was deprecated the moment Streamable HTTP existed — because a design that needs a permanent open connection before it can answer a single request does not survive a load balancer, a proxy timeout or a serverless runtime.

The practical consequence is a compatibility matrix rather than a flag day. A client may still connect to an SSE server, or it may not: Copilot Studio, for one, stopped accepting SSE in 2025, and a server that speaks only SSE simply cannot be added there. If you are choosing, there is nothing to choose; if you are maintaining an SSE server, moving it is one endpoint and an afternoon.

The two topologies. Everything in the lower row has an address, and an address is the precondition for every capability in the middle box.The two topologies. Everything in the lower row has an address, and an address is the precondition for every capability in the middle box.
The two topologies. Everything in the lower row has an address, and an address is the precondition for every capability in the middle box.

Telling them apart from the config

You seeIt isWhat that means
"command" and "args"stdioThe client starts a process on this machine; the key is in env
"type": "http" or "url" ending in one pathStreamable HTTPOne endpoint; a bearer header carries the key
"type": "sse" or a URL ending in /sseHTTP+SSEDeprecated; may be refused by newer clients
A URL and no typeWhatever the client defaults toUsually Streamable HTTP now, SSE in older clients — check the client
The transport is the one field a config entry cannot lie about: the client will do exactly what it says, and the failure is immediate.

What the choice actually decides

Latency is the argument people have, and it is the least important one — a stdio server saves a hop and then waits on the same upstream API a remote one would. The real difference is where four things can live: the credential, the call log, the allowance and the release gate. A stdio server has nowhere to put any of them. A Streamable HTTP server behind a gateway has a natural home for all four, and that is the reason to build remote even when local would run.

  • Build stdio when the data is already on the user’s machine — a repository, a local database, a file tree — and there is nothing to meter.
  • Build Streamable HTTP when the server holds a credential of its own, when calls cost you money, or when the thing being sold is a subscription that has to stop when it is cancelled.
  • Build both when the server is small and the audience is mixed; the tool code is shared and the transport is the last line of the file.
  • Never build SSE. Not for a new server, not in 2026.

And when installing rather than building: a config entry with a command runs code on your machine as you, and one with a URL sends your calls to somebody else’s. Neither is wrong. Knowing which one you just pasted is the entire point of being able to read the entry.