ChangeGamer

← All guides · MCP in practice

stdio vs. Streamable HTTP for MCP Servers: A Decision Framework

Part 1 of MCP in practice · 1,628 words · published 2026-08-06 · updated 2026-08-06 · Markdown variant

Which MCP transport to build against and why: the single-client-vs-shared decision rule, how state works without a session handshake under the 2026-07-28 spec, the auth-model switching cost, and what actually breaks migrating off HTTP+SSE.

In short

  • Pick transport by counting clients and machines, not by feature preference: stdio if the server and its one client always run on the same machine, Streamable HTTP the moment a second user, a second client, or a remote deployment enters the picture.
  • As of the 2026-07-28 spec revision, Streamable HTTP dropped the initialize/initialized handshake and the Mcp-Session-Id header entirely — any request can land on any server instance, and a server that needs cross-call state (a basket, a wizard step) has to mint its own opaque handle and have the model pass it back as an ordinary tool argument on the next call.
  • Moving a server from stdio to Streamable HTTP is not a drop-in swap of the transport binding — it is also an auth-model change, from "no OAuth surface, credentials via environment variables" to "OAuth 2.1 required if the server authenticates at all," and it is legitimate to land on the unauthenticated end of that range for a public, read-only server.
  • Migrating an existing server off the deprecated HTTP+SSE transport means re-deriving your auth model, not just re-pointing a client library — HTTP+SSE and Streamable HTTP have different auth surfaces, and any code that assumed a stable session ID will misbehave under the stateless 2026-07-28 model unless you replace that assumption with an explicit handle.
  • WebMCP is not a third MCP transport and does not belong in this decision — it is a same-tab, no-network-hop browser API for a page to expose tools to an in-browser agent, explicitly distinct from MCP's client-server model.

Part of the MCP Server in Production: How to Build, Ship and Run One guide.


The MCP transport comparison in MCP server in production tells you what the two transports are. This article is the decision you actually have to make: which one to build, what changes underneath you when you pick Streamable HTTP, and what breaks if you are migrating an existing server rather than starting fresh.

The decision rule

Count clients and machines, not features. If your server will only ever be spawned as a child process by exactly one client on the same machine — a local dev tool, a personal automation, an integration nobody but you will connect to — stdio is correct, and adding anything else is unnecessary surface. The moment a second user, a second client, or a remote deployment becomes plausible, Streamable HTTP is correct: retrofitting a network transport onto a server built around a local child process is a bigger rewrite than starting there.

This is not a preference call. stdio and Streamable HTTP differ in network exposure, auth model, and — as of the 2026-07-28 spec revision — state handling, and each difference cascades into how you write the server, not just how you deploy it (building an MCP server).

If… Use Why the alternative is wrong
One client, one machine, no plan to share it stdio Streamable HTTP adds a network surface, an Origin-validation obligation, and a real service to operate, for zero benefit if nobody remote will ever connect
Multiple users or clients need to reach it Streamable HTTP stdio has no network path at all — it cannot serve a second machine by design
It has to run somewhere other than the caller's machine Streamable HTTP stdio requires the host to spawn the process locally; there is no remote-spawn variant
You are not yet sure Streamable HTTP The migration from stdio to a network transport is the expensive direction; the reverse almost never has to happen

What Streamable HTTP actually costs you beyond the transport

Choosing Streamable HTTP is not just picking a different SDK binding. It brings two obligations a stdio server never faces, and a third that is new as of 2026-07-28.

A real network service to operate. stdio ships as a local process or package with no server to run — the host spawns it and owns its lifecycle. Streamable HTTP is a service you deploy, keep online, put behind TLS, and monitor. The one infrastructure fact grounded in the current spec: statelessness "simplifies horizontal scaling" for Streamable HTTP servers — because any request can land on any instance, no sticky routing or shared session store is required in front of a scaled deployment.

A security checklist that didn't exist a moment ago. Streamable HTTP adds obligations stdio never faces — Origin-header validation against DNS-rebinding attacks chief among them, detailed in MCP server in production. The decision-relevant point isn't the checklist itself, it's that stdio isn't a shorter version of it: there's no network hop for any of it to attach to, which is exactly why bolting Streamable HTTP onto a server architected around stdio later is a real rewrite, not a config change.

A stateless core, as of the 2026-07-28 revision. Most write-ups still describe the older session model — worth getting right before you design your first stateful tool.

Session-handle mechanics under the 2026-07-28 revision

Before 2026-07-28, a Streamable HTTP session opened with an initialize/initialized handshake and could carry an optional Mcp-Session-Id header for stateful sessions. SEP-2575 removes the handshake entirely; SEP-2567 removes the Mcp-Session-Id header along with the protocol-level session it carried (MCP goes stateless). There is no negotiation step before normal requests, and no session identifier to recognize "this is the same caller as the last call."

The practical implication for a stateful tool: any request can now land on any server instance, with no sticky routing and no shared session store guaranteed by the protocol. If your tool needs to remember something across calls — a shopping basket a user is building, a wizard partway through — the protocol will not do that for you. Instead: the server mints its own opaque handle on the first call — a basket_id, a job token — and returns it in the tool result. The model treats that handle like any other value and passes it back as an ordinary argument on the next call that needs the same state.

This changes where state lives, not whether it can exist. A stateful tool under the 2026-07-28 model looks like:

create_basket() -> { basket_id: "b_8f2a", items: [] }
add_item(basket_id: "b_8f2a", sku: "widget-1") -> { basket_id: "b_8f2a", items: [...] }

rather than relying on a session header to implicitly scope add_item to the right basket. The server owns the storage keyed by that handle — a database row, a cache entry with a TTL — the same way it owns any other application state; MCP's transport no longer does that bookkeeping for you.

Two smaller changes ship in the same revision, and both only matter once you're actually on a network transport: per-request routing headers a gateway can act on without parsing the body, and explicit cache-lifetime hints on list/resource results — mechanics covered in MCP server in production. The reason they belong in this decision at all: neither has a stdio equivalent, because stdio has no gateway sitting in front of it and no multi-instance cache to invalidate.

The auth switching cost

This is the part of the stdio-to-Streamable-HTTP move that gets treated as an afterthought and shouldn't be — the two transports have completely different auth models, not just different wire formats (MCP server authentication).

A stdio server has no OAuth surface at all — the spec explicitly says stdio implementations should not follow the HTTP-based OAuth authorization flow, and credentials for whatever upstream API your server calls are injected via environment variables or the host process. That pattern is exactly what stops working the day you switch transports: an environment-variable credential assumes a locally-spawned process the host already trusts, and that assumption is gone the instant the caller is a remote client on the network instead.

A Streamable HTTP server that authenticates callers must implement the full OAuth 2.1 flow instead: mandatory PKCE (S256 only), RFC 8707 audience binding against confused-deputy replay, and a hard rule against forwarding a caller's Bearer token to any upstream API — your server gets its own credentials for that.

The precision point: moving to Streamable HTTP doesn't automatically mean adding OAuth. A remote, unauthenticated, read-only server is legitimate — ChangeGamer's own /mcp endpoint is exactly that, gating only paid resources behind an application-layer api_key argument; which resources that gate covers, and what they cost, is published at access and pricing rather than derived from the transport choice. Streamable HTTP makes authentication possible, and for anything that writes state or acts on a user's behalf, necessary — a switch that doesn't exist on the stdio side at all. Budget for it as a design decision, not a checkbox flipped because the transport changed.

Migrating off HTTP+SSE

If you are operating a server built before the 2025-06-18 spec revision made Streamable HTTP the standard remote transport, you are likely still running the older HTTP+SSE transport, and migrating is not a config flag. Two things change together, and both need deliberate work:

If your HTTP+SSE-era server also assumed a stable session identifier for cross-call state, that assumption breaks outright under the 2026-07-28 stateless model, in a way that looks like a bug in your own code rather than a spec change — the fix is the handle pattern above, an explicit identifier passed back as a tool argument. There is no documented dual-transport transition period or specific migration tool; treat it as three steps — swap the transport binding, re-derive the auth model, replace session-ID-shaped state with a handle — then re-test against a real client rather than trusting the old test suite.

Not to be confused with: WebMCP

If you are searching around this decision, you will run into WebMCP — it is not a third MCP transport and does not belong in this comparison. WebMCP is a same-tab, no-network-hop browser API that lets a web page expose tools directly to an agent in that same tab, explicitly distinct from MCP's client-server model over stdio or Streamable HTTP (WebMCP). It solves a different problem — in-page tool exposure — than the one this article is about.

Where this leaves you

Default to stdio for anything genuinely local and single-client, and to Streamable HTTP for everything else — that one question resolves the transport decision correctly in nearly every case. What it does not resolve automatically is auth (gated by whether the server authenticates callers at all, not by which transport it uses) or state (now an explicit design choice under the 2026-07-28 spec, not something the protocol hands you for free). Get those two right alongside the transport decision, and the rest of what a production MCP server needs — tool design, versioning, testing, distribution — is covered in MCP server in production.

Frequently asked questions

How do I decide between stdio and Streamable HTTP for a new MCP server?
Count clients and machines, not features. If the server will only ever be spawned as a child process by one client on the same machine — a local dev tool, a personal automation — use stdio: zero network exposure, credentials injected via environment variables, no OAuth surface at all. If more than one user, more than one client, or a remote deployment is even plausible, build on Streamable HTTP from the start; retrofitting a network transport onto a server designed around a local child process is a bigger rewrite than starting there.
How does an MCP server keep state without Mcp-Session-Id?
It mints its own handle. As of the 2026-07-28 spec revision (SEP-2575, SEP-2567), MCP removed the `initialize`/`initialized` handshake and the `Mcp-Session-Id` header — there is no protocol-level session for the server to rely on, and no sticky routing guarantee that puts repeat requests on the same instance. A server that needs cross-call state — a shopping basket, a multi-step wizard — creates its own opaque identifier (a `basket_id`, a job token) on the first call and returns it in the response; the model is expected to pass that identifier back as an ordinary tool argument on subsequent calls, the same way it would pass any other parameter.
What actually breaks in my auth code when I move a server from stdio to Streamable HTTP?
Not a missing OAuth flow so much as a credential model that quietly stops being valid. stdio credentials — API keys injected via environment variables — work because the host trusts the process it just spawned; that trust doesn't survive once the caller is an arbitrary remote client. Moving to Streamable HTTP doesn't automatically obligate OAuth (a public, read-only server can stay unauthenticated, the way ChangeGamer's own `/mcp` endpoint does), but if the server authenticates anyone, the environment-variable pattern has to be replaced wholesale with the full OAuth 2.1 flow — there is no lighter-weight remote option in the spec.
What breaks when migrating an MCP server from HTTP+SSE to Streamable HTTP?
Two things, neither of which is a simple find-and-replace. First, the transport binding itself changes — HTTP+SSE and Streamable HTTP are different wire behaviors in your SDK, so you are swapping the transport layer, not tweaking a config flag. Second, and easy to miss, the auth surface changes with it, so any auth code written against the old transport needs to be re-derived rather than carried over unmodified. If your HTTP+SSE-era code additionally assumed a stable session ID for cross-call state, that assumption breaks outright under the 2026-07-28 spec's stateless model and has to be replaced with an explicit handle passed back as a tool argument.

#mcp #agents #protocols #transport #oauth #production

Agents: this guide is available as Markdown and JSON; the whole cluster is indexed at /api/articles.json. The reference corpus behind it is at /llms.txt, with licensing at pricing.