For your first MCP server, it's genuinely hard to tell whether you should use stdio or HTTP. The docs said both transports were supported. The SDK exposed both. Tutorials online mostly used stdio without explaining why. Whenever you ask someone, you get a version of "it depends on what you're building" which is a sentence that has never once helped a working developer ship anything.
The full picture eventually lands on much sharper than "it depends." The transport choice is a deployment-model choice. Not a technical preference, not a flexibility hedge — a one-step decision that tells you, in advance, who runs the process, who authenticates the caller, who pays for the hosting, and what the operational surface looks like. Once you frame it that way, picking between the two takes about ninety seconds.
This post is that frame. Here we'll walk both transports as concretely as we can, then give you the general guidelines to pick between them.
What "transport" means in MCP
The MCP protocol — the JSON-RPC 2.0 message shapes, the lifecycle, the tools/resources/prompts primitives — is the same regardless of how the bytes get from host to server and back. The transport is the wire. The two production-quality transports today are:
- stdio — host spawns the server as a subprocess and talks to it over stdin/stdout. No network involved.
- Streamable HTTP — server runs as a normal HTTP service; host connects over the network. Replaces the older HTTP+SSE transport.
Underneath both, the messages look identical. A tools/call request on stdio is the same JSON object as a tools/call request over HTTP. The semantics don't change. What changes is who runs what, where, on whose hardware, with which security model wrapped around it.
If you've read the no-SDK post in this series, you've seen what stdio looks like at the byte level — line-delimited JSON on stdin and stdout. Streamable HTTP is the same JSON, framed differently and shipped over the network.
The stdio model: process spawned by the host
This is the most common MCP transport in the wild and the one most local dev tools should pick.
The mental model: your MCP server is a child process of the host. The host runs node /path/to/server.js (or python ..., or whatever your preffered language is). It connects to that process's stdin and stdout. JSON-RPC messages travel both ways over those pipes. Nothing else exists. No port. No authentication step. No session. The host writes a request to stdin; your server reads it, handles it, writes a response to stdout; the host reads that response. When the host exits, it closes the pipes, and your server exits too because its parent has gone away.
The deployment model is zero deployment. The user installs the MCP server like any other CLI tool — npm install -g, or a pre-built binary, or git clone && npm install. They drop a config entry in their host's config file pointing at the binary. That's it. The server runs only when the host runs it, lives only inside the host's process tree, and dies when the host quits.
Implications
The implications stack up in a clean direction:
Authentication is the OS user. The user already authenticated to their own machine. The spawned process inherits that trust. Your server can read files the user can read, talk to APIs with the user's keys (loaded from ~/.config/... or wherever), and there's no separate auth step to add. That way, the user can automate almost any task that could be performed from their PC.
No network exposure means a class of attacks doesn't exist. Nobody can scan and find your server. There's no port open. The only thing that can talk to it is the parent process that spawned it. Most server-shaped CVEs are network-shaped CVEs and stdio just walks around them.
No hosting cost. No VPS, no container, no uptime SLO. The server only runs locally, only when needed.
Distribution is the only operational concern. "How do I get this on the user's machine?" is a real question (npm registry, Homebrew, GitHub releases), but it's a packaging question, not a runtime one.
This is why every "developer-tool MCP server" you'll find — mcp-blog-publisher, the Flutter pipeline server we built, the various community filesystem/git/sqlite servers — uses stdio. The model fits the use case: one user, on one machine, using one host, talking to one server. Spinning up a network service for that would be like deploying nginx so two scripts on your laptop can pass a string between them.
The Streamable HTTP model: server runs as a normal service
The second transport flips almost every property of stdio.
The mental model: your MCP server is a regular HTTP service. It listens on a port. Clients connect over the network. Multiple clients can connect simultaneously, each with their own session. Authentication happens at the protocol layer because the caller could be anyone.
The deployment model is whatever you'd do for any other backend service. A container in a Kubernetes cluster, a VM, Cloud Run, Fly, Render. TLS terminated at a load balancer. A real domain name. Logs going to your observability stack. The same operational surface as your existing APIs.
The implications stack the other way:
Authentication is now your problem. The OS-user shortcut is gone. The caller is "whoever connected to the URL," which by default is "anyone on the internet." OAuth 2.1 with PKCE is the protocol's mandated answer for remote servers — there's a whole post in this series on what that actually means and a companion piece on PKCE for the underlying mechanism.
Hosting costs and uptime become real. If your server is going to be used by a hosted agent (Claude.ai web, an enterprise's central agent platform), it needs to be up. That means a deployment pipeline, monitoring, SLOs, on-call.
Sessions matter. Streamable HTTP supports per-session state in a way stdio's "one process per session" doesn't naturally need to think about. The session ID is in the headers. State management — who's allowed to call what, what's been agreed in this session, what the user is mid-flow on — is a session-aware concern.
Multi-tenancy enters. A remote server might serve multiple users at once. The narrow-funnel principle from the security post gets sharper here, because a single bad tool on a multi-tenant server is a much bigger blast radius than the same tool on a single-user stdio server.
This transport exists for a real set of use cases that stdio doesn't cover: a SaaS company exposing tools for any LLM agent to integrate with, an internal "agent platform" team running a fleet of MCP servers behind an authenticated gateway for the rest of the company, an open-source project hosting an MCP server for its docs that anyone can connect to. None of those work over stdio.
The picking rule
Here's the rule, and it's almost always correct on the first pass:
If exactly one user, on one machine, will be talking to this server, use stdio. Otherwise, you're looking at Streamable HTTP.
The "one user, one machine" condition is doing the work. Stdio's whole shape is built around it: spawned-by-host, OS-user-as-auth, no port, no session, no hosting. Break any of those properties — two users, or one user from two machines (work laptop and personal), or "this is a service my org provides" — and stdio starts fighting you. At that point, biting the operational tax of a real HTTP service is the correct trade.
The temptation to reach for HTTP because it feels more "real" is one I want to specifically warn against. Half the MCP servers that are overengineered are stdio servers someone reimplemented as HTTP services for no concrete reason. The result is more code, more ops, more surface area, more auth complexity, and the same set of three tools running on a $10/month VPS for a single developer who could have run them locally. If your use case fits stdio, use stdio. The minimalism is the feature.
Some real-world flags
A few signals that push teams toward HTTP that turned out to be wrong:
"We want to deploy it once and have everyone use it." Sometimes legitimate, sometimes a misread. If "everyone" means "a couple of devs," each running their own copy of the stdio server is genuinely simpler than spinning up a shared service. Distribution via npm install -g and a config snippet works fine. If "everyone" means "300 employees including non-engineers who can't be expected to install CLIs," that's a real HTTP case.
"We need a UI." The MCP server isn't where UIs live. The host is the UI. Whether your server is stdio or HTTP doesn't change what the user sees. If you find yourself reaching for HTTP because of a UI requirement, you might actually be reaching for a regular web app that happens to call an MCP server, which is fine — but the MCP server itself can stay stdio.
"We want to scale." Single-user stdio servers don't have a scale problem. They have one user. Each instance handles exactly that user's traffic. If you genuinely have multi-user load coming, that's an HTTP signal — but make sure it's load and not vibes.
"We're worried about secrets in the user's config." Real concern. If your stdio server needs an API key, the user has it on their disk. For some integrations that's not acceptable — the org doesn't want every employee holding a Stripe key — and HTTP with a server-side secret is the right answer. For a personal blog publisher? The user already has their own DB password somewhere; one more isn't a category change.
"What if we want to add a web client later?" This is a fair migration concern. The good news is the SDK lets you swap transports late. Same handler code, different Transport object. If you start stdio and need to add HTTP later, that's a few days of work plus some overhead with proper authentication. If you used a good architecture, a full rewrite is not necessary. Don't pre-pay the operational cost for a future you might not reach.
What changes when you go HTTP
For completeness, the things you actually have to do differently when shipping a Streamable HTTP MCP server vs. a stdio one:
- TLS. Either terminate at a load balancer or run TLS in the app. Plain HTTP is not acceptable for anything that carries auth tokens.
- OAuth 2.1 + PKCE for caller identity. The protocol mandates this for remote servers. Either run an authorization server yourself (
node-oidc-provider) or trust an external IdP (Auth0, Okta, Azure AD, Cognito, Keycloak). The IdP path is what most teams should pick — there's a walkthrough of why earlier in this series. - Per-session state. Streamable HTTP carries a session ID. Your server needs to know which session a request belongs to, what's allowed in this session, and how to clean up when the session ends.
- Audit logging with caller identity. From the security post — non-negotiable for stdio too, but the caller identity is "the OS user" there. For HTTP it's the OAuth subject, which means linking to your IdP(Identity Provider)'s user table.
- Rate limits. The default for stdio is "the user's hands." For HTTP, you need real rate limits, usually at the gateway or load balancer.
- Deployment. Container, image registry, infra-as-code, deploy pipeline, rollback strategy. Same surface as any backend service.
That list is not a roadblock — most teams already have most of those primitives in place for their existing services. But it's a real list, and the right time to absorb it is when the use case actually requires HTTP, not when you've decided HTTP is "more professional."
Where this fits
The order I'd suggest reading the rest of this series, if transport is your current concern:
- Overview for the worked stdio example.
- Securing your MCP server for the tool-design half of security, transport-agnostic.
- This post (transport choice).
- Can I make my session auth OAuth 2.1 compliant? — the auth path for remote servers.
- PKCE in plain English — the cryptographic primitive that ties OAuth 2.1 to public clients.
The fact that transport choice is upstream of auth is the most important piece of intuition to walk away with. Stdio means OS-user-trust and no auth code. HTTP means OAuth 2.1 + PKCE and a real auth surface. That's not two flavours of the same problem. That's two different deployment models, and the cost of being wrong about which one you're in shows up months later as either "why is this so heavy" or "why is this exposed to the internet without auth."