Module 5: Transports
Duration: ~20 minutes | Level: Intermediate | Prerequisites: Module 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). They serve different deployment scenarios and have different operational characteristics. Choosing the wrong one doesn't break MCP, but it makes your architecture awkward.
An older transport, HTTP+SSE, was defined in protocol revision 2024-11-05 and was replaced by Streamable HTTP in revision 2025-03-26. It is still encountered in legacy deployments, so we cover it briefly as well.
Transport 1: stdio
stdio (standard input/output) is the simplest transport. The client launches the MCP server as a child process, then communicates by writing to the server's stdin and reading from its stdout.
Host Application
└─ MCP Client
├─ writes JSON to server's stdin ──→ ┐
└─ reads JSON from server's stdout ←── MCP Server Process
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 (e.g.
npx my-mcp-server,uvx my-mcp-server, or any executable on the user's machine) - From that point, all communication is newline-delimited JSON over stdin/stdout. Because stdout is the message channel, the server must never write anything to stdout that is not a valid MCP message; all logging and debug output belongs on stderr, which the client may capture, forward, or ignore. A stray print statement to stdout is the most common way to break a stdio server
- The server terminates when the host kills the process (on disconnect or shutdown)
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"
}
}
}
}
When to use stdio
- Local development: the simplest path from code to working server
- Personal tools: servers that only one person uses
- Desktop applications: filesystem access, local database, local APIs
- Sensitive credentials: credentials pass through environment variables, never over a network
Stdio limitations
- Single user only: the server is a child process of the user's application; it cannot serve other users
- Local only: the server must be installed on the user's machine
- Process lifecycle: tied to the host; if the host crashes, the server crashes too
- No horizontal scaling: you can't run multiple instances behind a load balancer
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 MCP spec revision 2025-03-26, replacing the older HTTP+SSE design (covered as legacy below).
The server exposes a single HTTP endpoint (commonly /mcp) that supports both POST and GET. 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, so it can deliver several MCP messages on a single HTTP response:
The current shape (spec revision 2026-07-28)
The endpoint MUST support POST, and that is the only method it has to handle:
- 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. - Three headers are REQUIRED on every POST:
MCP-Protocol-Version,Mcp-Method(mirroring the JSON-RPCmethod), andMcp-Name(mirroringparams.nameorparams.uriontools/call,resources/readandprompts/get). They exist so that load balancers, gateways and observability tooling can route and inspect a request without parsing its body. A server MUST reject a request whose headers do not match the body, or which is missing a required header, with400 Bad Requestand error code-32020(HeaderMismatch). - There are no sessions. The
Mcp-Session-Idheader is gone; a server on this revision ignores one sent by an older client rather than minting or echoing it. - There is no standalone
GETstream. A server that speaks only this revision SHOULD answer aGETorDELETEon the MCP endpoint with405 Method Not Allowed. - Long-lived change notifications come from a
subscriptions/listenrequest, whose own response stream stays open and carries only the notification types the client opted in to. - Servers never send JSON-RPC requests. 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 (SEP-2322).
Through revision 2025-11-25, the revision the Java MCP SDK 2.0.0 implements, the same endpoint answered both POST and GET. GET opened a long-lived SSE stream on which the server could push notifications and also its own JSON-RPC requests. Sessions were optional: the server could issue an Mcp-Session-Id header on the initialize response, which the client then echoed on every subsequent request and could terminate with an HTTP DELETE.
Both mechanisms are connection-scoped state, and that is what killed them. A load balancer cannot see which instance owns a client's GET stream or session, so anything depending on either has to be pinned to one instance with sticky routing. Making every message a self-contained POST is what lets a Streamable HTTP server scale horizontally and run on serverless platforms. The two jobs the GET stream did were reassigned rather than dropped: change notifications moved to subscriptions/listen, and server-initiated requests became Multi Round-Trip Requests.
Client Server
│ │
├── POST /mcp (tools/call) ──────────→ │ (MCP-Protocol-Version,
│ │ Mcp-Method, Mcp-Name)
│ ←── 200 + JSON ───────────────────── │ (single response)
│ or │
│ ←── 200 + SSE stream ─────────────── │ (progress notifications,
│ │ then the response, then closes)
│ │
├── POST /mcp (subscriptions/listen) ─→ │
│ ←── SSE stream, stays open ───────── │ (only the notification types
│ │ the client opted in to)
When to use Streamable HTTP
- Multi-user servers: deployed centrally, serving an entire team or organisation
- Remote resources: data that isn't local to the user's machine
- Authenticated access: can use standard HTTP authentication (Bearer tokens, OAuth)
- Scalable infrastructure: can be horizontally scaled behind a load balancer
- SaaS integrations: connecting to external services (GitHub, Jira, Salesforce)
Streamable HTTP considerations
- Authentication required: without auth, anyone who can reach the server can call your tools. The MCP authorization spec (introduced in revision 2025-03-26 and refined in 2025-06-18 and 2025-11-25) builds on top of Streamable HTTP and standardises OAuth.
- Origin and binding: servers MUST validate the
Originheader to prevent DNS rebinding attacks, and when bound locally, SHOULD listen on127.0.0.1rather than0.0.0.0. - Infrastructure friendliness: serverless functions can serve it, CDNs and reverse proxies handle it without special configuration, and the wire shape is closer to a normal REST API.
- No resumability (as of 2026-07-28): through revision 2025-11-25 a client could resume an interrupted SSE stream via the
Last-Event-IDheader, so messages were not lost during a transient disconnect. That mechanism is gone, along with SSE event IDs and message redelivery. A broken response stream loses the in-flight request, and the client MUST re-issue it as a new request with a new requestid. Closing the stream is itself the cancellation signal, so the server SHOULD stop work on the dropped request, but any side effects it has already committed are the server author's problem on the retry.
The Remote MCP Servers course covers Streamable HTTP in implementation detail.
Legacy: HTTP+SSE Transport
The HTTP+SSE transport defined in protocol 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
Client Server
│ │
├── GET /sse ────────────────→ │ (opens SSE stream)
│ ←── event: endpoint ──────── │ (server sends POST endpoint URL)
│ │
├── POST /message ──────────→ │ (client sends requests)
│ ←── data: {...} ──────────── │ (server responds via SSE stream)
How it worked:
- Client opened a persistent GET request to the server's SSE endpoint
- Server sent an
endpointevent with a unique POST URL for the session - Client sent all requests as HTTP POST to that endpoint
- Server returned responses and notifications as SSE events on the original stream
This transport was deprecated in spec revision 2025-03-26 in favour of Streamable HTTP, primarily because the two-channel design was hard for serverless infrastructure and HTTP middleware to handle. Many older clients and servers still implement it for backwards compatibility, so newer clients often speak both.
Choosing a Transport: Decision Guide
Does the server need to run on the user's local machine?
├─ YES → stdio
└─ NO → Streamable HTTP
Does the server handle sensitive local resources (files, local DB)?
├─ YES → stdio (credentials stay local)
└─ NO → either
Will multiple users share the same server?
├─ YES → Streamable HTTP
└─ NO → either
Is this a development/personal tool?
├─ YES → stdio (simpler to set up)
└─ NO → Streamable HTTP (production-grade deployment)
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): the two-channel design covered in detail above; deprecated since 2025-03-26 but still in many older deployments
- stdio is the right default for local development; Streamable HTTP for team/production deployments
- The transport only affects how bytes move; the protocol (JSON-RPC, MCP messages) is identical
In the next module, 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.