Concept · Unix processes and MCP

stdio is three numbered streams every process starts with

A program reads from fd 0 and writes to fd 1 and fd 2. Whoever starts the program decides where those streams lead.

stdin = fd 0 · stdout = fd 1 · stderr = fd 2 · fd = file descriptor, a small number the kernel gives an open stream

types output errors Keyboard any program 0 stdin stdout 1 stderr 2 Screen
Fig. 1In a terminal, fd 0 comes from the keyboard and fd 1 and fd 2 both go to the screen.

Redirection

The shell moves the streams; the program never changes

The program only writes “to fd 1”. It does not know if fd 1 is a screen, a file, or another program. The shell opens the target and connects it before the program starts. Step through four commands.

$ ./app $ ./app > out.txt $ ./app > out.txt 2> err.log $ ./app 2> err.log | sort types Keyboard ./app 0 stdin stdout 1 stderr 2 output errors Screen output errors out.txt Screen output errors out.txt err.log pipe errors sort Screen err.log
  1. No redirection: stdout and stderr both reach the screen.
  2. > connects fd 1 to out.txt. Errors still reach the screen, so you see failures.
  3. 2> connects fd 2 to err.log. The screen now gets nothing.
  4. | connects fd 1 to a pipe. The pipe is fd 0 of sort. sort writes its own fd 1 to the screen.
Fig. 2Every command runs the same ./app; only the targets of fd 1 and fd 2 move.

This is why there are two output streams. stdout carries the data that the next program consumes. stderr carries diagnostics. A normal pipe takes only stdout, keeping stderr out of the data.

Buffering

Output to a pipe arrives late unless you flush

By default, C stdio usually line-buffers stdout to a terminal and block-buffers it to a pipe or file. Python has similar stdout defaults. Explicit flushing sends buffered output sooner; runtimes can change these defaults.

immediate

stdout → terminal

app tty sends at each \n
delayed

stdout → pipe or file

app pipe full / flush / exit
immediate

C stderr → anywhere

app any no buffer
Fig. 3The same print appears at once on a terminal but waits in a buffer when stdout is a pipe.

Fix: call fflush(stdout), use print(…, flush=True), or run python -u. This matters for MCP, where the other end waits for every reply. C stderr is commonly unbuffered; Python stderr is line-buffered by default.

MCP

MCP stdio puts JSON-RPC on stdin and stdout

In the stdio transport, the client starts the server as a child process and holds the other ends of its three streams. Each message is one line of JSON. stdout is reserved for protocol messages; logs go to stderr.

spawns: node server.js stdin: {"method":"initialize",…}\n stdout: {"id":1,"result":{…}}\n stderr: connected to CRM stdout: debug x=3 ✕ closes stdin → server reads EOF exits 1 2 3 4 5 6 MCP client MCP server process
  1. The client launches the server as a subprocess. The client now owns the server’s stdin, stdout, and stderr.
  2. The client writes one JSON-RPC request, ending in a newline, to the server’s stdin.
  3. The server writes the response as one line to stdout. The client parses each line as a message.
  4. Logs go to stderr. The client may show, store, or ignore them; the protocol is unaffected.
  5. A stray print to stdout is not JSON. The client fails to parse it. The spec forbids this: stdout carries only MCP messages.
  6. To stop, the client closes stdin. The server reads end-of-file and exits; a client may then send a terminate signal.
Fig. 4MCP stdio is ordinary Unix stdio with one rule: every stdout line must be an MCP message.

Choosing a transport

Direct stdio connects a client to its local server process

local

stdio

one machine spawns client server
remote

Streamable HTTP

client client client /mcp
Fig. 5stdio gives one client its own private server process; Streamable HTTP lets many clients reach one deployed server.
stdioStreamable HTTP
Who starts the serverThe client, as a subprocessYou deploy it
Clients per server processOneMany
How the client reaches itInherited stdin and stdoutHTTP POST and GET to one URL
CredentialsFrom the environment, such as env varsAuthorization per request, such as OAuth bearer tokens
A cloud agent can reach your computerneeds a bridge direct stdio has no network endpointyes with network access and required authorization

Remember

Three facts carry most stdio problems

  1. A process gets fd 0, 1, and 2 from whoever starts it. The shell or the MCP client decides the targets.
  2. stdout is for data; stderr is for people. In MCP, one non-JSON line on stdout breaks the session.
  3. Many runtimes buffer stdout to a pipe. Flush after each message, or replies may arrive late.

Sources

MCP specification 2025-06-18, Transports: modelcontextprotocol.io/specification/2025-06-18/basic/transports. POSIX stdin(3) and setvbuf(3) for buffering defaults. Fig. 2 and Fig. 4 use simplified example commands and messages.