Skip to content

AI & Engineering / Field guide

Streamable HTTP vs SSE for MCP Servers: What Changed and What to Pick

By ·

stdio, SSE and Streamable HTTP look like three equal options for an MCP server. They are not: one is the old way, one is the new way for remote servers, and one is where most local servers should stay.

Search for MCP transports and you find three names side by side. Mixing them up is how people end up running an MCP server behind a load balancer on a transport the spec has already replaced. This guide separates them, shows what the exchange looks like on the wire, and ends with how Semitexa uses stdio for its MCP server and SSE for its browser transport, and why those two are different on purpose. If SSE itself is new to you, Server-Sent Events explained covers the protocol first.

The short version

  • stdio. The client starts your server as a subprocess and talks to it over standard input and output. The default for local tools.
  • Streamable HTTP. Your server is a separate process behind one HTTP endpoint. The client sends every message as a POST, and the server answers with plain JSON or opens an SSE stream. The transport for remote and shared servers.
  • HTTP+SSE. The transport from protocol version 2024-11-05, replaced by Streamable HTTP. The spec keeps a backwards compatibility section for it. A new server should not start here.
  • SSE itself did not go away. It became a tool inside Streamable HTTP, used when the server wants to stream more than one message.

What the old HTTP+SSE transport did

The old design used two doors. The client opened a GET request to an SSE endpoint, and the first event the server sent was an endpoint event telling the client where to POST its messages. From then on, server messages came down the stream and client messages went up through the separate POST endpoint.

That shape has a cost. The server holds one long-lived connection per client just to be reachable, even when it only ever answers short requests. Two endpoints have to agree on which session a message belongs to. And a dropped connection leaves the session in an unclear state.

What Streamable HTTP changed

Streamable HTTP keeps SSE but makes it optional, and puts everything behind a single MCP endpoint, for example https://example.com/mcp, which accepts both POST and GET.

POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: 1868a90c...

{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"ui_validate","arguments":{...}}}

The rules that matter, from the 2025-06-18 specification:

  1. Every client message is a new POST. The client must accept both application/json and text/event-stream in reply.
  2. The server chooses the reply. For a JSON-RPC request it answers with one JSON object, or with an SSE stream that ends with the response. A quick tool call needs no stream at all.
  3. Notifications and responses from the client get 202 Accepted and no body.
  4. A client may also GET the endpoint to open a stream the server can use for its own messages.
  5. Sessions are optional. If the server returns an Mcp-Session-Id header on the initialize response, the client sends it on every later request. A 404 for that id means the session is gone and the client must initialize again.
  6. The client sends MCP-Protocol-Version on every request after initialization.

Resumability is where SSE shows up in its classic form. A server may put an id on its events. If the connection breaks, the client reconnects with a GET and a Last-Event-ID header, and the server may replay what was missed on that same stream. The ids act as a cursor per stream, and a server must not replay messages that belong to a different stream. This is the same mechanism, with the same pitfalls, as in EventSource Reconnects: Last-Event-ID and the Events You Missed: it works only if the server numbered the events and kept them.

The security part people skip

The spec is explicit for Streamable HTTP. Servers must validate the Origin header to prevent DNS rebinding, should bind to localhost rather than all interfaces when they run locally, and should authenticate connections. A server on stdio has none of these concerns, because the MCP process has no network surface of its own. That is a real argument for stdio, not only a convenience.

What to pick

SituationPickWhy
The agent runs on the developer's machine and launches the toolstdioNo network port, no Origin checks, and the spec tells clients to support it whenever possible
One server, many clients, or a hosted serverStreamable HTTPOne endpoint, optional sessions, optional streaming, ordinary HTTP infrastructure
A tool call needs progress or several messagesStreamable HTTP with an SSE replyThe stream is per request and closes after the response
A browser page needs live server pushesPlain SSE plus a POST channelMCP transports are for MCP clients, and EventSource can only GET
An older client that only knows 2024-11-05Keep the old SSE and POST endpoints beside the new oneThe spec describes exactly this fallback

Inside Semitexa

Semitexa has both an MCP server and an SSE transport, and it deliberately does not use one for the other.

The MCP server: stdio only

bin/semitexa ui:mcp in semitexa/platform-ui is an MCP server on stdio. The command reads one JSON-RPC message per line from standard input and writes one per line to standard output, which is what the spec requires of the stdio transport:

while (($line = fgets(STDIN)) !== false) {
    $message = json_decode(trim($line), true);
    $response = $this->server->handle($message);
    if ($response !== null) {
        fwrite(STDOUT, json_encode($response, JSON_UNESCAPED_SLASHES) . "\n");
        fflush(STDOUT);
    }
}

This is the loop from the real command, shortened: the real one also skips empty lines and answers a line that is not JSON with a parse error. UiMcpServer::handle() answers initialize with protocol version 2025-06-18, lists four tools (ui_catalog, ui_schema, ui_validate and ui_render), and gives a notification no reply, as JSON-RPC says. A coding agent registers it like this:

{"mcpServers": {"semitexa-ui": {"command": "bin/semitexa", "args": ["ui:mcp", "--grant=catalog.read"]}}}

The --grant flag is the point. The agent launches the process, and the process acts for a visitor holding exactly those permissions, so the tools check a screen against the same catalog and permissions a person would get. The design is stated plainly in the code and the docs: there is no HTTP door, and in the browser a page reaches the composer through its own transport instead. A consequence worth noting is that there is no network endpoint to authenticate, no Origin to validate and no session id to guard.

What Semitexa does not have is an MCP server over Streamable HTTP. If you need a remote MCP server, this is not it. The tools and the permission model are documented in Screens an AI Composes.

The browser transport: one SSE stream and one POST channel

The browser is a different client with different rules, so it gets a different transport. A Semitexa page talks to the server through two doors, KISS and HUG:

DoorRouteDirectionCarries
KISSGET /__semitexa_kissserver to browserOne SSE stream per page: deferred slots, UI patches, component state, live feeds
HUGPOST /__semitexa_hugbrowser to serverEvery UI event and action, feed control, uploads

This is the same two-door shape the old MCP transport had: a GET that opens a stream and a separate POST for the other direction. For a browser it is the right shape, because EventSource can only GET and a page needs exactly one long-lived stream. For MCP over a network it was the part Streamable HTTP simplified. Same pattern, different client, different verdict.

Because that stream carries state a page needs, losing events matters. ReplayingSseTransport::resume() in semitexa/ssr reads the Last-Event-ID, asks a replay ring for the frames after it, and replays them; when the ring no longer holds that frame it sends a reset instead of pretending nothing was lost. That is the same resumability the MCP spec describes for Streamable HTTP, and the reconnect guide walks through it step by step.

A practical checklist

  • A local tool launched by the agent uses stdio. Log to stderr, and never print anything but protocol messages to stdout.
  • A remote or shared server uses Streamable HTTP on one endpoint. Validate Origin, authenticate, and bind narrowly.
  • Short calls return plain JSON. Open an SSE stream only when you really stream.
  • If you stream, number your events and keep them so Last-Event-ID can resume.
  • Do not start a new server on the 2024-11-05 HTTP+SSE transport. Keep it only if old clients need it.
  • Keep the browser transport and the agent transport separate. They have different clients and different permission questions.

Common questions

Is SSE deprecated in MCP?

The standalone HTTP+SSE transport from protocol version 2024-11-05 was replaced by Streamable HTTP. SSE itself is still part of Streamable HTTP, where a server may use it to stream several messages.

Should my MCP server use stdio or Streamable HTTP?

Use stdio when the client launches the server on the same machine. Use Streamable HTTP when the server is remote, shared between clients, or must be reachable over the network.

Can a browser talk to an MCP server directly?

The MCP transports are defined for MCP clients. A page that needs live updates should use its own SSE stream and a separate POST path, as the KISS and HUG doors do in Semitexa, and let the server talk MCP on its own side.

Does Semitexa support Streamable HTTP?

Not today. The MCP server shipped in semitexa/platform-ui runs on stdio. The browser transport uses SSE and POST and is not an MCP transport.

What does Last-Event-ID do in MCP?

When a stream breaks, the client reconnects with a GET and the last event id it received, and the server may replay missed messages on that stream. It only works if the server attached ids and kept the events.

← Back to the Semitexa Blog

New to the protocol underneath? Server-Sent Events Explained: How SSE Works and When to Use It

Choosing between SSE and WebSockets for a live screen? SSE vs WebSockets: How to Choose for Live Dashboards and Tables

Have a product idea?One free MVP every month