Streamable HTTP vs SSE: The Two Remote MCP Transports

Every remote MCP server you will encounter speaks one of two HTTP transports: the legacy HTTP+SSE transport from the original 2024-11-05 spec revision, or Streamable HTTP, which replaced it in the 2025-03-26 revision and has been the current transport ever since. They carry the same JSON-RPC 2.0 messages, but they look completely different on the wire — different methods, different connection lifecycles, in the legacy case even different URLs for different directions of traffic. If you build clients, operate servers, or monitor either, you need to know both, because the legacy transport is deprecated but far from gone.


The Legacy Transport: HTTP+SSE

The original remote transport splits the conversation across two channels:

  1. Server → client: the client GETs an SSE endpoint (conventionally something like /sse) with Accept: text/event-stream. The server holds this connection open and pushes JSON-RPC messages down it as SSE data: events — including all responses to the client's requests.
  2. Client → server: the first thing the server sends on the stream is an endpoint event containing a URL. The client POSTs its JSON-RPC requests to that URL, and the responses come back asynchronously on the long-lived stream — not in the POST response.

Two consequences follow. First, the transport is inherently stateful: the server must hold a persistent connection per client and correlate every POSTed request with the stream it belongs to. Second, the URL you were handed is only half the picture — you cannot send a request until you have opened the stream and learned the message endpoint.

On the wire, the tell is unmistakable: a bare POST of a JSON-RPC message to the advertised URL gets a 405 (that endpoint only accepts GET), while a GET opens an event stream that begins with an endpoint event.

The Current Transport: Streamable HTTP

Streamable HTTP collapses everything onto a single endpoint:

  • The client POSTs a JSON-RPC message to the endpoint and gets its answer in the POST response itself. The response body is either application/json (the message directly) or text/event-stream (one or more SSE frames carrying the message) — which is why every request should send Accept: application/json, text/event-stream.
  • A GET to the same endpoint may open a stream for unsolicited server-to-client messages, but a server that only does request/response is explicitly permitted to answer GET with 405. Merlonix's own MCP server takes exactly this shape: JSON-RPC over POST to /mcp, one JSON response per request, 405 on GET.
  • Session state, where a server wants it, rides in the Mcp-Session-Id header: issued on the initialize response, replayed by the client on subsequent requests.

Note the naming trap: Streamable HTTP also uses SSE framing — as an optional response encoding within a single request/response cycle. "The server answered my POST with text/event-stream" does not mean you are on the legacy transport. The distinguishing feature of legacy HTTP+SSE is the split-channel design: responses arriving on a separate long-lived stream rather than in the POST response.

Why the Ecosystem Moved

The migration was driven by operational reality more than protocol aesthetics:

  • Serverless and edge compatibility. A transport that requires one persistent connection per client for the lifetime of a session is a poor fit for platforms that bill per-request and cap execution time. Plain request/response POSTs run anywhere.
  • Infrastructure friction. Long-lived SSE connections are the classic victim of proxy idle timeouts, load-balancer connection limits, and corporate middleboxes. Every hop between client and server is a place the stream can silently die.
  • Horizontal scaling. With legacy SSE, the instance that holds your stream must be the instance that sees your POSTs — sticky routing or shared state, pick your pain. A single stateless endpoint scales out trivially.
  • Simpler clients. One URL, one request, one response. No stream to open first, no endpoint event to parse, no reconnect-and-resubscribe logic just to make a single tool call.

The direction of travel has continued past the transport itself: the 2026-07-28 spec revision makes the protocol stateless too, removing the initialize/initialized handshake and the Mcp-Session-Id header entirely. Client info and protocol version move into per-request _meta, and Mcp-Method/Mcp-Name routing headers let gateways route without parsing bodies. Streamable HTTP was the transport-level step in the same journey.

Telling Them Apart From the Outside

You can classify a server's transport with two requests and no documentation:

  1. POST a JSON-RPC initialize to the URL (with the dual Accept header). If you get a JSON-RPC message back in the response body — whether as plain JSON or inside an SSE frame — it speaks Streamable HTTP.
  2. If the POST gets a 405, the server is steering you to the legacy transport. Confirm with a GET sending Accept: text/event-stream: an event stream that opens with an endpoint event is legacy HTTP+SSE.
  3. If the POST is rejected in a JSON-RPC way rather than a transport way — reachable, not auth-gated, but the handshake itself is refused — try tools/list directly before concluding anything: a 2026-07-28 stateless server has no handshake to succeed. The full failure-mode decision tree lives in why MCP initialize fails.

This is precisely the sequence Merlonix's MCP health checker runs: it reports the transport it detected (streamable_http vs sse) alongside the handshake result, the spec generation, and the tool inventory, so you get the classification and the health verdict from one probe.

Migration Notes for Server Authors

If you still operate a legacy HTTP+SSE server, the migration is mostly subtraction:

  • Serve one endpoint. Handle POST for all JSON-RPC traffic and answer each request in its own response. If you have no server-initiated messages to push, return 405 on GET — that is spec-compliant, not a shortcut.
  • Honor the Accept header contract. Expect clients to advertise both application/json and text/event-stream; answer with whichever you produce. If you never stream, plain JSON is fine.
  • Decide your state story. Either issue an Mcp-Session-Id on initialize and validate it on subsequent requests, or hold no per-client state at all. If you are writing new code in 2026, look hard at the stateless 2026-07-28 revision before building session plumbing you will remove later.
  • Keep the old endpoint up during the cutover if you have existing clients, but publish the new one — including in your /.well-known/mcp.json server card, which is how clients and monitors discover the endpoint without guessing. If you are generating a server from an existing REST API rather than migrating one, the API-to-MCP converter emits current-spec servers from the start, and the MCP directory is worth a look at how live servers present themselves.
  • Echo the negotiated protocolVersion in your initialize result, and support more than one dated revision if you can — version negotiation failures are one of the most common self-inflicted handshake bugs.

What Monitoring Must Handle Differently

A monitor that assumes one transport lies about the other:

  • Legacy SSE servers: a POST-based probe reads 405 and, naively, "down." The probe has to recognize 405-on-POST as a transport signal, not an outage — and genuinely checking a legacy server means holding a stream open, which needs bounded timeouts so a stalled stream fails cleanly instead of hanging the check.
  • Streamable HTTP servers: the probe must parse both response encodings (direct JSON and SSE-framed JSON, including multi-line data: events and batches), replay Mcp-Session-Id on follow-ups, and treat 405-on-GET as healthy.
  • Both: a server changing transport or spec generation between checks is itself an alert-worthy event — clients pinned to the old behavior can break the moment the server migrates, while every HTTP-level signal stays green. That only falls out of continuous checks that record what they saw last time; the full alerting picture is in how to health-check a remote MCP server.

The transport is the part of MCP most people never think about until it breaks. Knowing which one you are speaking — and probing accordingly — turns a class of confusing failures into five-minute diagnoses.