From 2c80a8cf99f0251672054dfea7b4b28a55975e34 Mon Sep 17 00:00:00 2001 From: bradAGI <46579244+bradAGI@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:39:05 -0400 Subject: [PATCH] feat(mcp): add MCP-023, tool prints to stdout, corrupting a stdio transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP had no observability rule. OpenAI ships OAI-010 and ADK ships ADK-009 for the same pattern, both at low severity as a lost-diagnostic problem. It is not a lost-diagnostic problem here, which is why this ships at medium rather than low. On a stdio server — the FastMCP default, and the usual way an editor or desktop client launches a server — stdout is the protocol channel, so a print interleaves with the newline-delimited JSON-RPC frames the client is parsing. The client hits a parse error on a line that is not JSON and drops the response or tears down the connection, and the symptom does not resemble the cause: a tool call that returns nothing, or a mid-session disconnect, traced back to a print that reads like harmless debugging. Confidence 0.7 rather than higher because a server run over HTTP or SSE escapes the corruption, though it still loses the diagnostic. The fix names stderr specifically, which the transport leaves alone. --- mcp/observability.yaml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 mcp/observability.yaml diff --git a/mcp/observability.yaml b/mcp/observability.yaml new file mode 100644 index 0000000..500690f --- /dev/null +++ b/mcp/observability.yaml @@ -0,0 +1,41 @@ +policy: + id: mcp_observability + name: MCP tool observability hygiene + category: mcp + description: > + Rules covering how an MCP tool handler emits diagnostics. On a stdio + server — the default transport — stdout is the protocol channel, so a + handler that prints to it is not writing a log line, it is writing into the + JSON-RPC frame stream. + +rules: + - id: MCP-023 + title: MCP tool prints to stdout, corrupting a stdio transport + severity: medium + confidence: 0.7 + language: python + applies_to: + - mcp_tool + scope: tool + match: + has_print_call: true + explanation: > + The tool handler calls print(), which writes to the process's stdout. On + an MCP server using the stdio transport — the default for FastMCP and the + usual way an editor or desktop client launches a server — stdout is not a + log sink, it is the protocol channel: the client reads newline-delimited + JSON-RPC messages from it. A loose print interleaves with those frames, so + the client hits a parse error on a line that is not JSON and, depending on + the client, drops the response, logs a protocol error, or tears down the + connection. The failure does not look like the cause: the symptom is a + tool call that returns nothing or a server that disconnects mid-session, + while the print that broke it reads like harmless debugging. A server run + over HTTP or SSE instead escapes the corruption, but the diagnostic is + still lost — the model never sees stdout, only the return value. + fix: > + Remove the print(). Diagnostics from an MCP server belong on stderr, which + the transport leaves alone and clients typically capture as server logs — + emit through a module logger (logging.getLogger(__name__)) configured with + a StreamHandler on sys.stderr, and make sure nothing in the process + reconfigures logging onto stdout. If the information needs to reach the + model, return it as part of the tool's result instead.