Class 5: Transports
Duration: ~30 minutes | Level: Intermediate | Prerequisites: Class 4: The Protocol Layer
What Is a Transport?
The MCP protocol defines what messages look like. The transport defines how those messages physically move from client to server and back.
The current MCP spec standardises two transports: stdio (for local subprocess servers) and Streamable HTTP (for remote servers). Choosing the wrong one does not break MCP, but it makes your architecture awkward.
An older transport, HTTP+SSE, was defined in revision 2024-11-05 and replaced by Streamable HTTP in revision 2025-03-26. It still runs in legacy deployments.
Transport 1: stdio
stdio (standard input/output) is the simplest transport. The client launches the MCP server as a child process, a second program that the client starts and owns, then writes to the server's stdin and reads from its stdout.
How it works
- The host application (Claude Desktop, Cursor) reads its config file, which lists MCP servers and how to launch them
- When the user connects, the host spawns the server process:
npx my-mcp-server,uvx my-mcp-server, or any executable on the user's machine.npxruns a package from the Node.js registry,uvxone from the Python registry, downloading it on first use - Because stdout is the message channel, the server must write only valid MCP messages to it. Logging and debug output belongs on stderr, the process's third stream. A stray print to stdout is the most common way to break a stdio server
- The client shuts the server down by closing its stdin. A server SHOULD exit as soon as a read on stdin returns end-of-file, the one shutdown signal that works on every operating system. If it does not, the client forces it:
SIGTERMthenSIGKILLon POSIX,TerminateProcessor Job Objects on Windows
Example: Claude Desktop config
{
"mcpServers": {
"my-database-server": {
"command": "npx",
"args": ["-y", "my-database-mcp-server"],
"env": {
"DB_URL": "postgres://localhost:5432/mydb",
"DB_PASSWORD": "secret"
}
}
}
}
That file is plain text on disk: ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows. It holds the password in the clear, and the host runs command as the signed-in user, so the server can read and write everything that user can. The specification names this attack class local MCP server compromise, and Class 7 works through it.
When to use stdio
- Local development and personal tools: the simplest path from code to a working server, and the only transport that reaches the user's filesystem, local database or local APIs
- Sensitive credentials: the credential travels in an environment variable and does not cross a network, though it does sit in a file on the user's machine
Stdio limitations
- Single user, local only: the server is a child process of one user's application, and has to be installed on that user's machine
- Process lifecycle: tied to the host; if the host crashes, the server crashes too
- No horizontal scaling: one process serves one user, so you cannot start more copies behind a load balancer, the component that hands each request to one of several identical instances
Transport 2: Streamable HTTP
The Streamable HTTP transport enables remote MCP servers, long-lived processes that multiple clients can connect to over a network. It became the standard remote transport in revision 2025-03-26, replacing the older HTTP+SSE design.
The server exposes a single HTTP endpoint, commonly /mcp. Responses that carry more than one message use SSE (Server-Sent Events), a standard HTTP mechanism in which the server keeps the response open and streams a sequence of events.
The current shape (spec revision 2026-07-28)
The endpoint MUST support POST, and only POST:
- Every JSON-RPC message the client sends is its own
POST. The client MUST send anAcceptheader listing bothapplication/jsonandtext/event-stream, because the server decides per request whether to answer with a single JSON object or with an SSE stream scoped to that request. - Two headers are REQUIRED on every POST:
MCP-Protocol-Version, andMcp-Method, which mirrors the JSON-RPCmethod. A third,Mcp-Name, travels ontools/call,resources/readandprompts/getonly, mirroringparams.nameorparams.uri. They let load balancers, gateways and observability tooling route and inspect a request without parsing its body. A server MUST answer400 Bad Requestwith error code-32020(HeaderMismatch) when a header disagrees with the body, or when one required for that method is missing. - Sessions are gone, and with them the
Mcp-Session-Idheader. A server on this revision ignores that header from an older client. - A server that speaks only this revision SHOULD answer a
GETor aDELETEon the endpoint with405 Method Not Allowed. - Long-lived change notifications come from a
subscriptions/listenrequest, whose response stream stays open and carries only the notification types the client opted in to. - Every JSON-RPC request comes from the client. When a server needs sampling, elicitation or roots, it returns a result with
resultType: "input_required", and the client retries the original request carrying the answers. That is the Multi Round-Trip Requests pattern.
Each header repeats a value from the body:
POST /mcp HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "get_weather", "arguments": {"city": "Ljubljana"}}}
If Mcp-Name said get_forecast while the body said get_weather, the answer is 400 Bad Request:
{"jsonrpc": "2.0", "id": 1,
"error": {"code": -32020,
"message": "Header mismatch: Mcp-Name 'get_forecast' does not match params.name 'get_weather'"}}
Multi Round-Trip Requests turn one logical call into two POST requests.
The server does not keep any state between the two requests: everything it needs on the retry travels back in requestState, an opaque value the client returns untouched.
Through revision 2025-11-25, the revision the Java MCP SDK 2.0.0 implements, the same endpoint answered both POST and GET.
| What the client relies on | 2025-11-25 | 2026-07-28 |
|---|---|---|
| Methods on the MCP endpoint | POST and GET | POST only |
| Long-lived server push | a standalone GET SSE stream | the response stream of a subscriptions/listen request |
| Session | optional Mcp-Session-Id, issued on initialize, ended with HTTP DELETE | none |
| Server asking for sampling, elicitation or roots | a JSON-RPC request sent on the SSE stream | an input_required result the client answers by retrying |
| Interrupted stream | resume with Last-Event-ID | re-issue as a new request with a new id |
| Required headers | MCP-Protocol-Version | MCP-Protocol-Version, Mcp-Method, plus Mcp-Name on three methods |
The GET stream and the session both store state on the connection, which is why the newer revision removed them. A load balancer does not know which instance holds a client's GET stream or session, so either mechanism needs sticky routing: every request from that client pinned to the instance that served its first one. When every message is a self-contained POST, any instance can answer any request, so the server scales horizontally and runs on serverless platforms, where a fresh copy of the code may handle each request.
Three shapes of answer come back from that one endpoint.
When to use Streamable HTTP
- Multi-user servers: deployed centrally for a team, scaled behind a load balancer, and reaching data that does not live on the user's machine, such as a SaaS API (GitHub, Jira, Salesforce)
- Authenticated access: standard HTTP authentication, either a bearer token (a credential the client puts in the
Authorizationheader) or OAuth
Streamable HTTP considerations
- Authentication required: without auth, anyone who can reach the server can call your tools. Class 7 works through the trust boundaries, the OAuth-based authorization spec that Streamable HTTP carries, and the rules on tokens.
- Origin and binding: servers MUST validate the
Originheader to prevent DNS rebinding attacks, and MUST answer403 Forbiddento an invalid one. A locally bound server SHOULD listen on127.0.0.1rather than0.0.0.0. - No resumability (as of 2026-07-28): through revision 2025-11-25 a client could resume an interrupted SSE stream with the
Last-Event-IDheader, naming the last event it had received. That mechanism is gone, along with SSE event IDs and message redelivery. A broken response stream loses the request running on it, and the client MUST re-issue it as a new request with a newid. Closing the stream is itself the cancellation signal, so the server SHOULD stop work on the dropped request, and anything the client may retry should be safe to run twice.
The Remote MCP Servers course covers the rest: SSE through proxies and CDNs, the handles a server mints for work that cannot be repeated safely, and scaling.
Legacy: HTTP+SSE Transport
The HTTP+SSE transport defined in revision 2024-11-05 used a two-channel design:
- HTTP POST for client → server requests (sent to a per-session endpoint URL)
- Server-Sent Events for server → client messages over a separate, long-lived GET request
It was deprecated in revision 2025-03-26 in favour of Streamable HTTP, because serverless infrastructure and HTTP middleware handle that pair of channels badly. The POST and the stream that answers it have to land on the same instance, which a load balancer cannot arrange. Many older clients and servers still implement it, so newer clients often speak both.
Choosing a Transport: Decision Guide
| stdio | Streamable HTTP | |
|---|---|---|
| Who starts the process | the host, as a child process | you, as a long-lived service |
| Who can reach it | only the user whose host launched it | anyone who can reach the URL |
| Where credentials live | environment variables in the host config file on that machine | an HTTP Authorization header, checked by the server |
| How it scales | one process per user, without a load balancer | any instance answers any POST |
| What ends it | the client closes stdin and the server exits | each response stream closes on its own |
| What breaks it most easily | anything written to stdout that is not an MCP message | a missing or mismatched header, answered with 400 and -32020 |
Three questions pick the transport, asked in this order.
The first question decides most cases: a server that reads files or a database on the user's own machine has to run there.
Key Takeaways
- stdio: client launches server as a child process; simple, local, single-user
- Streamable HTTP: current remote transport; single endpoint, SSE only when needed
- HTTP+SSE (legacy): a GET stream plus POSTs to a session URL; deprecated since 2025-03-26 but still in older deployments
- The messages mean the same thing on both transports. Each binding decides how they are framed, how request metadata travels (
_metaon every request, mirrored into HTTP headers on Streamable HTTP), and how a client cancels (the response stream closing on Streamable HTTP,notifications/cancelledon stdio)
In the next class, we look at capability negotiation: what each side declares it supports, how that travels on every request in the current revision, and the initialize handshake that carried it through 2025-11-25.
Further Reading
- Streamable HTTP: the normative rules behind this class: endpoint, headers, errors and streams.
- stdio: the stdio binding in full, including the stdout rule and the shutdown sequence.
- Key Changes (2026-07-28): what this revision removed, and what replaced each piece.
- Multi Round-Trip Requests: the result shape and the
requestStaterules for the two-step call. - Transports (2025-11-25): the shape with sessions and a
GETstream, which the Java MCP SDK2.0.0speaks. - Transports (2024-11-05): the original HTTP+SSE transport, for anyone maintaining a server that hosts it.
- Security Best Practices: the attack classes that follow from each transport choice.
- Replace HTTP+SSE with a new Streamable HTTP transport (PR 206): the proposal that retired HTTP+SSE, and its reasoning.
Sources
- Streamable HTTP: the
POST-only endpoint, theAcceptand mirrored headers,-32020, the405answer,subscriptions/listen,X-Accel-Buffering, andOriginvalidation with403. - stdio: the stdout rule, stderr for logging, and the shutdown escalation from closing stdin to
SIGTERM,SIGKILL,TerminateProcessor Job Objects. - Key Changes (2026-07-28): re-issuing a lost request with a new
id, the server-minted handles that replaced sessions, and this revision's authorization changes. - Multi Round-Trip Requests: the
input_requiredresult,inputRequests,inputResponses, and the newidon the retry. - Overview: Transports: each binding owns framing, request metadata and cancellation.
- Cancellation: closing the response stream cancels on Streamable HTTP; stdio uses
notifications/cancelled. - Key Changes (2025-03-26): Streamable HTTP replaced HTTP+SSE in this revision.
- Transports (2025-11-25): the session header, the
GETstream, server-initiated requests, andLast-Event-ID. - Connect to local MCP servers: the config file shape and paths, what
-ydoes, and the user-permissions warning. - Release v2.0.0, modelcontextprotocol/java-sdk: the SDK
2.0.0tracks the2025-11-25specification. - Deprecated Features: HTTP+SSE is Deprecated and eligible for removal in a future revision.