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.
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.
- No redirection: stdout and stderr both reach the screen.
>connects fd 1 toout.txt. Errors still reach the screen, so you see failures.2>connects fd 2 toerr.log. The screen now gets nothing.|connects fd 1 to a pipe. The pipe is fd 0 ofsort.sortwrites its own fd 1 to the screen.
./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.
stdout → terminal
stdout → pipe or file
C stderr → anywhere
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.
- The client launches the server as a subprocess. The client now owns the server’s stdin, stdout, and stderr.
- The client writes one JSON-RPC request, ending in a newline, to the server’s stdin.
- The server writes the response as one line to stdout. The client parses each line as a message.
- Logs go to stderr. The client may show, store, or ignore them; the protocol is unaffected.
- A stray
printto stdout is not JSON. The client fails to parse it. The spec forbids this: stdout carries only MCP messages. - To stop, the client closes stdin. The server reads end-of-file and exits; a client may then send a terminate signal.
Choosing a transport
Direct stdio connects a client to its local server process
stdio
Streamable HTTP
| stdio | Streamable HTTP | |
|---|---|---|
| Who starts the server | The client, as a subprocess | You deploy it |
| Clients per server process | One | Many |
| How the client reaches it | Inherited stdin and stdout | HTTP POST and GET to one URL |
| Credentials | From the environment, such as env vars | Authorization per request, such as OAuth bearer tokens |
| A cloud agent can reach your computer | needs a bridge direct stdio has no network endpoint | yes with network access and required authorization |
Remember
Three facts carry most stdio problems
- A process gets fd 0, 1, and 2 from whoever starts it. The shell or the MCP client decides the targets.
- stdout is for data; stderr is for people. In MCP, one non-JSON line on stdout breaks the session.
- 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.