MCP Transports: stdio and Streamable HTTP

The same JSON-RPC messages can travel down a subprocess pipe or across the network, and choosing the right transport is mostly a question of where your server lives and who it serves.

The previous post looked at what the Model Context Protocol (MCP) sends: JSON-RPC requests, responses, and notifications. This post is about how those bytes get from client to server and back. MCP deliberately separates the message layer from the transport layer, which means the exact same messages work whether the server is a script on your laptop or a service in another data center. There are two standard transports — stdio and streamable HTTP — and knowing when to reach for each is most of the battle.

Why transport is a separate concern

Keeping transport independent from the message format is a small design decision with large payoffs. Server and client code that builds and handles JSON-RPC messages does not care how they travel; you can move a server from local to remote by changing one line, not rewriting its logic. It also means the protocol can gain new transports over time without touching the semantics of tools, resources, and prompts. So think of transport as the envelope, not the letter.

stdio: the local subprocess transport

The stdio transport is the workhorse for local, single-user servers. The model is simple and robust: the client launches the server as a child process and talks to it over the process’s standard streams.

Because the server is a subprocess of the client, its lifecycle is tied to the session — the client starts it on connect and lets it exit on close. There is no network, no port, no listening socket. Trust is inherited from the local user: the server runs with your permissions on your machine.

With the official Python SDK, choosing stdio is a single argument when you run the server:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes")

# ... register tools/resources/prompts ...

if __name__ == "__main__":
    mcp.run(transport="stdio")

stdio is ideal for developer tools, editor integrations, and anything that wraps local resources like the filesystem or a local database. It is fast, has no auth ceremony, and is trivial to debug.

Streamable HTTP: the remote transport

When a server needs to be reachable over a network — hosted centrally, shared by many users, or run in a different environment from the client — MCP uses the streamable HTTP transport.

The shape is: the client sends JSON-RPC messages to the server as HTTP POST requests to a single endpoint. For a plain request/response, the server answers in the HTTP response body. But some interactions are not a simple round trip — the server may need to stream multiple messages back (progress notifications during a long tool call, or server-initiated requests). For those, the server can respond with a stream, using Server-Sent Events (SSE) to push a sequence of messages to the client over one connection.

That single-endpoint, optionally-streaming design is why it is called streamable HTTP. It supersedes an earlier “HTTP+SSE” transport that used two separate endpoints; if you read older material referring to that split design, the streamable HTTP transport is its modern replacement, and new servers should prefer it.

Running an HTTP server with the Python SDK is, again, a transport choice:

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

Remote servers bring real infrastructure concerns that local ones do not: authentication, authorization, and origin validation. We treat those properly in the security post; for now, the key point is that the moment a server listens on the network, “trust the local user” stops being enough.

Choosing a transport

The decision is usually straightforward:

A useful tie-breaker: if the server would need its own deployment, scaling, and authentication story anyway, it belongs on HTTP. If it can ride along as a subprocess of whatever launched it, stdio keeps things simple.

Security implications, previewed

Transport choice changes your threat model. A stdio server runs as a local subprocess with the user’s privileges — the risks are the risks of running any local program, and isolation means sandboxing that process. An HTTP server is exposed to the network, so it needs authenticated clients, authorization on every call, and validation of request origins to avoid being driven by pages or hosts you did not intend. These are not optional niceties for remote servers; they are the price of network reach. The final post in this series covers the concrete mechanisms. For now, internalize the rule: stdio inherits local trust; HTTP must establish trust explicitly.

Key takeaways

Further reading

Sources & References

Transport specification