Senior · Core path · 8 chapters
Transports
Stdio for local processes, streamable HTTP for remote servers; which applies when, and the operational difference
Transport mistakes surface as hanging sessions, missing authentication, unusable logs, or accidental access to a user's local privileges.
Read these first
What you will know
Run a server over stdio and the same one over HTTP
The subprocess model suits a single local client, while a remotely reachable service must also handle network failures, connection management, and deployment boundaries.
Choose the right transport for a local tool and for a shared remote one
The decision hinges on process ownership, trust boundaries, concurrency, and whether clients need independent authentication and scaling.
Instrument and compare lifecycle, scaling, authentication, and observability for stdio and HTTP
HTTP deployments add proxy timeouts, session state, credential handling, and distributed tracing concerns that a local pipe can hide.
Debug a transport that connects but never responds
A successful connection can still conceal mismatched framing, buffering, message termination, proxy behavior, or an event-loop stall.
Run a privilege test showing that a local stdio server is not a security boundary
A child process commonly inherits the user's identity and filesystem access, so launching it locally does not constrain what the tool can do.
Chapters
- 1
Choosing stdio or streamable HTTP
MCP defines two transports, and one implementation can be deployed over either: as a subprocess of a single local client, or as a reachable service. The property that decides is process ownership rather than latency — a per-user tool wants to be the user, with their checkout and their credentials, which the parent-child relationship supplies for nothing, while a shared tool wants to be nobody's process in particular. Running the HTTP build on localhost to keep one artefact throws all of that away and puts an unauthenticated socket on every laptop.
- 2
The stdio message stream
Under stdio the server's standard output is the wire, so a print() statement that passes every unit test corrupts the message stream the instant a real client launches the server; standard error is left free for exactly this. Block buffering, pretty-printed JSON, CRLF translation and a wrapper script piping the server through tee are the same fault in other costumes. When a launch succeeds and zero messages arrive, raising the connect timeout from 10 s to 60 s diagnoses nothing — it converts a fast, legible failure into a slow one.
- 3
Caller identity under HTTP
Moving a working stdio server to streamable HTTP makes authentication your problem for the first time: there is no parent process whose identity the server can quietly inherit, and one long-lived process now serves every caller. The database password in a dev environment variable silently converts one person's access into everyone's, usually by way of a service role granted pg_read_all_data for an afternoon during the rollout. Establishing a subject per request is OAuth 2.1 work; turning that subject into downstream authority is a choice between SET LOCAL ROLE, row-level security and token exchange, and no library makes it for you.
- 4
The streamable HTTP transport
The same server that spoke framed JSON-RPC over stdout now speaks it to a single HTTP endpoint: the client POSTs a request, the server answers either with one JSON response or with an SSE stream it can keep writing to, and a session identifier issued during initialize ties every later request to the same session. Because the choice between the two answer shapes is the server's and is made per request, a client that does not accept both content types is refused with 406 before its message reaches a handler. Event ids on the stream are what let a client resume after a dropped connection instead of paying for a 40-second call twice.
- 5
Proxy timeouts on long calls
Sessions establish, short calls work, and anything slow fails at a suspiciously round number — that is an idle timeout in a proxy closing the response stream while your handler is still working, not your handler crashing. Failures clustered inside 100 ms of 60 seconds come from a configured limit, and the server logging a completed call followed by a broken pipe is the evidence that settles it. The fixes are traffic that keeps the stream from idling, raised timeouts at every hop, event ids the client can resume from, or a tool contract that hands back a job id in 40 ms — and each covers a different subset of the failure.
- 6
Session affinity across replicas
An MCP session over HTTP is not stateless: the identifier issued during initialize is a handle onto in-memory state — negotiated capabilities, the event buffer, subscriptions, progress tokens, a warm workspace — that a second replica has no way to reconstruct, so it answers 404 and is right to. Going from one pod to three round-robins two requests in three onto replicas that never saw the handshake. The options are session affinity hashed on the Mcp-Session-Id header, a shared store every replica reads, or a stateless mode that gives up resumption, subscriptions and server-initiated requests.
- 7
Loopback binding and Origin validation
Binding an unauthenticated MCP server to 0.0.0.0:8931 because it is 'only local' exposes it twice: anyone on the same network can hit the port, and any page the developer visits can make their browser post to it from inside the loopback interface where every firewall rule has already been satisfied. The specification's local-transport guidance is two rules together — validate the Origin header on all incoming connections, and bind to 127.0.0.1 — because DNS rebinding defeats address-based reasoning and headers survive it. A token in a 0600 file covers the column neither rule reaches: every other process running as that user.
- 8
Transport and confinement
A child process inherits the launching user's identity, environment, filesystem and network position, so running locally as a subprocess bounds who may reach the server and constrains nothing about what it may do. The stronger version of the objection is not that the user could run the query themselves — it is that the caller is a model steered by text the user did not write, including retrieved documents and the tool descriptions of the other five or six servers in the same session. Confinement belongs in the handler and in the operating system, and a container launched with `-i` still speaks stdio.
External resources
- Transports — Model Context Protocol specification (2025-11-25)modelcontextprotocol.io
Normative wire rules for stdio and Streamable HTTP, including the single POST/GET endpoint.
- Layermodelcontextprotocol.io
Explains when to use stdio versus Streamable HTTP and their operational differences.
- MCP for Beginners — HTTP Streaming modulegithub.com
Runnable walkthrough of moving a server from stdio to streamable HTTP.
- MCP Transport: Stdio vs Streamable HTTP — Architecture, Latency, Trade-offstruefoundry.com
Operational comparison: process lifecycle, latency, and deployment consequences.
- stdio vs Streamable HTTP — The MCP Transport DecisionStackOps AI
Shows a local stdio server breaking once containerized, and why.