If you search for MCP transports today you find three names that look like three options: stdio, SSE and Streamable HTTP. They are not three equal options. One of them is the old way, one is the new way for remote servers, and one is the best choice most local servers never leave. Mixing them up is how people end up running an MCP server behind a load balancer with a transport that was replaced.
This article separates them, shows what the message flow 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.
The short version
- stdio: the client starts your server as a subprocess and talks to it over standard input and output. 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 may answer with plain JSON or open an SSE stream. This is 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. New servers 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. 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 SSE stream and client messages went up through the separate POST endpoint.
That shape has a cost. The server must hold 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 means the session is 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:
- Every client message is a new POST. The client must accept both
application/jsonandtext/event-streamin reply. - For a JSON-RPC request, the server chooses: answer with one JSON object, or answer with an SSE stream that ends with the response. A quick tool call needs no stream at all.
- Notifications and responses from the client get
202 Acceptedand no body. - A client may also GET the same endpoint to open a stream the server can use for its own messages.
- Sessions are optional. If the server returns an
Mcp-Session-Idheader on the initialize response, the client sends it on every later request. A404for that id means the session is gone and the client must initialize again. - After the first exchange the client sends
MCP-Protocol-Versionon each request.
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.
If you have read about EventSource reconnects, this is the same mechanism with the same pitfalls: 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 running locally, and should authenticate connections. A server on stdio has none of these concerns, because it has no network surface of its own. That is a real argument for stdio, not just a convenience.
What to pick
| Situation | Pick | Why |
|---|---|---|
| The agent runs on the developer's machine and launches the tool | stdio | No network port, no Origin checks, the spec tells clients to support it whenever possible |
| One server, many clients, or a hosted server | Streamable HTTP | One endpoint, sessions, optional streaming, plain HTTP infrastructure |
| A tool call needs progress or several messages | Streamable HTTP with an SSE reply | The stream is per request, closed after the response |
| A browser page needs live server pushes | Plain SSE plus a POST channel | MCP clients are not browsers; EventSource can only GET |
| An older client that only knows 2024-11-05 | Keep the old SSE and POST endpoints beside the new one | The 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); } } UiMcpServer::handle() answers initialize with protocol version 2025-06-18, lists four tools (ui_catalog, ui_schema, ui_validate, ui_render) and treats notifications as 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 code and docs state the design plainly: 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: an MCP server over Streamable HTTP. If you need a remote MCP server today, this is not it. An idea to derive MCP tools from the route contract exists in the backlog but is not built, and this article does not depend on it.
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. Semitexa's page transport is two doors, KISS and HUG:
| Door | Route | Direction | Carries |
|---|---|---|---|
| KISS | GET /__semitexa_kiss | server to browser | one SSE stream per page: deferred slots, UI patches, component state, live feeds |
| HUG | POST /__semitexa_hug | browser to server | every 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 that Streamable HTTP simplified. Same pattern, different client, different verdict.
Because the 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 says so rather than pretending. That is the same resumability the MCP spec describes for Streamable HTTP, and the article EventSource Reconnects: Last-Event-ID and the Events You Missed walks through it step by step.
A practical checklist
- Local tool launched by the agent: stdio. Log to stderr, never print anything else to stdout.
- Remote or shared: Streamable HTTP on one endpoint. Validate
Origin, authenticate, bind narrowly. - Return plain JSON for short calls and open an SSE stream only when you really stream.
- If you stream, number your events and keep them so
Last-Event-IDcan resume. - Do not add 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.
Sources
- MCP specification 2025-06-18, Transports: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
Originally published on semitexa.com: Streamable HTTP vs SSE for MCP Servers. The ui:mcp command and the docs on screens an AI composes are at semitexa.com/docs/rendering/ai-ui.


