← Back to Blog

stdout belongs to the protocol: stdio discipline in pyrpc mcp

·4 min read

stdio MCP servers have one inviolable rule: stdout is the wire. Every byte written there is parsed as protocol by the client. One cheerful print statement from your code, one library banner, one buffered leftover at interpreter shutdown, and the connection corrupts in ways that look like flaky bugs rather than what they are.

How the official SDK helps, and where it cannot

While serving, the SDK redirects flushed stdout writes to stderr, which catches the common case of subprocesses and print calls mid-session. Two windows remain dangerous: anything flushed before serving begins, and anything buffered until process exit drains it. That puts the obligation on us: between process start and mcp.run(), the command must emit nothing to stdout under any path, including failure.

Our rule, mechanically enforced

The dependency-missing path is the trap most integrations fumble. Ours prints its remediation through typer.echo(err=True) and exits before the server module is even imported, verified by a unit test asserting stderr content and exit code 2. During serving, diagnostics flow exclusively through logging, which the SDK routes to stderr. And the stdio test suite parses every single stdout line as JSON-RPC, so contamination is not a code-review opinion, it is a red build:

# every stdout line must be protocol, not commentary
line = proc.stdout.readline()
message = json.loads(line)  # raises on any contamination

The payoff

Clean transport discipline is invisible when present and catastrophic when absent. It is also the cheapest kind of production polish: a convention, a helper, and tests that turn the convention into physics. The result is a server you can point Claude Desktop, Cursor, or OpenCode at with the same confidence as any flagship integration.