Skip to main content

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

  1. The host application (Claude Desktop, Cursor) reads its config file, which lists MCP servers and how to launch them
  2. 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. npx runs a package from the Node.js registry, uvx one from the Python registry, downloading it on first use
  3. 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
  4. 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: SIGTERM then SIGKILL on POSIX, TerminateProcess or 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 an Accept header listing both application/json and text/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, and Mcp-Method, which mirrors the JSON-RPC method. A third, Mcp-Name, travels on tools/call, resources/read and prompts/get only, mirroring params.name or params.uri. They let load balancers, gateways and observability tooling route and inspect a request without parsing its body. A server MUST answer 400 Bad Request with 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-Id header. A server on this revision ignores that header from an older client.
  • A server that speaks only this revision SHOULD answer a GET or a DELETE on the endpoint with 405 Method Not Allowed.
  • Long-lived change notifications come from a subscriptions/listen request, 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.

What 2025-11-25 did instead, and why it changed

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 on2025-11-252026-07-28
Methods on the MCP endpointPOST and GETPOST only
Long-lived server pusha standalone GET SSE streamthe response stream of a subscriptions/listen request
Sessionoptional Mcp-Session-Id, issued on initialize, ended with HTTP DELETEnone
Server asking for sampling, elicitation or rootsa JSON-RPC request sent on the SSE streaman input_required result the client answers by retrying
Interrupted streamresume with Last-Event-IDre-issue as a new request with a new id
Required headersMCP-Protocol-VersionMCP-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 Authorization header) 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 Origin header to prevent , and MUST answer 403 Forbidden to an invalid one. A locally bound server SHOULD listen on 127.0.0.1 rather than 0.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-ID header, 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 new id. 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

stdioStreamable HTTP
Who starts the processthe host, as a child processyou, as a long-lived service
Who can reach itonly the user whose host launched itanyone who can reach the URL
Where credentials liveenvironment variables in the host config file on that machinean HTTP Authorization header, checked by the server
How it scalesone process per user, without a load balancerany instance answers any POST
What ends itthe client closes stdin and the server exitseach response stream closes on its own
What breaks it most easilyanything written to stdout that is not an MCP messagea 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 (_meta on every request, mirrored into HTTP headers on Streamable HTTP), and how a client cancels (the response stream closing on Streamable HTTP, notifications/cancelled on 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

Sources

  • Streamable HTTP: the POST-only endpoint, the Accept and mirrored headers, -32020, the 405 answer, subscriptions/listen, X-Accel-Buffering, and Origin validation with 403.
  • stdio: the stdout rule, stderr for logging, and the shutdown escalation from closing stdin to SIGTERM, SIGKILL, TerminateProcess or 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_required result, inputRequests, inputResponses, and the new id on 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 GET stream, server-initiated requests, and Last-Event-ID.
  • Connect to local MCP servers: the config file shape and paths, what -y does, and the user-permissions warning.
  • Release v2.0.0, modelcontextprotocol/java-sdk: the SDK 2.0.0 tracks the 2025-11-25 specification.
  • Deprecated Features: HTTP+SSE is Deprecated and eligible for removal in a future revision.