This episode tears down the MCP transport layer: Streamable HTTP combining JSON-RPC request/response with an SSE stream for notifications, criteria for choosing it over stdio, and stdio for local processes — interop with editors and CLI agents, plus the process lifecycle from spawn to exit.

In episode 12 you handled long-running work with MCP Tasks. All those requests — tools/list, tools/call, tasks/update — have to reach the server through something. That something is what we dissect now: transport. You got a glimpse of stdio and SSE in episode 2; episode 13 digs into the details because the transport choice determines the architecture, operations, and security boundaries of your entire server.
This episode's roadmap: the transport map in modern MCP, how Streamable HTTP works (JSON-RPC request/response plus an SSE stream for notifications), criteria for choosing Streamable HTTP versus stdio, how stdio interoperates with editors and CLI agents, and the process lifecycle from spawn to exit.
MCP officially supports three transports, and two of them are still relevant in the stateless 2026-07-28 era:
Our focus is the two modern ones. The practical rule we'll keep using: stdio for local, single-user integration; Streamable HTTP for shared, multi-client production services.
In Streamable HTTP, a single endpoint receives all JSON-RPC messages as POST. A client calls tools/call with a normal payload:
curl -X POST https://mcp.example.com/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MCP_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"get_weather","arguments":{"city":"Bandung"}}}'What sets Streamable HTTP apart from an ordinary HTTP API is the second layer: an SSE stream. When a request produces notifications — for example tasks/update progress from episode 12 or MRTR status updates from episode 8 — the server opens a Server-Sent Events stream on the same response. The client reads the data stream, formatted as one event per line:
event: message
data: {"jsonrpc":"2.0","method":"tasks/update",
"params":{"taskId":"task_7f3a91c2","state":"running","progress":55}}
event: message
data: {"jsonrpc":"2.0","id":1,
"result":{"content":[{"type":"text","text":"Bandung 29C, berawan"}]}}In other words: you get classic JSON-RPC request/response for synchronous methods, and an SSE stream for notifications and streaming long results. Both share a single endpoint and a single stateless model — every request carries its own identity and capabilities, exactly as you learned in episode 3.
Streamable HTTP is the default choice for production. Some concrete reasons:
Authorization header and the OAuth 2.1 flow from episode 10 work naturally over HTTP.Its downsides are real too: you must manage security (origin allowlist, rate limiting), operations (health checks, observability), and still provide the SSE mechanism for notifications. For a single-machine, single-co-located-process scenario, all of this is overhead without benefit — and that's where stdio wins.
stdio places the server as a subprocess spawned by the host. JSON-RPC is sent as lines to the server's stdin, and responses/notifications are read from its stdout. No ports, no network connections, no authentication — security is determined entirely by file system permissions and host configuration.
This is the transport most used in daily integration:
Because it communicates over stdin/stdout, a stdio server has a strict contract: never print logs to stdout — you'll break the protocol. All logs must go to stderr or a file.
The host controls the server's lifetime. The typical flow:
initialize handshake and verifies the server's capabilities.notifications/exit or closes stdin to signal the end of the session.An example spawn from the host side in Node.js:
import { spawn } from "node:child_process";
const child = spawn("npx", ["-y", "mcp-server-git"], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env }
});
child.stdin.write(JSON.stringify({
jsonrpc: "2.0", id: 1, method: "initialize", params: {
protocolVersion: "2026-07-28",
capabilities: {},
clientInfo: { name: "my-host", version: "1.0.0" }
}
}) + "\n");
child.stderr.on("data", (chunk) => console.error(chunk.toString()));Because the server dies along with its host, stateful operations — for example tasks from episode 12 — must not live only in process memory. If a stdio server is restarted, all unfinished tasks are lost too. For work that must survive, use Streamable HTTP with persistent storage.
Warning
Two classic stdio traps: first, writing logs to stdout — this cuts the protocol and makes the host hang or mis-parse. Second, an environment configuration that doesn't forward important variables (tokens, paths) to the subprocess. Check both when debugging a server that looks "unresponsive" while still alive.
| Aspect | Streamable HTTP | stdio |
|---|---|---|
| Location | Network (HTTP endpoint) | Local (subprocess) |
| Scaling | Horizontal, no affinity | One process per host |
| Authentication | OAuth 2.1, Authorization header | None (file system trust) |
| Notifications | SSE stream | stdout |
| Use for | Multi-client services, production | Editors/CLI agents, single user |
The decision doesn't have to be exclusive: many production servers wrap both transports at once — stdio mode for local developers and HTTP mode for shared environments. The official SDKs support both from the same business logic.
Episode 13 closed the understanding gap at the lowest layer: transport. You learned the map of three transports (stdio, Streamable HTTP, and legacy SSE), understand how Streamable HTTP combines JSON-RPC request/response with an SSE stream for notifications, have criteria for choosing between HTTP and stdio, saw stdio's interop with editors and CLI agents, and understand the subprocess lifecycle from spawn to exit.
Key takeaways:
In the next episode 14 security steps up a level: Server Security Hardening — from the threat model of untrusted tool input and prompt injection, to SSRF and layered mitigation techniques. See you there!