MCP in Practice

Lesson 7 of 36

Two transports and one decision

Two pipes, and they are not interchangeable

MCP messages travel one of two ways. stdio, where the host launches your server as a subprocess and they speak over standard input and output. Streamable HTTP, where your server is a web service and clients POST to it.

Every introduction presents these as two ways to do the same thing, choose whichever suits your deployment. That framing is wrong in a way that costs people real incidents. They are two trust models, and the choice determines what you must build. Pick stdio and you inherit the user's machine as your security boundary. Pick HTTP and you must construct one.

The house intercom and the public phone line

An intercom has no authentication, and nobody considers that a flaw. Being inside the house is the authentication — to press the button you had to get through the front door, which is where the security actually lives. A phone line has no such property. Anyone can dial it, so if only certain people should reach you, the phone itself must check.

That is the whole difference, and it lands the same way for both audiences. A beginner takes away that one of them needs a password and the other does not. An engineer takes away that stdio's security boundary is the operating system's process and file permissions, inherited for free, while HTTP has no inherited boundary at all — which is why every remote server needs authentication, origin validation and rate limiting, and why a local one usually needs none of them.

stdio inherits the operating system's boundary and serves one client; HTTP inherits nothing and serves many
stdio inherits the operating system's boundary and serves one client; HTTP inherits nothing and serves many

stdio: a subprocess on your machine

The host runs your server as a child process and writes JSON to its stdin, reading responses from its stdout.

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

That is the entire transport setup. What it gets you:

  • No network. Nothing is listening on a port. Nothing is reachable from another machine, or from a web page the user happens to be visiting.
  • The user's own permissions. The server can read the files the user can read. For a filesystem or database server that is exactly right, and arranging the same thing over HTTP would be a project.
  • One client. The host that launched it, for as long as the process lives.
  • No latency worth measuring. Pipes between local processes.

And one constraint that is not a detail: a stdio server must never write to stdout. Stdout is the protocol channel. A stray print() injects text into the JSON-RPC stream and the connection breaks, usually with an error that says nothing about printing. Use logging, which writes to stderr, which the host captures for you. Lesson 21 returns to this because it is the single most common way a working server stops working.

Streamable HTTP: one endpoint, many callers

The same server, served over HTTP:

python
app = mcp.streamable_http_app()
bash
uvicorn server:app --host 127.0.0.1 --port 8000

That gives you a Starlette application with the MCP endpoint mounted at /mcp. Clients POST JSON-RPC to it; when a response needs to stream, the server replies with Server-Sent Events over the same connection.

What it gets you: many clients at once, a server that can live anywhere, deployment and scaling as an ordinary web service, and — because the protocol is stateless — no sticky routing, so an ordinary load balancer in front of several instances just works.

What it costs you is everything the intercom got for free. There is no inherited boundary, so you need authentication (Lesson 31), origin and host validation (Lesson 30), and the ordinary web concerns of rate limiting and abuse. A local HTTP MCP server on default settings is reachable from any web page the user visits, which is a genuinely surprising sentence the first time you read it, and Lesson 30 is largely about it.

Choosing

The question is not "where will this run?" — it is "whose machine holds the thing this server touches?"

The transport follows from whose machine holds the thing the server touches
The transport follows from whose machine holds the thing the server touches
  • The user's own machine or credentials — their files, their local database, their git checkout, a CLI authenticated as them: stdio. You want their permissions to be the boundary, and you cannot get that over HTTP without rebuilding it.
  • A shared system with its own accounts — your product's API, a team database, a SaaS integration: HTTP. Many users, real authentication, one deployment you can update.

Atrium Works is instructive because it plausibly wants both, and will get both. Through Lesson 28 we run it over stdio, because the reader is the only user and a SQLite file on their laptop is the whole database. In Lesson 29 we serve the same code over HTTP, because a co-working floor with members has many users and a booking that only exists on one laptop is not a booking. The server code does not change — only how it is run, which is the point of separating transport from protocol.

server/discover is optional, and that changes your client

If you have read an older tutorial, it likely shows a connection opening with an initialize handshake that must complete before anything else. That is gone; server/discover replaced it and no client is obliged to call it, because a stateless protocol has nothing to establish. The reason is scaling: a handshake implies a session, and a session implies sticky routing.

Practically, for a server author: you cannot assume a client has called server/discover before it calls a tool. Every request carries the version and capabilities it needs, so handle each on its own terms rather than relying on a setup step that may never have happened.

For a client author, discovery is a convenience worth using anyway — one cacheable request that tells you what a server offers before you spend a turn finding out the hard way.

What to take into the next lesson

stdio and HTTP are two trust models, not two deployment options: stdio inherits the operating system's boundary and serves one local client, HTTP inherits nothing and serves many, so the choice follows from whose machine holds the thing you are touching. Next: enough theory — we connect to servers other people wrote, and get a real agent doing real work before writing a line of our own.

← Previous