Explain the stdio transport: message framing, and why servers must never write to stdout except protocol messages.
Quick Answer
Over stdio, each JSON-RPC message is written as a single line of UTF-8 text terminated by a newline (\n) on stdout (client-to-server messages go to the server's stdin the same way), with no other framing — no length prefixes, no delimiters beyond the newline. This means a server's stdout is exclusively the protocol channel: if any library, print() call, or dependency writes anything else to stdout — a debug message, a warning, a progress bar — it corrupts the stream, since the client will try to parse that stray text as a JSON-RPC message and fail. This is why MCP servers are expected to redirect all logging to stderr (or a file) instead, and why using print() for debugging during development is a classic first-time mistake that silently breaks the connection.
Detailed Answer
The stdio transport's simplicity — no HTTP, no auth, just pipes — is also exactly where its main foot-gun lives.
The framing rule
{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n
{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}\n
One JSON object per line. No other delimiter, no length header. The client reads stdout line by line and attempts json.loads() (or equivalent) on each line.
What breaks it
# A server that accidentally corrupts its own stdio transport
@mcp.tool()
def fetch_data(url: str) -> str:
print(f"Fetching {url}...") # BUG: this goes to stdout, not a log
data = requests.get(url).text
return data
That innocuous print() statement writes a line to stdout that isn't valid JSON-RPC. The client's parser either crashes or silently discards it. Either way, the connection is now broken or behaving unpredictably. The bug is often invisible in casual testing if it only triggers occasionally.
The fix: log to stderr, or a proper logger
import sys
def log(msg: str):
print(msg, file=sys.stderr) # stderr is safe; it's not part of the protocol
Most SDKs also wire up structured logging — Python's logging module, or the MCP notifications/message logging capability — so ordinary debug output never touches stdout in the first place.
Why this constraint exists at all
stdio is deliberately minimal: no framing overhead, trivial to implement, works with any language that can read/write a subprocess's pipes. The cost of that simplicity is that stdout has to be treated as a strict, single-purpose channel. Some third-party dependencies log to stdout by default, especially older CLI-style libraries. Those have to be explicitly reconfigured or suppressed when running inside an MCP server. Otherwise they intermittently break the transport in a way that's easy to misdiagnose as "the server is flaky" rather than "something wrote to the wrong stream."