skip to content

Transports

Two bindings carry identical messages: a subprocess on stdin and stdout, or one HTTP endpoint taking POST. Interviewers probe it because the choice decides local versus remote deployment and auth.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

17

In MCP's stdio transport, how are JSON-RPC messages framed on stdin and stdout?

level: juniorimportance: must knowfreq 72%

answer

  1. one message, one line
  2. no header layer at all
  3. think about what else prints
  4. the other stream is for logs
  5. UTF-8, compact JSON, newline-terminated

basics

~10 s

MCP stdio framing is newline-delimited JSON: each JSON-RPC message is one line of UTF-8 text ending in a newline, contains no embedded newlines, and stdout carries nothing except MCP messages.

solid answer

~50 s

The client launches the server as a subprocess, writes JSON-RPC messages to its **stdin** and reads messages back from its **stdout**. Framing is deliberately trivial: one message per line, UTF-8 encoded, terminated by a newline, and a message **must not** contain embedded newlines, so you serialize compactly instead of pretty-printing. There is no `Content-Length` header or any other header layer, so a reader simply splits on newlines and parses each line as one complete message. The other half of the rule is exclusivity: **stdout is reserved for MCP messages only**. A banner, a stray `print`, a progress bar or a library warning on stdout corrupts the stream and breaks the peer's parser; diagnostics belong on `stderr`. In practice you also flush after each write, or the peer blocks on a message still sitting in a pipe buffer. This framing is unchanged in MCP revision 2026-07-28.

code

bash · 1 line
bash
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' | my-mcp-server

go deeper

for a junior

Be able to state the three rules plainly: one JSON-RPC message per line, UTF-8, newline-terminated, and stdout used for nothing but MCP messages. Say where logs go instead.

for a middle

Explain why there is no header layer and what follows from that, including compact serialization, escaped newlines inside strings, and matching responses to requests by JSON-RPC id on a shared stream.

for a senior

Show you have debugged this: flushing and pipe buffering, a single serialized writer so concurrent messages never splice, and hunting down the dependency that printed to stdout and corrupted the stream.

for a principal

Own the tradeoff. Newline-delimited JSON buys zero-dependency framing and shell-testability at the cost of a fragile exclusivity rule on stdout and no place for transport metadata, which is why per-request _meta exists.

## The channel Under the stdio transport the client is also the process supervisor: it spawns the MCP server as a child process and talks to it over the three standard streams it already owns. The client writes to the child's **stdin**, reads the child's **stdout**, and the child writes logs to **stderr**. There is no socket, no port, no URL and no authorization layer; the operating-system process boundary is the whole perimeter, which is why stdio is the binding used for local servers. ## One message, one line The framing rule is as small as it can be. Each JSON-RPC message is written as a single line of text terminated by a newline character, and messages **MUST NOT** contain embedded newlines. Concretely that means you serialize JSON in its compact form: `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{...}}\n`. A pretty-printed object spread over several lines is not legal on this transport, because the reader's only delimiter is the newline. Newlines *inside* string values are not a problem, because JSON escapes them as `\n`, which is two ordinary characters in the byte stream. Messages **MUST** be UTF-8 encoded. There is no charset negotiation and nothing to declare it, so an implementation that writes the platform's default 8-bit encoding will produce garbage the moment a non-ASCII character appears in a tool description or a resource body. ## No header layer at all Developers coming from the Language Server Protocol expect a `Content-Length` header and a blank line before each payload. MCP has none of that. This matters beyond parsing: because there is no header layer, everything that the Streamable HTTP binding puts in headers has nowhere to live on stdio. Under revision 2026-07-28 the protocol version and the client capabilities travel in `params._meta` on every single request (`io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`), so a stdio line is fully self-describing. Launching the subprocess is not a handshake and not a session; the 2026-07-28 revision removed the `initialize` request entirely. ## stdout is reserved The second MUST is that a server **MUST NOT** write anything to stdout that is not a valid MCP message. This is the single most common way a hand-written server fails on its first run: - a startup banner or version line printed by the framework; - a `print()` or `console.log()` left in from debugging; - a dependency emitting a deprecation warning to stdout; - a progress bar or spinner writing control characters. Any of these lands in the middle of the message stream and the client's line parser sees a line that is not JSON. Everything human-facing goes to `stderr` instead, which the transport explicitly reserves for logging. The mirror-image rule exists on the client side: the client **MUST NOT** write anything that is not a valid MCP message to the server's stdin. ## Direction is asymmetric Revision 2026-07-28 tightened the stream contract further: the client **MUST NOT** write JSON-RPC *responses* to stdin, and the server **MUST NOT** write JSON-RPC *requests* to stdout. Requests flow one way only. A server that needs something from the client returns an interim result from the call it is already serving rather than originating a request of its own. ## Practical failure modes **Buffering.** Pipes are block-buffered by default in many runtimes. If a server writes a response but never flushes, the client waits forever on a message that is physically sitting in a buffer. Flush after every message, or open the streams line-buffered. **Concurrent writers.** Requests may be in flight concurrently and both peers may interleave notifications with responses, but a *line* must be written atomically. Two threads writing to stdout without a lock will splice two messages into one corrupt line. Serialize writes behind a single writer. **Correlation, not ordering.** Because everything shares one stream, responses are matched to requests by the JSON-RPC `id`, not by arrival order. A server is free to answer a later request first. **Binary content.** There is no binary frame type. Non-text resource contents are base64-encoded inside the JSON, which keeps every message a printable single line. ## Why this design Newline-delimited JSON needs no framing library, is trivially testable from a shell (`echo` a line into the process and read one back), and is easy to log and diff. The cost is the exclusivity rule on stdout, which is a discipline problem rather than a protocol problem, and the loss of any place to put headers, which 2026-07-28 answers by making every request carry its own metadata.

  • A colleague pretty-prints the JSON before writing it. What breaks?
    Framing. A pretty-printed object spans several lines, and the reader treats every newline as a message boundary, so it tries to parse `{` alone as a complete JSON-RPC message and fails. Messages must be serialized compactly with no embedded newlines. Newlines inside string values are fine because JSON escapes them as \n, which is two ordinary characters in the byte stream.
  • How does a reader tell a response apart from a notification on the same stdout stream?
    By the JSON-RPC envelope, not by framing. A response carries the `id` of the request it answers plus `result` or `error`; a notification carries a `method` and no `id`. Both share the single stdout channel, so the reader dispatches per line: match `id` to a pending request, or route the notification by its method name.
  • Why is flushing after each write more than a performance detail here?
    Because pipes are usually block-buffered. If the server writes a complete response but the runtime holds it in a buffer waiting for more bytes, the client sees nothing and blocks on a read while the server blocks waiting for the next request. That is a deadlock, not slowness. Flush after every message or open the stream line-buffered.

saying these in an interview costs you the question

  • Says MCP stdio uses Content-Length headers like LSP
  • Pretty-prints JSON across multiple lines on the wire
  • Logs or prints banners to stdout alongside messages
  • Assumes responses arrive in request order on the shared stream
  • Thinks binary data is sent as raw bytes between messages

context

open as a page

In MCP's Streamable HTTP transport, what does a client POST and what can a server reply?

level: juniorimportance: must knowfreq 72%

basics

~20 s

MCP's Streamable HTTP transport exposes a single endpoint that accepts POST only. The client POSTs one JSON-RPC message with an Accept header listing both application/json and text/event-stream, and the server answers with either a JSON body or an SSE stream.

open as a page

In MCP 2026-07-28, what replaced protocol-level sessions and the Mcp-Session-Id header?

level: middleimportance: must knowfreq 72%

basics

~20 s

Nothing replaced them. MCP revision 2026-07-28 removed protocol sessions entirely: every request is self-contained and carries its own protocol version and capabilities, so a server must not rely on anything an earlier request on the same connection established.

open as a page

On MCP stdio, where do the protocol version and client capabilities travel?

level: middleimportance: must knowfreq 60%

basics

~10 s

Inside the message itself. Stdio has no header layer, so every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in its params._meta object, on every single call.

open as a page

What headers must an MCP Streamable HTTP POST carry, and what is HeaderMismatchError?

level: middleimportance: must knowfreq 62%

basics

~10 s

An MCP Streamable HTTP request must carry MCP-Protocol-Version, Mcp-Method, and Mcp-Name for tools/call, resources/read and prompts/get. If MCP-Protocol-Version disagrees with the value in params._meta, the server returns HTTP 400 with JSON-RPC error -32020, HeaderMismatchError.

open as a page

Where does cross-call state live in MCP 2026-07-28 now that sessions are gone?

level: seniorimportance: must knowfreq 58%

basics

~20 s

In explicit, server-minted handles. A tool returns an opaque identifier for whatever the server is holding, and the client passes it back as an ordinary argument on later calls — application data in the tool's own schema, not a protocol field or a header.

open as a page

If an MCP response stream breaks mid-request, how does a 2026-07-28 client recover?

level: middleimportance: should knowfreq 56%

basics

~10 s

It cannot resume. Revision 2026-07-28 removed stream resumability, so a broken stream loses the in-flight request outright and the client must re-issue the same call as a brand-new request with a new JSON-RPC id.

open as a page

On MCP's stdio transport, how does a client cancel an in-flight request?

level: middleimportance: should knowfreq 44%

basics

~10 s

The client writes a notifications/cancelled notification naming the requestId of the call it wants abandoned. Cancellation is advisory: the server should stop work, and the client stops waiting whether or not it does.

open as a page

How should a client shut down an MCP stdio server subprocess cleanly?

level: middleimportance: should knowfreq 50%

basics

~10 s

Close the server's stdin so it sees end-of-file and exits on its own, wait a reasonable period for the process to end, and only then force-terminate it with an operating-system signal.

open as a page

In MCP stdio, what is stderr for and why is it not an error signal?

level: middleimportance: should knowfreq 54%

basics

~20 s

On MCP's stdio transport stderr is the server's logging channel. A client may capture, forward or discard it, but should not read output on stderr as an indication that the request or the server failed.

open as a page

Why does an MCP Streamable HTTP server answer GET or DELETE with 405 Method Not Allowed?

level: middleimportance: should knowfreq 58%

basics

~20 s

Because in MCP revision 2026-07-28 the Streamable HTTP endpoint is POST-only. The GET listening stream and the DELETE termination call both belonged to earlier revisions, so a server implementing only 2026-07-28 SHOULD reject those verbs with 405 Method Not Allowed.

open as a page

Why must an MCP server never treat possession of a state handle as authentication?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Because a handle is a reference, not a credential. MCP 2026-07-28 names this threat State Handle Hijacking and states that possession MUST NOT be treated as authentication: handles pass through model context, transcripts and logs, so anyone who sees one could otherwise act as the original caller.

open as a page

What does MCP 2026-07-28 statelessness change for a remote server behind a load balancer?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Session affinity stops being required: with sessions removed, any node can serve any request, so ordinary round-robin routing works. What remains is that long-lived response streams still occupy one node, and any state a tool mints must live in shared storage.

open as a page

Why must an MCP stdio server never write a JSON-RPC request to stdout?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Because MCP revision 2026-07-28 removed server-initiated requests entirely. Requests flow one way on stdio: the client sends them on stdin, and the server may write only responses and notifications on stdout.

open as a page

In MCP Streamable HTTP, what does closing the response stream mean to the server?

level: seniorimportance: should knowfreq 48%

basics

~20 s

It means cancellation. In MCP revision 2026-07-28 a server MUST treat the client closing the HTTP response stream as cancellation of that request and stop the work. Anything that drops the connection — a proxy idle timeout, a closed tab — therefore cancels the call.

open as a page

Why must an MCP Streamable HTTP server validate the Origin header and return 403?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Because a web page can make the browser POST to any reachable HTTP endpoint, including one on localhost. MCP revision 2026-07-28 requires the server to validate the Origin header and answer an invalid one with 403, so a hostile page cannot drive a locally running MCP server's tools.

open as a page

When should an MCP tool mint a state handle instead of taking the state in every call?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Mint a handle when the state is large, sensitive, or meaningless to the model — a handle keeps it out of the context window. Re-send it in arguments when it is small and self-describing, which avoids owning storage, expiry and cleanup for every workflow.

open as a page