Learn MCP - Transport Deep Dive
Series/Learning MCP/Episode 13
Episode 13 of 23

Learn MCP - Transport Deep Dive

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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.

The MCP Transport Map

MCP officially supports three transports, and two of them are still relevant in the stateless 2026-07-28 era:

  • stdio — the server runs as a local subprocess, communicating over stdin/stdout. No network, no ports, no authentication.
  • Streamable HTTP — the server is an HTTP endpoint; clients send JSON-RPC as requests and read notifications from an SSE stream.
  • SSE (legacy) — the old SSE transport from the 2024-11-05 spec, using two separate HTTP connections and relying on session state. Replaced by Streamable HTTP since the 2025-03-26 spec and not recommended for new servers.

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.

Streamable HTTP: Request/Response + SSE

In Streamable HTTP, a single endpoint receives all JSON-RPC messages as POST. A client calls tools/call with a normal payload:

Memanggil method tools/call lewat curl
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:

Format event SSE yang diterima client
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.

When to Choose Streamable HTTP

Streamable HTTP is the default choice for production. Some concrete reasons:

  • A server can be called by many clients at once — the stateless HTTP architecture enables horizontal scaling with a load balancer and no session affinity (episode 9).
  • Standard authentication — the Authorization header and the OAuth 2.1 flow from episode 10 work naturally over HTTP.
  • Optional for cross-machine integration — clients on other servers, containers, or CI can call the endpoint without a shared process.

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: Local Process & Interop

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:

  • IDE agents like VS Code run local MCP servers over stdio.
  • CLI agents (Claude Code, Codex) do the same — every local tool configuration is a subprocess definition.
  • Local scripts and pipelines get tool access without having to run a web server.

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 Process Lifecycle in stdio

The host controls the server's lifetime. The typical flow:

  1. The host reads the configuration (command + arguments + environment), then calls spawn.
  2. The host performs the initialize handshake and verifies the server's capabilities.
  3. During its lifetime, JSON-RPC requests are sent to stdin; the server replies on stdout.
  4. The host sends a notifications/exit or closes stdin to signal the end of the session.
  5. The server cleans up its resources and exits; the host waits for the exit code and captures error messages from stderr.

An example spawn from the host side in Node.js:

host.ts - spawn server stdio
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.

Comparing the Two Transports

AspectStreamable HTTPstdio
LocationNetwork (HTTP endpoint)Local (subprocess)
ScalingHorizontal, no affinityOne process per host
AuthenticationOAuth 2.1, Authorization headerNone (file system trust)
NotificationsSSE streamstdout
Use forMulti-client services, productionEditors/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.

Conclusion

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:

  • Streamable HTTP uses one endpoint for JSON-RPC request/response, with an SSE stream as the channel for notifications and streaming.
  • Choose Streamable HTTP for production — multi-client, horizontal scaling, and OAuth 2.1 work naturally on top of it.
  • Choose stdio for local integration — IDE agents and CLI agents run servers as subprocesses without network.
  • stdio has a strict contract: communication over stdin/stdout, and stdout must not be used for logs — always use stderr.
  • The lifecycle is host-controlled; state that must survive a single process shouldn't be stored in memory.

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!

Learn MCP - Transport Deep Dive | Learning MCP