Skip to content

MCP stdio: failure-path status text is written to stdout and corrupts JSON-RPC #304

@justrach

Description

@justrach

Summary

codedb currently writes human-readable status/error text to stdout on at least one mcp startup failure path. For stdio MCP, that is a protocol violation: stdout must be reserved for JSON-RPC messages only.

This can cause hosts to fail parsing startup output with errors like:

invalid character 'â' looking for beginning of value

The â is the leading UTF-8 byte from symbols like / when those bytes hit the JSON-RPC stream.

Reproduction

On current main (33b11ab when verified locally):

./zig-out/bin/codedb /tmp mcp

Observed stdout output:

✗ refusing to index temporary root: /private/tmp

That line is currently emitted from src/main.zig on the mcp root-policy failure path.

What I verified

  • A normal launch on current main:

    ./zig-out/bin/codedb --mcp > /tmp/out 2> /tmp/err < /dev/null

    produced:

    • stdout: 0 bytes
    • stderr: the normal codedb mcp: root=... info log
  • So this is not evidence that all mcp startup paths are dirty on current main.

  • It is evidence that at least one real mcp failure path contaminates stdout and can break stdio hosts.

Relevant code

Current verified offending path:

  • src/main.zig:145
    • ✗ refusing to index temporary root: ...

Potentially related out.p(...) call sites in src/main.zig should also be audited for mcp safety.

Why this is out of spec

For stdio MCP servers, human-readable logs/status text should go to stderr, not stdout.

The official TypeScript SDK docs state this explicitly in the stdio server quickstart:

https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server-quickstart.md

Always use console.error() instead of console.log() in stdio-based MCP servers. Standard output is reserved for JSON-RPC protocol messages, and writing to it with console.log() will corrupt the communication channel.

Expected fix

  • Keep stdout clean for JSON-RPC only when running codedb mcp
  • Send all human-readable startup/status/error output to stderr on the mcp path
  • Audit main.zig for any other out.p(...) paths reachable under cmd == "mcp"

Related

This was split out from the broader long-session transport-close discussion in #278, since this stdout-corruption bug is concrete and independently actionable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions