ClaudeMap

·MCP Servers

A 2026 walkthrough of MCP's two transports — stdio (local subprocess) and Streamable HTTP — covering JSON-RPC framing, the handshake, OAuth 2.1, session state, debuggability, and a selection matrix, plus five transport incident patterns and the full impact of the 2026-07-28 spec change (stateless MCP, Mcp-Session-Id removal).

MCP Transports Explained: stdio vs Streamable HTTP — Choosing the Right One (2026)

The MCP 2025-11-25 spec defines two transports — stdio (local subprocess) and Streamable HTTP (remote HTTP + SSE) — and the choice between them is not just "port vs pipe." It's a fundamentally different trust model, auth boundary, session story, and performance profile. This guide, current as of the MCP 2025-06-18 transports spec and the 2025-11-25 documentation refresh, breaks down each transport by JSON-RPC framing, auth, session, performance, debuggability, and gives a 2026 decision table for five production scenarios.

TL;DR

  • stdio: Host runs the server as a child process; OS process boundary is the trust boundary; zero network attack surface; pick this for local / CLI / internal tools
  • Streamable HTTP: server runs on a separate machine; HTTP POST + SSE; OAuth 2.1 required; pick this for team-shared / SaaS / multi-Host reuse
  • Choosing a transport = choosing a trust model — not a performance question
  • The 2025-11-25 spec uses Streamable HTTP to replace the older HTTP+SSE transport; new projects should not use the old one
  • Coverage: JSON-RPC framing, handshake protocol, auth, session, debugging, performance, 5-scenario production decision table

stdio: the OS process is the trust boundary

In stdio transport, the Host starts the server process as its own child, with bidirectional communication over stdin/stdout carrying JSON-RPC frames. This is the "simplest but strongest" form of local integration — no network, no port, no TLS, no auth protocol, zero attack surface. The Host and server are separated by a single OS process boundary, and that boundary is the trust boundary.

JSON-RPC framing: each message is newline-delimited JSON (NDJSON), read line by line, UTF-8 encoded. The client writes to stdin; the server writes responses to stdout. Stderr is reserved for the server's own logs — this is a convention, not optional. If the server writes one console.log to stdout, the Host receives a non-JSON byte stream and the JSON-RPC parser immediately fails.

Lifecycle: Host starts → forks child → both run the initialize handshake → client sends notifications/initialized to tell the server "I'm ready" → server replies with nothing (this is a one-way notification) → enter normal tools/list / tools/call flow → on Host exit, send shutdown and close stdin; the child process sees EOF and exits naturally.

Debugging advantage: because both processes are on the same machine, the debugging toolchain is mature — gdb / lldb attach directly, strace shows syscalls, console.error goes straight to stderr. The reason MCP Inspector defaults to stdio is precisely this "open and debug" affordance. The flip side: the server must live on the same machine as the Host — no cross-network, no team sharing, no multi-Host reuse.

Streamable HTTP: the cross-network contract

Streamable HTTP transport was introduced in the MCP 2025-11-25 spec, replacing the 2024-11-05 spec's old HTTP+SSE protocol. New projects must use Streamable HTTP — the old one will be removed sometime in 2026.

Architecture: the client POSTs JSON-RPC requests to a single MCP endpoint; the server pushes notifications/* events back over a GET-stream (SSE) on the same endpoint. One endpoint carries both requests and events. The Mcp-Session-Id header maintains session state across requests.

JSON-RPC framing: client → server is a single JSON body in a POST request; server → client SSE events look like data: {json}\n\n. Every frame is a complete JSON object — no NDJSON, no line-separator ambiguity.

Auth: OAuth 2.1 is required (MCP authorization spec). The client first POSTs to /oauth/authorize → redirects to the IdP → user grants → receives access_token → subsequent requests carry Authorization: Bearer <token>. Auth moves from the OS boundary to a token — the server is on a public network and must explicitly authorize every call.

Session state: Mcp-Session-Id is a UUID the server assigns; the client must include this header on every subsequent request. The server uses it to maintain "this client's tools/call context" (OAuth scope, connection pool index, subagent state). When multiple clients share a server, session isolation is the server's explicit responsibility — you can't just slice by IP.

Debugging advantage: cross-network, multiple Hosts can call the same server, team-shareable. Debugging pain: you need a proxy between server and client (mcp-proxy) to see the protocol flow; SSE event-stream breakpoints are hard to localise; OAuth failures often present as "401 with no body."

Five-dimension comparison: when to use which

| Dimension | stdio | Streamable HTTP | |---|---|---| | Trust model | OS process boundary = trust boundary | OAuth 2.1 + Bearer Token | | Auth | None (Host user's full permissions) | Required (RFC 8707 Resource Indicators forces audience binding) | | Deployment | Must share Host's machine | Any machine, HTTPS suffices | | Cross-Host reuse | Impossible (each Host forks its own child) | Possible (multiple Hosts share one server) | | Session state | In-process variables | Mcp-Session-Id header across requests | | Debugging tools | Inspector / socat / gdb | mcp-proxy / DevTools / OAuth debug page | | Start-up latency | Process fork + JSON-RPC handshake (~100-300ms cold start) | TLS handshake + OAuth refresh (if any) + JSON-RPC (50-200ms) | | Concurrency | Bounded by subprocess model; each Host forks its own | One server, many clients; uses session pool | | Network attack surface | Zero (stays on this machine) | HTTPS / DNS rebinding / Origin checking |

5-scenario production decision table:

  1. Personal local tool / CLI wrapper: stdio. Example: wrap ffmpeg as a Claude-callable tool.
  2. Team-internal tool (≤10 people): stdio + shared start script (each dev runs the server on their own machine). Simple, secure, zero ops.
  3. SaaS tool / public catalog: Streamable HTTP + MCP Registry (modelcontextprotocol/registry). The most common deployment shape in 2026.
  4. Multi-Host reuse of the same set of tools: only Streamable HTTP makes sense here — under stdio each Host forks its own copy and configurations can't be shared.
  5. Server itself needs to call external APIs (GitHub / Slack / databases): stdio or HTTP both fine; the key is that the server uses its SDK to talk to external services (official MCP SDKs).

Anti-pattern: treating stdio as "good enough" and trying to deploy it remotely — you've only swapped a "local process problem" for a "server deadlocked on reconnect" problem. As soon as you need team sharing, cross-machine hosting, or multi-Host reuse, upgrade to Streamable HTTP.

The three-step handshake: what happens when the client starts

Regardless of transport, the client follows the MCP lifecycle spec through a three-step handshake — but each step behaves differently per transport:

Step 1: initialize (request/response)

  • stdio: client writes to stdin, server reads, server writes JSON response to stdout
  • HTTP: client POSTs /mcp with the initialize JSON-RPC body, server returns 200 OK + response

Two key fields in the server's response:

  • protocolVersion: e.g. "2025-11-25". A mismatch here is the most common handshake failure of 2026 — client upgraded to 11-25, server stuck on 06-18 (or vice versa) — and you get UnsupportedProtocolVersionError.
  • capabilities: the server declares which primitives it supports (tools / resources / prompts). If capabilities doesn't list a primitive, the client won't send requests for it — if the server didn't declare tools, the client won't send tools/call.

Step 2: notifications/initialized (client → server notification)

The client sends a JSON-RPC notification without an id field to tell the server "I'm ready." The server must not reply — this is one-way. Under HTTP, the server still returns 202 Accepted (HTTP semantics), but the body is empty.

Common pitfall: missing the notifications/initialized notification is the most common cause of OAuth scope failures — the server initialises OAuth context at this step; if it's missing, subsequent tools/call will 401 because the server can't find the scope.

Step 3: enter the tools/list / tools/call main loop

  • stdio: the client keeps writing requests to stdin; the server keeps writing responses to stdout. The child process lives until the Host exits.
  • HTTP: the client keeps POSTing requests; if the server needs to push notifications/resources/updated, the client first GETs /mcp to open an SSE stream, the server pushes events down that stream. Two independent channels.

Typical 401 debug flow:

  1. Capture the client's initialize response — does protocolVersion and capabilities look right?
  2. Capture the client's notifications/initialized notification — was it actually sent?
  3. Capture the first tools/list request and response — does the server's scope cover the resource that tool needs?

Custom transports: when to write your own

The MCP spec doesn't restrict you to stdio and Streamable HTTP — anything that can carry newline-delimited JSON-RPC frames (stdio) or HTTP POST + SSE (HTTP) is valid. Three common custom scenarios:

Unix domain socket: cross-process communication on the same machine, without exposing a port. More secure than a TCP port (machine-local only), more flexible than stdio (multiple servers can share socket paths). The MCP Python SDK's in-memory transport is a reference implementation.

WebSocket: an alternative to HTTP+SSE — single connection, bidirectional, long-connection friendly. The MCP spec does not currently standardise a WebSocket transport, so verify server and client use the same custom implementation before writing one.

gRPC / MessagePack: performance scenarios — JSON-RPC frames serialised to binary, single message size compressed 30–50%. But you lose debuggability (no more cat to read messages) and the ecosystem toolchain doesn't support it.

Decision rule: stdio or Streamable HTTP covers 90% of scenarios. Only consider a custom transport when (1) existing transports don't meet performance needs and you're willing to invest engineering for that 10%, (2) you have ready transport code (e.g. a Unix socket library), (3) the team accepts the debuggability loss.

Common Pitfalls and Anti-Patterns: Transport Incidents in the Field

Pitfall 1: stdout polluted by logs on a stdio server. Symptom: intermittent "malformed message" errors. Cause: stdout is the JSON-RPC protocol channel — every byte from print / console.log corrupts frame boundaries. Fix: route all diagnostics to stderr, and make it a lint rule in the server repo.

Pitfall 2: a reverse proxy buffers the streamed response. Symptom: everything works on a direct connection, but behind nginx / a cloud load balancer messages arrive in bursts or time out. Cause: the proxy buffers event-stream responses. Fix: disable proxy buffering (nginx's X-Accel-Buffering: no) and make sure read timeouts cover long-lived connections.

Pitfall 3: a Streamable HTTP deployment without Origin validation. Symptom: a security scan finds that a malicious browser page can drive your server (the DNS-rebinding attack surface). Cause: the local or internal HTTP server never checks the Origin header. Fix: validate Origin as the spec requires, rejecting browser-originated requests outside your allowlist.

Pitfall 4: mistaking "no sessions" for "no state". Symptom: after upgrading to the 2026-07-28 spec, every multi-step flow that relied on Mcp-Session-Id breaks. Cause: the new version removed protocol-level sessions (SEP-2567), but your business state did not vanish — it simply stopped being hosted by the transport. Fix: carry cross-call state in explicit, server-minted handles passed as ordinary tool arguments.

Pitfall 5: leftover initialize-handshake code. Symptom: new clients can't connect to an old server, or both sides throw protocol-version errors after upgrading. Cause: 2026-07-28 removed the initialize / notifications/initialized handshake; the protocol version and capabilities now travel in each request's _meta (SEP-2575), and mismatches return UnsupportedProtocolVersionError. Fix: upgrade the SDK, implement the server/discover probe endpoint, and delete the hand-written handshake.

What the 2026-07-28 Spec Changed for Transports

The specification published on July 28, 2026 is the biggest transport rework in a year. The official changelog lists nine major changes; six land directly on the transport layer:

| Change | Impact | Source | |---|---|---| | Protocol-level sessions and Mcp-Session-Id removed | list endpoints no longer vary per connection; cross-call state moves to explicit handles | SEP-2567 | | initialize handshake removed; MCP goes stateless | every request carries protocol version and capabilities in _meta; new server/discover probe RPC | SEP-2575 | | SSE resumability removed (Last-Event-ID and event IDs) | a broken response stream loses the in-flight request; clients must re-issue with a new ID | SEP-2575 | | subscriptions/listen replaces the GET endpoint and resources/subscribe | one long-lived POST stream carries server-to-client notifications, opted in by type | SEP-2575 | | ping / logging/setLevel removed | log level is set per request via the logLevel _meta key | SEP-2575 | | the old HTTP+SSE transport formally Deprecated | minimum 12-month deprecation window; new implementations must not adopt it | SEP-2596 |

Three actions for server authors:

  1. If you still run HTTP+SSE, schedule the migration now — it is in the official deprecated-features registry, and the migration path is exactly the Streamable HTTP transport described in this guide.
  2. Statelessness is a dividend, not a burden: with sessions gone, a Streamable HTTP server scales horizontally and fits container orchestration naturally — a Kubernetes restart no longer tears sessions apart, and the deployment model aligns with any ordinary REST service.
  3. List endpoints now return ttlMs and cacheScope (the CacheableResult interface, SEP-2549): clients cache tools/list responses to reduce polling, and the spec also says list results SHOULD be deterministically ordered to improve client prompt-cache hit rates — your tools/list implementation needs to keep up.

In the same announcement, the Roots, Sampling, and Logging features entered Deprecated (transport-adjacent, but it changes server design): directories and files move to tool parameters or resource URIs, and logs go to stderr (stdio) or OpenTelemetry.

Frequently asked questions

Should I pick stdio or Streamable HTTP?

Three checks: (1) Does the server have to live on the Host's machine? Yes → stdio; no → HTTP. (2) Will multiple Hosts call the same server? Yes → HTTP (under stdio each Host forks its own copy, configurations can't be shared). (3) Can you absorb OAuth ops cost? No → stdio; yes → HTTP for team-sharing. One self-check: if your server only runs on one machine serving one Host, stdio is the most stable choice.

How do I move a stdio server to production?

You can't ship it directly. stdio means the server lives on the Host's machine; for team sharing you need Streamable HTTP. Migration steps: (1) split the server into "business logic + transport adapter" layers; the business logic is reusable; (2) pick an official SDK and re-wrap with Streamable HTTP transport; (3) deploy to a remote machine or cloud; (4) configure OAuth 2.1 + Bearer Token (per authorization spec); (5) walk through security best practices — Origin checking + DNS rebinding defence.

Can I still use the old HTTP+SSE transport (2024-11-05 spec)?

You can, but new projects shouldn't. The 2025-11-25 spec's Streamable HTTP replaced it — the old one required the client to accept text/event-stream one-way streams plus a separate POST endpoint; the new one unifies on a single endpoint, bidirectional, SSE optional. Upgrade path: on the server side, switch from the old transport to StreamableHttpServerTransport (Python SDK) or the equivalent @modelcontextprotocol/server TS implementation; on the client side, just upgrade the SDK to the 2025-11-25 release and it'll adapt automatically.

How do I test a server that supports both transports?

Write the server with business logic decoupled from transport adapters, and run unit tests with the in-memory transport (Python) or MemoryTransport (TypeScript) — spin up two client instances, one on stdio and one on HTTP, and verify they behave the same. For integration tests, use MCP Inspector to exercise the stdio path, and curl to simulate POST requests on the HTTP path.

Does Mcp-Session-Id still work after the 2026-07-28 spec?

No — protocol-level sessions and the Mcp-Session-Id header have been removed from the Streamable HTTP transport (SEP-2567). When you need state across calls, the server mints explicit handles that travel as ordinary tool arguments. The change makes Streamable HTTP servers naturally stateless, so horizontal scaling no longer fights session stickiness.

Can multiple clients connect to one stdio server?

No — stdio's model is one client, one subprocess: the host spawns the server as a child process, and stdin/stdout are the protocol channel of that single connection. Multiple clients each spawn their own instance, sharing nothing; to be shared, deploy as a Streamable HTTP network service instead.

Transport selection cheat sheet

| Scenario | Recommended | Why | |---|---|---| | Personal local tool | stdio | Zero network, zero auth, zero ops | | Team internal tool (≤10 people) | stdio + start script | Simple and safe, no OAuth | | Cross-team shared server | Streamable HTTP | One server, multiple Hosts reuse | | SaaS MCP server | Streamable HTTP + MCP Registry | Public discovery + auth | | Containerised deployment (Docker / k8s) | Streamable HTTP | Containers usually run cross-Host | | Debugging a server | stdio + Inspector | Simplest debug path |

One-line summary: stdio is simplest; HTTP reaches farthest. Pick stdio when you don't want OAuth ops; pick HTTP when you must serve multiple Hosts or cross networks.

Official references

This article is current as of the MCP 2025-11-25 specification (August 2026). The Streamable HTTP transport was introduced in 2025-11 and replaces the older HTTP+SSE; if you're using the 2024-11-05 spec's old transport, please migrate soon.