TAI BUI
Back to curriculum
LearnPython

MCP Transports — stdio vs Streamable HTTP vs SSE Migration

stdio is for the local machine. Streamable HTTP (2025-03-26) is the remote standard, and the old HTTP+SSE transport is deprecated and being removed in mid-2026. Picking the wrong transport costs you a migration. Picking the right one gets you a remote-hostable MCP server with session continuity and DNS-rebinding protection.

TIME: ~45 minutesPREREQUISITES: Phase 13 · 07, 08 (MCP server and client)

Learning Objectives

  • Pick between stdio and Streamable HTTP based on deployment shape (local vs remote, single-process vs fleet).
  • Implement the Streamable HTTP single-endpoint pattern: POST for requests, GET for session stream.
  • Enforce `Origin` validation and session-id semantics to defeat DNS-rebinding.
  • Migrate a legacy HTTP+SSE server to Streamable HTTP before the mid-2026 removal deadlines.

The Problem

The first MCP remote transport (2024-11) was HTTP+SSE: two endpoints, one for the client's POSTs and one Server-Sent-Events channel for the server-to-client stream. It worked, but it was clumsy. Each session needed two endpoints, some CDNs broke caching in front of it, and it depended on long-lived SSE connections that some WAFs terminate aggressively.

The 2025-03-26 spec replaced it with Streamable HTTP: one endpoint, POST for client requests, GET for establishing a session stream, and a shared Mcp-Session-Id header tying them together. Servers built or migrated since then use Streamable HTTP, and the old SSE mode is being deprecated. Atlassian Rovo removed it June 30, 2026, Keboola April 1, 2026, and most remaining enterprise servers are due by the end of 2026.

stdio still matters for local servers. Claude Desktop, VS Code, and other IDE-shaped clients spawn servers over stdio. The mental model I use is stdio for "this machine" and Streamable HTTP for "over the network", with no crossover between them.

Warm-up0/3
1. In the MCP servers and clients from Lessons 07 and 08, what message format do the two sides exchange?Warm-up
A client box labelled Lesson 08 and a server box labelled Lesson 07 with arrows in both directions to a dashed box in the middle that reads a message, format question mark.
Schematic. The message format is left unknown.
2. Which MCP request must a client send before it can call `tools/list`?Warm-up
A client lane and a server lane. Two requests go from client to server, top to bottom: a dashed box reading first request with a question mark, then a box reading tools/list.
Schematic. The first request is left blank.
3. A browser requests a URL and the server answers with status 404. What does that status normally mean?Warm-up
A browser window with an address bar showing https://example.com/missing-page and a large status code 404 in the page area.
Illustrative page. The address is a placeholder.
Check: The Problem0/4
1. What is the HTTP+SSE transport?Define
A card titled HTTP+SSE with the nickname Legacy and three removal notes: Atlassian Rovo removed June 30, 2026, Keboola removed April 1, 2026, and most remaining enterprise servers due by the end of 2026.
Removal dates as given in the lesson's Problem section.
2. What is Streamable HTTP?Define
A terminal with the command python code/main.py and its printed line: Streamable HTTP MCP endpoint on 127.0.0.1:8017/mcp, Ctrl-C to stop.
The printed line is the string main.py prints on start.
3. How should you choose between stdio and Streamable HTTP?Explain
Two dashed boxes side by side, stdio and Streamable HTTP, each reading use it when question mark.
Schematic. The deciding factor is left blank.
4. You ship a file-search tool that an IDE launches on each developer's laptop, and a shared company tool that hundreds of staff reach through Cloudflare. Which transports fit?Design
Two boxes: a file-search tool launched by an IDE on each laptop, and a shared company tool reached by hundreds of staff via Cloudflare. Each has a dashed slot reading transport question mark.
Schematic. Both transports are left blank.

The Concept

stdio

  • Child-process transport. The client spawns the server and talks to it over stdin/stdout.
  • One JSON object per line, newline-delimited.
  • No session id. The process identity is the session.
  • No auth needed, because the child inherits the parent's trust boundary.
  • Don't use it for remote servers. You would need SSH or socat to tunnel, and at that point Streamable HTTP is the better tool.

Streamable HTTP

A single endpoint, /mcp (or any path), supports three HTTP methods:

  • POST /mcp. The client sends a JSON-RPC message. The server replies with either a single JSON response or an SSE stream of one or more responses, which is useful for batched responses and notifications related to that request.
  • GET /mcp. The client opens a long-lived SSE channel. The server uses it for server-to-client requests (sampling, notifications, elicitation).
  • DELETE /mcp. The client explicitly terminates the session.

Sessions are identified by the Mcp-Session-Id header. The server sets it on the first response and the client echoes it on each subsequent request. Session ids must be cryptographically random (128+ bits), and client-chosen ids are rejected for safety.

Single endpoint vs two

Two-endpoint mode from the old spec is still callable in 2026, and the spec declares it "legacy compatible". New servers should still be single-endpoint. The official SDKs emit single-endpoint, so reach for the legacy mode only when talking to an unmigrated remote.

Origin validation and DNS-rebinding

Browsers are not MCP clients (today), but an attacker can craft a webpage that convinces a browser to POST to localhost:1234/mcp, where the user's local MCP server listens. If the server does not check Origin, the browser's same-origin policy will not save it, because Origin: http://evil.com is valid cross-origin.

The 2025-11-25 spec requires servers to reject requests whose Origin is not on an allowlist. The allowlist typically contains the MCP client host (https://claude.ai, vscode-webview://*) and localhost variants for local UIs.

Session id lifecycle

  1. Client sends first request without Mcp-Session-Id.
  2. Server assigns a random id and sets Mcp-Session-Id on the response header.
  3. Client echoes that header on all subsequent requests and on GET /mcp for the stream.
  4. Session can be revoked by the server. The client sees 404 on subsequent requests and must re-initialize.
  5. Client can explicitly DELETE the session for clean shutdown.

Keepalive and reconnect

SSE connections drop. The client re-establishes by re-GETing with the same Mcp-Session-Id. The server must queue events missed during the outage (up to a reasonable window) and replay them via the last-event-id header the client echoes.

Phase 13 · 13 covers Tasks, which let long-running work survive even a full-session reconnect.

Backwards compatibility probe

A client that wants to support both old and new servers:

  1. POST to /mcp.
  2. If the response is 200 OK with JSON or SSE, this is Streamable HTTP.
  3. If the response is 200 OK with Content-Type: text/event-stream AND a Location header pointing to a secondary endpoint, this is legacy HTTP+SSE. Follow the Location.

Cloudflare, ngrok, and hosting

Production remote MCP servers in 2026 run on Cloudflare Workers (with their MCP Agents SDK), Vercel Functions, or containerized Node/Python. The constraint to check is whether your hosting supports long-lived HTTP connections for the SSE GET. Vercel's free tier caps at 10 seconds and is unsuitable. Cloudflare Workers support indefinite streams.

Gateway composition

When you front multiple MCP servers with a gateway (Phase 13 · 17), the gateway is a single Streamable HTTP endpoint that rewrites session ids and multiplexes upstream. Tools are merged at the gateway layer, so the client sees a single logical server.

Transport failure modes

  • stdio SIGPIPE. Child process death mid-write raises SIGPIPE, and servers should exit cleanly. Clients should detect EOF and mark the session dead.
  • HTTP 502 / 504. Cloudflare, nginx, and other proxies emit these on upstream failure. Streamable HTTP clients should retry once after a short backoff.
  • SSE connection drop. TCP RST, proxy timeout, or a client network change closes the stream. The client reconnects with Mcp-Session-Id and optional last-event-id to resume.
  • Session revocation. The server invalidates a session id, the client sees 404 on the next request, and it must re-handshake.
  • Clock skew. Resource-TTL calculations on the client diverge from the server. The client should treat server timestamps as authoritative.

When to bypass Streamable HTTP

Some enterprises deploy MCP servers behind gRPC or message-queue transports inside their own networks. This is non-standard, since MCP's spec does not formally define these. A gateway can expose a Streamable HTTP surface to MCP clients while using gRPC internally. Keep the external surface spec-compliant and let the gateway own the translation.

Check: The Concept0/21
1. What is the stdio transport?Define
A JSON configuration snippet naming a server called file-search with command python and arguments server.py.
Typed example in the style of IDE configs. The names are made up and the file is not from the lesson.
2. What does 'single endpoint' mean in Streamable HTTP?Define
A client box with an arrow to a door labelled /mcp on a server box, with a question mark above the arrow.
Schematic. The question mark stands for the unexplained term.
3. What must an `Mcp-Session-Id` value look like?Define
A terminal listing two requests. The first has no session header and the server's 200 reply carries Mcp-Session-Id with a hidden value. The second request repeats that header with the same hidden value.
Typed, not captured. The value is hidden.
4. What is an `Origin` allowlist?Define
A code excerpt from main.py defining ORIGIN_ALLOWLIST as a set with four entries: http://localhost, http://127.0.0.1, https://claude.ai and vscode-webview://localhost.
Excerpt copied from the lesson's main.py.
5. What is session revocation?Define
Three boxes in a row joined by arrows: session in use with requests carrying the id, session revocation highlighted, and a dashed box reading question mark, what happens next.
Schematic. The last step is left blank.
6. What is the `last-event-id` header for?Define
A terminal showing three server-sent events with id lines 0, 1 and 2, an event line and a shortened data line each. Beside it a dashed box reads last-event-id, header question mark.
Event lines captured from a real run of main.py. Data lines are shortened from the left.
7. How does a client tell a legacy HTTP+SSE server from a Streamable HTTP one?Define
A client sending POST /mcp to two servers, one Streamable HTTP and one legacy HTTP+SSE, each shown as a dashed box with reply question mark.
Schematic. The replies are hidden.
8. What does 'long-lived HTTP' mean for an MCP server?Define
A terminal with a GET /mcp request carrying a session id, then the server's 200 reply and its first event: id 0, event message and a shortened data line.
Reply lines captured from a run of main.py. The session id is a placeholder and the data line is shortened.
9. The client already sends requests with POST. Why does Streamable HTTP also need GET /mcp?Explain
A client and a server at /mcp. A solid arrow labelled POST /mcp carries a client request and gets a JSON or SSE reply. A dashed line labelled GET /mcp has a question mark under it.
Schematic. The purpose of GET is left blank.
10. Why does stdio need no auth layer, and where should you avoid it?Explain
A client parent process and a server child process connected by a stdio arrow. Two dashed slots read auth layer question mark and where to avoid it question mark.
Schematic. Both slots are left blank.
11. What is the guidance for new servers on the old two-endpoint mode?Explain
Two dashed boxes: single-endpoint mode and two-endpoint mode from the old spec, each reading for a new server question mark.
Schematic. The guidance is left blank.
12. How does a DNS-rebinding attack reach a local MCP server?Explain
The user's machine holds a local MCP server at localhost:1234/mcp. An attacker box on the internet is joined to it by a dashed line marked with a question mark and the word how.
Schematic. The path is left blank.
13. A server revokes a session id. What does the client see and do?Explain
A client and server sequence: a request with the session id, a box where the server revokes the id, a next request from the client, and a dashed reply labelled client sees question mark.
Schematic. The reply is left blank.
14. A client's SSE stream drops and it reconnects with `last-event-id: 1`. What should a complete server do?Explain
Four event boxes id 0 to id 3 with a dashed red line after id 1 marking where the stream drops. A reconnect request below carries the same session id and last-event-id 1, and a dashed box asks what a complete server sends.
Illustrative event count. The server's action is left blank.
15. What is the right response to an SSE connection drop and to clock skew?Explain
Two panels. The first shows an SSE stream cut by a red X with client should question mark. The second shows a client TTL calculation and a server timestamp that disagree, with client should question mark.
Schematic. Both responses are left blank.
16. Why is Vercel's free tier a poor host for a Streamable HTTP MCP server?Explain
A client connected to a Streamable HTTP MCP server box with POST, GET and DELETE on /mcp, inside a region labelled Vercel Functions, free tier. A question mark asks what goes wrong here.
Schematic. The failure is left blank.
17. What does a gateway in front of several MCP servers do?Explain
A client connects to a dashed gateway box that reads does question mark, which connects to server 1, server 2 and server 3. Below it a line reads the client sees question mark.
Schematic. The gateway's role is left blank.
18. A developer implements the `https://*.example.com` wildcard by checking that the Origin starts with `https://` and contains `example.com`. Which Origin does it wrongly accept?Analyze
A code block with the allowed pattern https://*.example.com and a function allowed that returns true when the origin starts with https:// and contains example.com.
Typed from the question's description of the check.
19. A proxy in front of a Streamable HTTP server cuts every SSE stream after about a minute. Clients reconnect but miss notifications sent during each gap. What is missing?Analyze
A timeline with three green SSE stream bars of about one minute each separated by two red dashed gaps. Each gap is labelled client reconnects, with a notification sent during it.
Schematic of the situation in the question.
20. You run a Streamable HTTP server behind a proxy. After a deploy, every client gets 404 on its next request, though the server is healthy. What is the most likely cause?Analyze
Three clients each get 200 before a deploy line and then 404 on their next request after it. A green box says the server is healthy and a dashed box reads cause question mark.
Schematic of the question's situation.
21. An internal MCP service speaks gRPC between teams, and a client vendor now needs to reach it over MCP. Which design fits?Design
Inside the company, teams and a service talk gRPC. Outside, a vendor client speaks MCP. A dashed line with a question mark and the word design joins the two.
Schematic. The connecting design is left blank.

Use It

code/main.py implements a minimal Streamable HTTP endpoint using http.server (stdlib). It handles POST, GET, and DELETE on /mcp, sets Mcp-Session-Id on the first response, validates Origin, and rejects requests from non-allowlisted origins. The handler follows the dispatch shape from the Lesson 07 server, with a single ping tool.

What to look at:

  • The POST handler reads the JSON-RPC body, dispatches, and writes a JSON response (the single-response variant; the SSE variant is structurally similar).
  • The Origin check rejects the default http://evil.example probe but accepts http://localhost.
  • Session ids are random 128-bit hex strings, and the server keeps per-session state in memory.
Check: Use It0/5
1. A client POSTs `{"jsonrpc":"2.0","id":2,"method":"tools/list"}` to `code/main.py` with an allowed Origin but no `Mcp-Session-Id` header. What comes back?Apply
A terminal showing POST /mcp with Origin http://localhost, a JSON body for tools/list with id 2, no Mcp-Session-Id header, and a final line response question mark.
Request as described in the question. The response is hidden.
2. With `origin_allowed` in `code/main.py` and its allowlist, which Origin value is accepted?Apply
A code excerpt from main.py: the four-entry ORIGIN_ALLOWLIST set and the origin_allowed function, which returns True when the origin equals an entry or starts with an entry followed by a slash or a colon.
Copied from the lesson's main.py. The long condition is wrapped over three lines.
3. A client sends DELETE /mcp with an allowed Origin and an `Mcp-Session-Id` the server never issued. What does `do_DELETE` return?Apply
A terminal showing DELETE /mcp with Origin http://localhost and a session id the server never issued, a note that SESSIONS does not hold it, and a line response question mark.
Request as described in the question. The response is hidden.
4. What do the Origin check and session storage in `code/main.py` do?Apply
Two dashed boxes, Origin check and session storage in code/main.py, each reading behaviour question mark.
Schematic. Both behaviours are left blank.
5. The GET handler in `code/main.py` streams events with ids 0, 1 and 2, then returns. A client reconnects to the live session with `last-event-id: 1`. What does it receive?Apply
An excerpt of the GET handler loop that writes events with ids 0 to 2 and sleeps 0.05 s, followed by a reconnect request with a live session id and last-event-id 1 and a line receives question mark.
Handler lines shortened from main.py. The result is hidden.

Ship It

You produce outputs/skill-mcp-transport-migrator.md. Given an HTTP+SSE (legacy) MCP server, the skill produces a migration plan to Streamable HTTP with session-id continuity, Origin checks, and backwards-compatible probe support.

Check: Ship It0/1
1. What does the mcp-transport-migrator skill produce?Explain
A legacy HTTP+SSE server feeds the mcp-transport-migrator skill, which feeds a dashed box reading output question mark.
Schematic. The output is left blank.
Author's Note

I'd run python main.py --probe before reading the handler code. It starts the server on port 8017 and walks five steps over loopback: a POST from http://evil.example gets a 403, an initialize from http://localhost gets a session id, that id is echoed on tools/list, a DELETE returns 204, and a GET on the dead session returns 404. That covers the session lifecycle from the Concept section in a few lines of output.

The function worth reading closely is _resolve_session. Only initialize can mint a session, and any other method with a missing or unknown Mcp-Session-Id gets a 404. That gives the client one signal for revocation and one response to it, which is to re-initialize.

I'd also be clear about what the skeleton skips. The GET handler sends three progress events with ids and then returns, and nothing reads last-event-id, so there is no replay. POST here answers with a single JSON body rather than an SSE stream. Sessions live in a dict in process memory, so a restart drops all of them. Exercise 3 closes the replay gap, and I'd close it before putting anything like this behind a proxy.

Exercises

  1. Run code/main.py. POST an initialize from curl and observe the Mcp-Session-Id response header. POST a second request echoing the header and verify session continuity.

  2. Change the GET handler so the SSE stream stays open and sends one notifications/progress event every five seconds, instead of three events 0.05s apart. Reconnect by re-GETing with the same session id and confirm the server accepts it.

  3. Implement the last-event-id replay logic. On reconnect, replay any events generated since that id.

  4. Extend Origin validation to support a wildcard pattern (https://*.example.com) and confirm it accepts https://app.example.com but rejects https://evil.example.com.attacker.net.

  5. Take a legacy HTTP+SSE server from the official registry (there are several) and sketch the migration: what changes in endpoint handling, session id generation, and header semantics.

Key Terms

TermWhat people sayWhat it actually means
stdio transport"Local child process"JSON-RPC over stdin/stdout, newline-delimited
Streamable HTTP"The remote transport"Single-endpoint POST + GET + optional SSE, 2025-03-26 spec
HTTP+SSE"Legacy"Two-endpoint model being removed in mid-2026
Mcp-Session-Id"Session header"Server-assigned random id echoed on every subsequent request
Origin allowlist"DNS-rebinding defense"Reject requests whose Origin is not approved
Single endpoint"One URL"/mcp handles POST / GET / DELETE for all session operations
last-event-id"SSE replay"Header used to resume a dropped stream without missing events
Backwards-compat probe"Old vs new detection"Client response-shape check that auto-selects transport
Long-lived HTTP"SSE streaming"Server pushes events for minutes or hours on one TCP connection
Session revocation"Force re-init"Server invalidates a session id; client must handshake again

Further Reading

Code

Ships With