02 · Streamable HTTP & SSE Transport¶
Everything so far has used stdio: the host spawns your server as a child process and talks to it over stdin/stdout. That only works when client and server live on the same machine. A server deployed to the cloud, shared by many users, needs a transport that works over a network — that's Streamable HTTP.
Why stdio can't cross a network¶
stdio transport assumes:
- The server is a subprocess the host directly spawned and owns.
- There's exactly one client talking to exactly one server process.
- The pipe is private — no other process can read or write it.
None of that holds for a server running on infrastructure you don't control from the client's machine. You need a transport built on a protocol that networks actually support, with a notion of separate client connections and message framing that survives going through load balancers and proxies. HTTP is that protocol.
How Streamable HTTP works¶
A Streamable HTTP MCP server exposes a single endpoint, typically /mcp,
that handles two kinds of traffic:
POST /mcp— the client sends a JSON-RPC request or notification as the request body. For a simple request/response exchange, the server's HTTP response body is just the JSON-RPC response.GET /mcpwithAccept: text/event-stream— opens a long-lived Server-Sent Events stream the server can use to push messages to the client without the client having to poll (notifications, or responses to requests the server is still processing).
A Mcp-Session-Id header, set by the server on its first response and
echoed back by the client on every subsequent request, ties a sequence of
HTTP requests together into one logical MCP session — the network
equivalent of "the same stdio pipe."
A minimal HTTP server (Python)¶
# http_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("http-demo")
@mcp.tool()
def echo(message: str) -> str:
"""Returns the message back unchanged."""
return message
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Point the Inspector at a URL instead of a local command to connect to it:
npx @modelcontextprotocol/inspector
# then choose "Streamable HTTP" and enter http://127.0.0.1:8000/mcp
A raw POST, to see the shape¶
You don't need the Inspector to talk to this server — it's just HTTP:
curl -s http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": "echo", "arguments": {"message": "hi"}}
}'
The response is the same JSON-RPC result object you'd see over stdio —
the transport changed, the protocol on top of it didn't.
How It Actually Works¶
- Every request is still JSON-RPC. Streamable HTTP doesn't change what
a request, response, or notification looks like — only how the bytes get
from client to server and back.
tools/callover HTTP has the exact sameparamsshape astools/callover stdio. - The session ID is the glue. stdio gets "this is the same
conversation" for free, because it's one process talking over one pipe.
HTTP requests are independent by default, so the server issues a
Mcp-Session-Idon theinitializeresponse, and the client must send it back on every later request so the server can find the right in-memory session state. - SSE is for the server-initiated direction. A plain HTTP response can
only answer the request that triggered it. But MCP needs the server to
be able to send unsolicited messages — a progress notification during a
long tool call, or a
sampling/createMessagerequest asking the client's LLM for a completion. TheGETendpoint's event stream is how those reach the client without the client polling. - A
POSTcan itself open a stream. If handling a request will take a while or needs to emit intermediate notifications, the server can respond to thatPOSTwithContent-Type: text/event-streaminstead of a single JSON body, streaming the eventual result as one of the events. - Statelessness is a deployment choice, not a protocol requirement. A server can be written to keep no session state at all (treating every request independently) specifically so it scales horizontally without sticky sessions — more on this trade-off in Level 4.
Exercise¶
Add a second tool to http_server.py, slow_count(n: int), that sleeps for
one second n times. Run the server, then use the Inspector's "History" or
network panel to watch what happens on the connection while a call to
slow_count(3) is in flight — note that the HTTP request doesn't return
until the tool finishes, even though nothing is streamed back incrementally
in this simple version. That gap is exactly what Level 3's progress
notifications are for.