Module 6 · The Model Context Protocol (MCP) ⏱ 18 min

The Two Transports: stdio vs Streamable HTTP

By the end of this lesson you will be able to:
  • Name MCP's two official 2026 transports (stdio, Streamable HTTP) and what each is for
  • Explain how stdio frames messages as newline-delimited lines and why embedded newlines break it
  • Choose the correct transport for a scenario based on whether the server is local or shared

A protocol needs a way to travel. JSON-RPC defines the shape of every message, but it says nothing about how those bytes actually move between a client and a server — that is the job of a transport. MCP defines exactly two of them, and they are not interchangeable. One runs a server as a private subprocess of the client; the other exposes a server as an independent process reachable over HTTP. Choosing the wrong one is a structural mistake, not a tuning knob: a stdio server cannot be shared, and an HTTP server cannot ride along inside your editor.

This lesson names the two transports, shows how each frames its messages, and gives you the rule for picking between them.

flowchart LR
  C1["client"] -->|"launches"| S1["stdio server"]
  S1 -->|"stdin and stdout"| C1
  C2["clients"] -->|"HTTP POST"| S2["HTTP server"]
  S2 -->|"response, maybe SSE"| C2
stdio is a private 1:1 subprocess channel; Streamable HTTP is a shared, network-reachable process.

The two official transports

The first is stdio. The client launches the server as a child process and talks to it over that process's standard input and output — the same channels a shell uses. It is local, private, and 1:1: one client, one server, no network. The second is Streamable HTTP. The server is its own long-lived process listening for HTTP requests; any authorised client POSTs a JSON-RPC message to it, and the server may stream a long response back over Server-Sent Events within that same channel. Note the count: there are two, not three. The older standalone HTTP+SSE transport is retired as of 2026; if you see it in old notes, treat it as history.

How stdio frames messages

On stdio the frame boundary is the newline character: one JSON-RPC message per line, written to the server's stdin and read from its stdout. That single rule has a sharp consequence — a message must never contain an embedded newline, because the receiver would treat it as two frames and desynchronise the whole stream. In practice that means you serialise a message to a single line of JSON, append a newline, flush, and the other side reads line by line. There is no Content-Length header and no chunking metadata; the newline is the entire framing protocol, which is why stdio is so easy to implement and so cheap to run inside an editor.

flowchart TD
  SRC["stream of bytes"] --> SPL["split on newline"]
  SPL --> M1["message 1"]
  SPL --> M2["message 2"]
  SPL --> M3["message 3"]
  style SPL fill:#1d4ed8,color:#fff
stdio framing: the newline is the only boundary, so the receiver splits the byte stream into one message per line.
A stdio reader splits the byte stream on newline into one message per line. Press Run.
const NL = String.fromCharCode(10);
function readLines(buffer) {
  return buffer.split(NL).filter(function (line) { return line.length > 0; });
}
const incoming = JSON.stringify({ method: 'ping', id: 1 }) + NL + JSON.stringify({ method: 'ping', id: 2 }) + NL;
const messages = readLines(incoming);
console.log(messages.length);
console.log(JSON.parse(messages[0]).method);
A valid stdio message contains no embedded newline. Press Run.
const NL = String.fromCharCode(10);
function validStdioMessage(s) {
  return s.indexOf(NL) === -1;
}
console.log(validStdioMessage(JSON.stringify({ method: 'ping', id: 1 })));
console.log(validStdioMessage(JSON.stringify({ note: 'a' + NL + 'b' })));

How Streamable HTTP works

Streamable HTTP drops the subprocess assumption. The server is an independent process exposing an HTTP endpoint; a client sends a JSON-RPC request as the body of a POST and reads the JSON-RPC response from the reply. For short request/response calls that is the whole story. For longer operations the server can keep the HTTP response open and stream progress or partial results back as Server-Sent Events inside the same response. The 2026-07-28 release candidate pushes this further toward a stateless core that scales on ordinary HTTP infrastructure, and adds Mcp-Method and Mcp-Name headers so a load balancer can route a request by its operation without parsing the body — the direction the protocol is heading, not something you must implement today.

Transports carry messages; they do not change them

The crucial separation is that a transport only moves bytes. The JSON-RPC message inside — its method, id, params, the envelope your router builds — is identical no matter which transport carries it. That is why the router you just built takes a parsed message object, not a raw socket: once the transport has framed and delivered a complete message, its job is done, and the same router serves both stdio and HTTP. Build the router once, then bolt on either transport as a thin adapter that reads bytes, splits them into messages, and hands each one to the dispatch table. This layering is what keeps an MCP server small: one dispatcher, two small framing adapters, nothing duplicated between them.

Which one to choose

The decision collapses to one question: does the client own the server, or share it? If the server is a private helper that one client launches — a local tool exposed to your editor, a CLI that wraps a database on this machine — stdio is correct: no network, no port to manage, and the server's lifetime is the client's. If the server is a shared resource many clients reach over the network — a hosted knowledge base, a company-wide API gateway — Streamable HTTP is the only fit, because stdio's 1:1 subprocess model cannot be load-balanced or addressed by a second client. When in doubt, ask who launches the server: one local owner means stdio; many remote callers means HTTP.

flowchart TD
  Q["who launches the server"] --> L["one local client"]
  Q --> R["many remote clients"]
  L --> STDIO["stdio"]
  R --> HTTP["Streamable HTTP"]
  style STDIO fill:#166534,color:#fff
  style HTTP fill:#1d4ed8,color:#fff
The choice is about ownership: a private local subprocess takes stdio; a shared networked server takes Streamable HTTP.
Pick the transport from who owns and shares the server. Press Run.
function chooseTransport(scenario) {
  if (scenario.local && !scenario.shared) {
    return 'stdio';
  }
  return 'http';
}
console.log(chooseTransport({ local: true, shared: false }));
console.log(chooseTransport({ local: false, shared: true }));
Exercise

What does this print? The buffer holds two newline-framed messages; readLines drops the trailing empty segment.

const NL = String.fromCharCode(10);
function readLines(buffer) {
  return buffer.split(NL).filter(function (line) { return line.length > 0; });
}
const incoming = JSON.stringify({ id: 1 }) + NL + JSON.stringify({ id: 2 }) + NL;
console.log(readLines(incoming).length);
Exercise

Write parseJsonLines(buffer) that splits buffer on the provided NL newline, drops empty segments, and returns an array of JSON.parse-d objects.

const NL = String.fromCharCode(10);
function parseJsonLines(buffer) {
  // split on NL, drop empties, JSON.parse each
}
Exercise

On stdio each message is one line, so a message must not contain an embedded newline — it would split into two frames. This validateStdioMessage should return false for any string that contains a newline, but it accepts everything. Fix it.

const NL = String.fromCharCode(10);
function validateStdioMessage(s) {
  // must return false if s contains a newline anywhere
  return true;
}
Exercise

Write chooseTransport(scenario) returning 'stdio' or 'http'. Use 'stdio' only when the client runs the server as a private local subprocess (scenario.local is true) AND it is not shared across clients (scenario.shared is false). Otherwise return 'http'.

function chooseTransport(scenario) {
  // local and not shared -> 'stdio'; otherwise -> 'http'
}
Exercise

You deploy an MCP server that many clients across the network will share, behind a load balancer. Which transport do you choose?

Recap

  • MCP has exactly two transports: stdio (a local subprocess) and Streamable HTTP (an independent networked process). The old standalone HTTP+SSE transport is retired.
  • stdio frames messages as one JSON-RPC object per line; a message must never contain an embedded newline, or the stream desynchronises.
  • Streamable HTTP carries a request as a POST body and may stream a long reply via SSE within the same response; the 2026 RC adds Mcp-Method headers so load balancers can route without reading the body.
  • Choose by ownership: one local client that launches the server means stdio; many remote clients sharing it means HTTP.
  • That completes the module: you can frame a message, shake hands, expose primitives, route them, and now carry them over the wire.

Checkpoint quiz

How are individual JSON-RPC messages separated on the stdio transport?

Why is stdio the wrong choice for a server that multiple clients share over the network?

Go deeper — technical resources