prerequisite

Processes and standard streams

What a running program is, and how stdin, stdout, and stderr let one program talk to another through pipes.

Before this

Nothing beyond first-year college math. This is a starting page.

Why you need this

Most MCP servers you run on your own computer are not websites. The host (the app you use, such as a desktop assistant) starts the server as a separate program and talks to it by writing text into it and reading text out of it. That conversation runs over three channels every program has, called the standard streams. If you know how they work, you know why a server must never print a debug message to the wrong one.

The idea

A program is a file on disk. A process is a program that is running: the operating system has loaded it into memory, given it a process ID number, and is letting it execute. Run the same program twice and you get two processes.

A process can start another process. The one that starts it is the parent; the new one is the child. When launching a child, the parent chooses:

Choice Example What the child sees
The program python Which file to run
Arguments child.py --mode shout A list of strings, sys.argv in Python
Environment variables GREETING=hi Named strings, os.environ in Python
Where its streams go pipes back to the parent See below

Every process starts with three standard streams, each a one-way flow of bytes:

Stream Number Direction Normal use
stdin (standard input) 0 into the process Data the program reads
stdout (standard output) 1 out of the process The program's real output
stderr (standard error) 2 out of the process Logs, warnings, errors for a human

In a terminal, stdin is your keyboard and stdout and stderr both go to the screen, which is why they look like one thing. They are separate. A pipe is a connection that carries one process's output stream into another process's input. When a parent launches a child "with pipes", the parent holds the other end of the child's stdin (it writes there) and of its stdout (it reads there).

Two more ideas make pipes work in practice.

Buffering. Writing to a stream does not always send bytes immediately. To save effort, the program collects output in a buffer (a holding area in memory) and sends it in large pieces. Python sends output to a terminal line by line, but output to a pipe waits until the buffer fills, which can be thousands of characters. Flushing means "send what is in the buffer now". When two programs take turns line by line, each must flush after every line, or both wait forever.

End of file. When the parent closes its end of the child's stdin, the child's next read returns nothing, called end of file (EOF). That is the polite way to say "we are done".

When a process ends, it returns an exit code: a small integer, where 0 means success and anything else means some failure. The parent can wait for the child and read it.

The rule that matters for MCP follows from all this. When stdout is the channel two programs use to exchange messages, anything else written to stdout corrupts the conversation. Logs go to stderr.

Worked example

A parent launches a child with two arguments and one extra environment variable, then exchanges newline-terminated lines with it.

# child.py
import os
import sys

greeting = os.environ.get("GREETING", "hello")
print(f"child started with args {sys.argv[1:]}", file=sys.stderr)

for line in sys.stdin:  # one line at a time until the parent closes stdin
    word = line.strip()
    sys.stdout.write(f"{greeting}, {word.upper()}\n")
    sys.stdout.flush()  # without this the reply may sit in a buffer
    print(f"answered {word!r}", file=sys.stderr)

print("stdin closed, exiting", file=sys.stderr)
sys.exit(0)
# parent.py
import os
import subprocess
import sys

here = os.path.dirname(os.path.abspath(__file__))
child = subprocess.Popen(
    [sys.executable, os.path.join(here, "child.py"), "--mode", "shout"],  # program plus arguments
    stdin=subprocess.PIPE,    # we write to the child's stdin
    stdout=subprocess.PIPE,   # we read the child's stdout
    stderr=None,              # child's stderr goes straight to our terminal
    env={**os.environ, "GREETING": "hi"},
    text=True,
    bufsize=1,                # line-buffered on our side of the pipes
)

for word in ["apple", "banana"]:
    child.stdin.write(word + "\n")
    child.stdin.flush()
    reply = child.stdout.readline()
    print("parent got:", repr(reply))

child.stdin.close()           # end-of-file tells the child to finish
code = child.wait()
print("child exit code:", code)

sys.executable is the path of the Python running the parent, so the child uses the same Python. Run it with python -u parent.py (the -u turns off the parent's own output buffering, explained below):

child started with args ['--mode', 'shout']
answered 'apple'
parent got: 'hi, APPLE\n'
answered 'banana'
parent got: 'hi, BANANA\n'
stdin closed, exiting
child exit code: 0

Follow one round:

Step Who Stream Bytes
1 parent writes child's stdin apple\n
2 child reads one line its stdin apple\n
3 child writes and flushes its stdout hi, APPLE\n
4 child logs its stderr, to the terminal answered 'apple'\n
5 parent reads one line child's stdout hi, APPLE\n

The lines starting child started and answered never reached the parent's readline. They went to stderr, which the parent left connected to the terminal. Only the protocol (word in, greeting out) travelled through the pipe.

The same run without -u, with the output captured by another program, printed all four of the child's stderr lines first and the parent's three lines after them. Nothing was wrong with the exchange; the parent's own stdout was a pipe that time, so its prints sat in a buffer until it exited. Buffering changes when you see output, not what was sent.

In a server's life

  • Build and connect (stage 3). A local MCP server is the child, the host is the parent, and the server's stdin and stdout carry one JSON message per line. Transports covers that framing, and Building a server in Python shows a server logging to stderr.
  • Secure it (stage 4). A local server runs with your user's permissions and receives its configuration and secrets through arguments and environment variables, so anyone who can edit the host's server list can run code as you.
  • Maintain it (stage 6). stderr is where a local server's logs go, and the exit code tells the host whether it crashed; see Observability and operations.

Common mistakes

  • Printing debug output to stdout. The parent tries to read debug: got apple as a protocol message and fails or hangs. Send it to stderr.
  • Forgetting to flush. A child that writes sys.stdout.write(line.upper()) without flushing left the parent waiting: in a check that gave it three seconds, the parent had received nothing; the reply arrived only when the child exited and its buffer was emptied.
  • Never closing stdin. A child that loops "until end of file" never ends, and the parent waits on wait() forever. Close stdin when you are done.
  • Ignoring stderr completely. If the parent sends stderr to a pipe and never reads it, the pipe fills up and the child freezes on its next log line. Either leave stderr on the terminal, read it, or send it to a file.
  • Treating any stderr output as failure. Plenty of programs write progress to stderr and exit 0. Judge success by the exit code.

Cost

Starting a process is expensive compared with talking to one that is already running. On the Windows machine these pages were checked on, starting and stopping an empty Python process took about 45 milliseconds, while sending one line to a running child and reading the echo back took about 43 microseconds: roughly a thousand times less. A real server also imports its libraries at startup, which adds more. That is why a host starts a local server once and keeps it running rather than launching it per request. Memory is the main ongoing cost: each local server is a full process, so a host with ten local servers runs ten interpreters.

Going further

  • Python's subprocess module documentation, especially Popen, communicate, and the warning about deadlocks.
  • Shell redirection: 2> sends stderr to a file, | pipes stdout into another program.
  • Signals and how a process is asked, then forced, to stop.
  • Unix domain sockets, which carry the same kind of line-by-line conversation between processes.

Leads to

Back to Building and maintaining MCP servers