Skip to content

fix(mcp): route tracing output to stderr to prevent JSON-RPC stdio corruption#470

Open
namesreallyblank wants to merge 1 commit into
ruvnet:mainfrom
namesreallyblank:fix/mcp-stdio-stderr
Open

fix(mcp): route tracing output to stderr to prevent JSON-RPC stdio corruption#470
namesreallyblank wants to merge 1 commit into
ruvnet:mainfrom
namesreallyblank:fix/mcp-stdio-stderr

Conversation

@namesreallyblank
Copy link
Copy Markdown

Summary

The ruvector-mcp binary initializes its tracing subscriber without specifying a writer, causing it to default to stdout. Under the stdio MCP transport, this contaminates the JSON-RPC frame stream with log lines, breaking any client that parses stdout as newline-delimited JSON-RPC. This patch adds .with_writer(std::io::stderr) to both the debug and release tracing subscriber initialization branches.

Root Cause

crates/ruvector-cli/src/mcp_server.rs:50-56tracing_subscriber::fmt() without .with_writer(...) defaults to std::io::stdout. When the binary starts with stdio transport, the first bytes on stdout are a tracing log line, not a JSON-RPC frame:

2026-05-18T...Z INFO ruvector_mcp: Starting MCP server with STDIO transport

Any MCP client doing JSON.parse() on the first stdout line receives a SyntaxError.

Reproduction

printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}\n' \
  | ruvector-mcp

Before this fix, first stdout line is the tracing log. After this fix, first stdout line is a valid JSON-RPC initialize response.

MCP clients using @modelcontextprotocol/sdk throw a Zod parse error of this shape (observed in Claude Code's MCP logs):

ZodError: id=null, method=undefined, unrecognized key "error"

— the SDK is trying to validate the plain-text tracing line as a JSON-RPC frame.

Fix

--- a/crates/ruvector-cli/src/mcp_server.rs
+++ b/crates/ruvector-cli/src/mcp_server.rs
@@ -48,10 +48,12 @@ async fn main() -> Result<()> {
     // Initialize logging
     if cli.debug {
         tracing_subscriber::fmt()
+            .with_writer(std::io::stderr)
             .with_env_filter("ruvector=debug")
             .init();
     } else {
         tracing_subscriber::fmt()
+            .with_writer(std::io::stderr)
             .with_env_filter("ruvector=info")
             .init();
     }

Why this works

The MCP stdio transport specification (modelcontextprotocol.io/docs/concepts/transports#stdio) reserves stdout exclusively for newline-delimited JSON-RPC 2.0 frames. All diagnostic output — logs, traces, debug info — must go to stderr, which MCP clients either discard or surface as server-side diagnostics without attempting to parse as protocol frames. Routing tracing_subscriber to std::io::stderr restores correct protocol operation.

Testing

Smoke test against the patched release binary:

Stdin:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}

Stdout (first and only line):

{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"prompts":{},"resources":{},"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"ruvector-mcp","version":"2.2.0"}}}

Stderr (tracing, as expected):

INFO ruvector_mcp: Starting MCP server with STDIO transport
INFO ruvector_mcp::mcp::transport: MCP STDIO transport started

Result: clean stdio. First and only stdout line is a valid JSON-RPC initialize response.

Related

…rruption

The ruvector-mcp binary initializes its tracing subscriber without
specifying a writer, defaulting to stdout. Under the stdio MCP
transport this contaminates the JSON-RPC frame stream with log lines,
causing every @modelcontextprotocol/sdk client to throw a Zod parse
error on the very first frame.

Add .with_writer(std::io::stderr) to both the debug and release
tracing subscriber builders in crates/ruvector-cli/src/mcp_server.rs.

Verified by stdio smoke test: first line of stdout is now a valid
JSON-RPC initialize response with serverInfo.name == "ruvector-mcp",
and tracing output appears exclusively on stderr as required by the
MCP stdio transport spec.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant