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.
Critical because any `console.log()` in a stdio-transport server corrupts the protocol channel, causing immediate and total communication failure with connected clients.
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.
ID: mcp-server.transport-protocol.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 that console.error() is used instead of console.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() or print() calls write to stdout in production code paths. Debug/status output uses console.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