MCP stdio vs. Streamable HTTP: which transport should you use?

LLM Mart · Sep 19, 2026 · 3 views 233 listing impressions

Choose stdio when the client should launch the server as a local subprocess on the user's machine. Choose Streamable HTTP when one hosted endpoint serves many clients over a network. The protocol semantics are identical either way — a transport is only a binding that defines how messages are framed, delivered, and cancelled.

That makes the decision an operational one rather than a protocol one. Ask where the capability runs and who has to reach it, and the transport follows.

The short version

stdio Streamable HTTP
Runs as A subprocess the client launches An independent service
Reached by One client, on one machine Many clients, over a network
Framing Newline-delimited JSON-RPC on stdin/stdout One HTTP POST per message to a single MCP endpoint
Credentials From the process environment OAuth 2.1 bearer tokens
Authorization spec Should not follow it Should follow it
Scaling One process per client Horizontal, stateless
Distribution A package (npm, PyPI, a binary, a container) A URL
Who upgrades it Each user You
Network exposure None Public or private, but real

If MCP terminology is new, start with what an MCP server is.

How stdio actually works

The client launches your server as a child process and talks to it over the standard streams. The server reads JSON-RPC messages from stdin and writes them to stdout, one message per line, with no embedded newlines.

Three rules follow, and each one is a real-world bug when broken:

  • The server must write nothing to stdout that is not a valid MCP message. This is the single most common stdio defect. A stray console.log or print() corrupts the stream and the connection drops with no useful error.
  • stderr is yours. The server may write UTF-8 to stderr for any logging purpose, and clients may capture, forward, or ignore it — and should not treat output there as an error condition.
  • There is no header layer. All request metadata travels inline in the message body, in _meta.io.modelcontextprotocol/* fields.

Lifecycle is the other half of the binding. The client initiates shutdown by closing the server's input stream and waiting for it to exit, escalating to forced termination if it does not. Servers should exit promptly when stdin closes or reads return end-of-file — that is the primary graceful shutdown signal and the only portable one. If the process exits unexpectedly, the client should restart it; because the protocol is stateless, in-flight requests are simply lost and can be retried against the fresh process.

Cancellation on stdio is a notifications/cancelled notification referencing the request ID, because a single shared channel has no per-request stream to close.

How Streamable HTTP actually works

The server exposes a single HTTP endpoint that accepts POST — for example https://api.example.com/mcp. Every JSON-RPC request or notification the client sends is its own POST.

The server answers each request with one of two content types, and clients must support both:

  • application/json — a single JSON object.
  • text/event-stream — an SSE stream scoped to that request, carrying any request-related notifications (progress, logging) and then the final response, which terminates the stream.

Long-lived change notifications are not a side channel: a client obtains them by sending a subscriptions/listen request whose response is itself a long-lived SSE stream.

Three requirements in the current revision catch people who learned an earlier one:

Required headers. Every POST must carry MCP-Protocol-Version, and its value must match the io.modelcontextprotocol/protocolVersion field in the body. Mcp-Method is required on all requests, and Mcp-Name on tools/call, resources/read, and prompts/get. These mirror body fields into headers so load balancers and gateways can route and rate-limit without parsing the body.

Header and body must agree. A server that processes the body must reject requests whose headers do not match it, with 400 Bad Request and a HeaderMismatch error (-32020). This exists because an intermediary routing on the header while the server executes on the body is a security problem, not a cosmetic inconsistency.

Origin validation. Servers must validate the Origin header on all incoming connections and respond 403 Forbidden when it is present and invalid. Servers running locally should bind to 127.0.0.1 rather than 0.0.0.0. Without both, a malicious web page can use DNS rebinding to reach a local MCP server from the browser.

What changed in the 2026-07-28 revision

If you are reading older tutorials or maintaining an older client, four mechanisms are gone from Streamable HTTP:

Removed What replaced it
The standalone GET stream endpoint subscriptions/listen, whose response stream carries change notifications
Protocol-level sessions (Mcp-Session-Id, DELETE to terminate) Statelessness; carry state in explicit handles returned by tools
Server-initiated JSON-RPC requests on SSE streams Input requests embedded in results, per multi round-trip requests
Resumable streams via Last-Event-ID Nothing; streams are not resumable

A server that supports only this revision and receives older traffic should answer GET or DELETE on the MCP endpoint with 405 Method Not Allowed, ignore any Mcp-Session-Id without minting or echoing one, and ignore Last-Event-ID.

The older HTTP+SSE transport from 2024-11-05 is a separate thing and has been deprecated since 2025-03-26. New implementations should not adopt it, and existing ones should migrate. It remains eligible for removal in a future revision. Publish an "sse" remote entry only to keep existing clients working, and plan its retirement.

Choosing: the questions that actually decide it

Does the capability need the user's machine? Reading local files, driving a local application, using a locally installed toolchain, or reaching a private network from the user's position — all stdio. A hosted endpoint cannot see the user's filesystem, and giving it that reach would be worse if it could.

Does the capability need your infrastructure? A database, a licensed API, a model you host, an internal system with its own access control — HTTP. Shipping those credentials to every user's machine in a stdio package is a distribution problem you do not want.

Who must be able to fix a bug? With stdio, every user upgrades a package. With HTTP, you deploy once. For a capability that changes often or has a security surface, that difference dominates.

How many clients? One process per client is fine for a developer tool and untenable for a product with users. The HTTP handler builds a fresh server instance per request and holds nothing between requests, so the endpoint scales horizontally with no extra work.

Prototyping? stdio, almost always. There is no deployment, no TLS, no OAuth, and MCP Inspector launches it with one command.

Credentials work differently, on purpose

This is where the two transports genuinely diverge, and the specification is explicit about it.

stdio implementations should not follow the authorization specification. They retrieve credentials from the environment. A stdio server gets an API key the way any local process does — an environment variable, a credential helper, a config file the user controls. There is no OAuth flow because there is no network boundary to authorize across.

HTTP implementations should follow it. A protected MCP server is an OAuth 2.1 resource server: it publishes protected resource metadata, challenges unauthenticated requests with 401 and a WWW-Authenticate header, accepts bearer tokens on every request, and must validate that a token was issued for it as the intended audience. Clients must send the resource parameter per RFC 8707 so tokens are bound to your server rather than usable anywhere.

The practical consequence: moving a server from stdio to HTTP is not a transport swap. It is a transport swap plus an authorization model you did not previously need. MCP authentication explained covers what that involves.

Debugging differs too

With stdio, the failure surface is the process: wrong command, wrong arguments, missing environment variable, a dependency that will not resolve, or output on stdout. MCP Inspector's Console view shows the process's stderr, which is where the answer usually is.

With HTTP, the failure surface is the request: status codes, headers, TLS, proxies, and auth challenges. Inspector's Network view shows status, headers, and bodies. The two views never appear together, because a server is one or the other.

One HTTP-specific operational note: when initiating an SSE stream, servers should send X-Accel-Buffering: no so reverse proxies do not buffer events. Without it, a proxy can accumulate messages and turn a streaming response into a long silence followed by a burst.

Publishing and installation

The choice also decides how people get your server.

A local server is published as a package — npm, PyPI, NuGet, a container image — and the registry entry describes the package plus its transport:

"packages": [
  { "registryType": "npm", "identifier": "@example/catalog-mcp",
    "version": "1.2.3", "transport": { "type": "stdio" } }
]

A remote server is published as a URL, and must be publicly accessible at that URL:

"remotes": [
  { "type": "streamable-http", "url": "https://analytics.example.com/mcp" }
]

The two can coexist in one entry, letting the host application choose. Remote entries also support URL template variables for multi-tenant deployments and declared headers for API-key authentication. See how to publish an MCP server to the official MCP Registry for the mechanics.

On the client side, the distinction shows up directly in installation. Claude Code, for example, takes --transport http with a URL for remote servers and a launch command after -- for stdio ones; see how to add an MCP server to Claude Code safely.

Support both when it earns its keep

Nothing stops one codebase from serving both. In the current TypeScript SDK the server is built by a factory function, and the same factory is handed to serveStdio for local use or createMcpHandler for HTTP. The tool definitions do not change.

Do it when you genuinely have both audiences: developers who want a local process and an organisation that wants a managed endpoint. Do not do it by default — two transports means two deployment stories, two auth stories, and two sets of bugs.

Next step: Use LLM Mart to compare local and remote MCP server installation requirements before choosing a transport, then check whether the server you are modelling yours on needs the user's machine or yours.

Sources

0 0 0 0 Sign in to react

Comments (0)

Sign in to join the conversation.

No comments yet.