# Stdio transport reads stdin, writes stdout, no console.log pollution

- **Pattern:** `ab-001834` (`mcp-server.transport-protocol.stdio-transport`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001834
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001834)

## 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.

```ts
// 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`:

```ts
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 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:

  ```ts
  // src/index.ts — stdio transport setup
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
  const transport = new StdioServerTransport()
  await server.connect(transport)
  ```

  ```ts
  // 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:
  ```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
- external mcp-spec-stdio — https://modelcontextprotocol.io/docs/concepts/transports#stdio

Taxons: inference-contract, operational-readiness

HTML version: https://auditbuffet.com/patterns/ab-001834
