Stdio transport reads stdin, writes stdout, no console.log pollution
Why it matters
When an MCP server uses stdio transport, stdout is the JSON-RPC channel. A single console.log() in a tool handler writes non-JSON text into that channel, corrupting the message stream. Clients receive parse errors or silently drop subsequent messages. This is the most common MCP server defect in production — debug logging left in handlers from development. CWE-116 (improper encoding/escaping) applies directly: the output stream carries protocol data, not freeform text, and mixing the two is a protocol violation that makes the server non-functional.
Severity rationale
Critical because any `console.log()` in a stdio-transport server corrupts the protocol channel, causing immediate and total communication failure with connected clients.
Remediation
Replace every console.log() in server code with console.error(). stderr is safe; stdout is the protocol channel.
// src/tools/search.ts — WRONG and CORRECT
// WRONG — corrupts stdio transport
console.log(`Searching: ${query}`) // do not commit
// CORRECT — stderr is safe for debug output
console.error(`Searching: ${query}`)
For logging libraries (Winston, Pino), set their transport explicitly to process.stderr:
import pino from 'pino'
const logger = pino({ transport: { target: 'pino/file', options: { destination: 2 } } }) // fd 2 = stderr
Run grep -rn 'console\.log' src/ before every release to catch regressions.
Detection
-
ID:
stdio-transport -
Severity:
critical -
What to look for: Count all transport implementations. Enumerate whether stdio transport reads from stdin and writes to stdout with proper newline-delimited JSON. Check every
console.log(),console.info(),console.warn(),console.error(),print(),fmt.Println()call in the codebase. When using stdio transport, stdout is the JSON-RPC channel — any non-JSON-RPC output (debug messages, status updates, error dumps) will corrupt the protocol stream. Check thatconsole.error()is used instead ofconsole.log()for debug output (stderr is safe). Check for logging libraries that default to stdout. Check that third-party dependencies don't write to stdout. -
Pass criteria: No
console.log()orprint()calls write to stdout in production code paths. Debug/status output usesconsole.error()(stderr). Logging libraries are configured to write to stderr or a file. The only stdout output is JSON-RPC messages from the transport. stdio transport must handle at least 100 messages per second without buffering issues. -
Fail criteria:
console.log()calls in tool handlers or server initialization code, logging library writing to stdout, or debug messages that would corrupt the JSON-RPC stream. -
Skip (N/A) when: The server only uses HTTP/SSE transport (no stdio). All checks skip when no MCP server is detected.
-
Cross-reference: For HTTP transport alternative, see
http-transport. -
Detail on fail:
"Found 12 console.log() calls in tool handlers — these will write to stdout and corrupt the JSON-RPC stream. Clients will receive parse errors or silently drop messages"or"Winston logger defaults to stdout — will corrupt stdio transport. Configure transport to write to stderr or a file" -
Remediation: This is the most common MCP server bug. Every console.log corrupts the protocol:
// src/index.ts — stdio transport setup import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" const transport = new StdioServerTransport() await server.connect(transport)// WRONG — corrupts stdio transport server.tool('search', ..., async ({ query }) => { console.log(`Searching for: ${query}`) // writes to stdout = protocol corruption const results = await search(query) console.log(`Found ${results.length} results`) // more corruption return { content: [{ type: 'text', text: JSON.stringify(results) }] } }) // CORRECT — use stderr for debug output server.tool('search', ..., async ({ query }) => { console.error(`Searching for: ${query}`) // stderr is safe const results = await search(query) console.error(`Found ${results.length} results`) // stderr is safe return { content: [{ type: 'text', text: JSON.stringify(results) }] } })For Python:
import sys # WRONG print(f"Searching for: {query}") # writes to stdout # CORRECT print(f"Searching for: {query}", file=sys.stderr) # stderr is safe
External references
- cwe · CWE-116 — Improper Encoding or Escaping of Output (stdout pollution corrupts protocol stream)
- external · mcp-spec-stdio — MCP Specification — Stdio transport
Taxons
History
- 2026-04-18·v1.0.0·Initial import from mcp-server·automated