Skip to main content
Vantaige

Fix MCP Server Not Working: The Stdout Bug Hiding Claude's Tools

A
Aymen B
11 min read
Fix MCP Server Not Working: The Stdout Bug Hiding Claude's Tools

Fix "Claude Sees No Tools": The MCP Server Stdout Bug 60% of Setups Hit (And the 1-Line Fix)

Your MCP server runs fine when you launch it manually. But when Claude Code or Cursor connects, you get "no tools available" or MCP error -32000: Connection closed. The cause is almost always the same: a single console.log (or print) writing to stdout, which corrupts the JSON-RPC stream the MCP host parses. This guide is the diagnosis, the 1-line fix per language, and the debug workflow that takes under 5 minutes.

TL;DR

  • MCP stdio uses stdout for JSON-RPC only. anything else corrupts the stream

  • The host (Claude Code, Cursor, Claude Desktop) reports "no tools" or error -32000

  • The fix: send all logs to stderr. console.error in Node, print(..., file=sys.stderr) in Python

  • Verify the handshake with npx @modelcontextprotocol/inspector on localhost:6274

  • Other top breakers: missing env block, relative paths, Windows npx instead of npx.cmd


Symptoms. what "broken MCP server" looks like

Each MCP host surfaces the same underlying stdio failure differently. If you see any of the messages below, treat stdout pollution as suspect #1.

Claude Code (/mcp panel):

The server shows status failed in the /mcp panel. The terminal log line is MCP error -32000: Connection closed. Claude Code retries up to five times with exponential backoff before marking the server failed; you can manually retry from /mcp after fixing the cause (Claude Code MCP docs).

Cursor 3.3 (released May 6, 2026):

Cursor's MCP panel shows the server with a red dot and "0 tools." Cursor 3.3 added a context-usage breakdown that surfaces MCP failure states alongside rules and skills (Cursor changelog). The same -32000 shows up in the developer console.

Claude Desktop:

Tools simply do not appear in the Connectors menu. The host writes a log file to:

  • macOS: ~/Library/Logs/Claude/mcp*.log

  • Windows: %APPDATA%\Claude\logs\mcp*.log

The relevant lines look like Invalid JSON-RPC message or Unexpected token. the parser hit your stray stdout output (MCP debugging docs).

Across all three hosts, the giveaway is that the server runs cleanly when you invoke its command directly in a terminal. If the standalone process looks healthy but the host can't see tools, you have a transport bug, not a logic bug.

The root cause. stdio transport explained simply

In the MCP stdio transport, stdout carries JSON-RPC messages and stderr carries logs. full stop. The MCP specification states it directly: "The server MUST NOT write anything to its stdout that is not a valid MCP message" (MCP transports spec).

The host (Claude Code, Cursor, Claude Desktop) launches your server as a subprocess and reads stdout line-by-line, expecting one JSON-RPC message per line. Any other text. a startup banner, a console.log, a dotenv debug line. lands in the same pipe and breaks the parser on the next read.

Stream

What MCP uses it for

If you write the wrong thing

stdin

Host → server JSON-RPC

Host should never violate this

stdout

Server → host JSON-RPC

Server crashes the protocol

stderr

Free-form server logs (host MAY capture)

Safe. used for all debug output

The MCP debugging docs put it in a callout: "Local MCP servers should not log messages to stdout (standard out), as this will interfere with protocol operation" (MCP debugging). One stray write is enough to nuke the handshake.

This is the single most common failure mode. The MCP Playground troubleshooting guide attributes 43% of error -32000 cases directly to stdout pollution (MCP Playground troubleshooting).

The 1-line fix per language

Replace every log call that writes to stdout with the stderr equivalent. Your protocol output stays clean, and the host captures stderr automatically.

Node.js (TypeScript / JavaScript)

BROKEN:

console.log("Server starting on stdio transport");
console.log("Loaded", tools.length, "tools");

FIXED:

console.error("Server starting on stdio transport");
console.error("Loaded", tools.length, "tools");

console.error writes to stderr by default in Node. Also check dotenv. require('dotenv').config() prints debug output to stdout in some versions; pass { quiet: true } or call it before any conditional log path.

Python

BROKEN:

print("Server starting")
print(f"Loaded {len(tools)} tools")

FIXED:

import sys
print("Server starting", file=sys.stderr)
print(f"Loaded {len(tools)} tools", file=sys.stderr)

# For the standard logging module:
import logging
logging.basicConfig(stream=sys.stderr, level=logging.INFO)

If you use the mcp Python SDK, call ctx.session.send_log_message(...) instead. it routes through MCP's logging notification channel rather than the stderr fallback.

Go

BROKEN:

fmt.Println("Server starting")
log.Println("Loaded tools")

FIXED:

fmt.Fprintln(os.Stderr, "Server starting")
log.SetOutput(os.Stderr)     // make this explicit at startup
log.Println("Loaded tools")

fmt.Println writes to os.Stdout and will corrupt the stream. Either use Fprintln(os.Stderr,...) or set the standard logger's output to stderr at process start.

Rust

BROKEN:

println!("Server starting");

FIXED:

eprintln!("Server starting");

// Or, for the log crate:
env_logger::Builder::from_default_env().target(env_logger::Target::Stderr).init();

println! writes to stdout, eprintln! writes to stderr. For larger codebases, configure your logger (env_logger, tracing, etc.) to target stderr explicitly so contributors can't accidentally regress the fix.

How to debug with MCP Inspector

The MCP Inspector is the official transport-agnostic debug UI from the MCP team. It lets you connect to your server outside of any host and watch the raw JSON-RPC. Use it before you wire the server into Claude Code or Cursor.

Install and run (no permanent install needed):

npx @modelcontextprotocol/inspector

That command boots two processes: a React UI on http://localhost:6274 and a proxy on localhost:6277 (Inspector repo).

Connect to your stdio server by entering the command and args in the sidebar (transport: stdio), then click Connect.

A healthy handshake looks like this in the message log:

→ {"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"mcp-inspector","version":"0.x"}},"id":0}

← {"jsonrpc":"2.0","result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"my-server","version":"1.0.0"}},"id":0}

→ {"jsonrpc":"2.0","method":"notifications/initialized"}

→ {"jsonrpc":"2.0","method":"tools/list","id":1}

← {"jsonrpc":"2.0","result":{"tools":[{"name":"my_tool","description":"..."}]},"id":1}

A broken one looks like this. note the unparseable line where stdout was polluted:

→ {"jsonrpc":"2.0","method":"initialize",...}
← Server starting on stdio transport          ← stray console.log to stdout
← {"jsonrpc":"2.0","result":...               ← never parsed; stream is poisoned
   ERROR: Invalid JSON-RPC message
   Connection closed (-32000)

First-hand artifact: ship a side-by-side screenshot of the Inspector UI. red error state on the left (broken handshake, stderr pane showing the leaked console.log), green checkmark on the right (clean initialize → tools/list round trip). It is the single clearest visual proof of the fix.

Other common MCP errors and their fixes

Once stdout is clean, these are the next failures you hit. Each has a deterministic fix.

1. Environment variables not inherited. MCP servers spawned over stdio inherit a limited set of env vars; your shell's .bashrc/.zshrc exports do not propagate. Add an env block in claude_desktop_config.json:

{
  "mcpServers": {
    "myserver": {
      "command": "/absolute/path/to/node",
      "args": ["/absolute/path/to/server.js"],
      "env": { "MYAPP_API_KEY": "sk-..." }
    }
  }
}

2. Relative paths. The working directory of an MCP server launched by a host is undefined. Use absolute paths everywhere. for command, args, and any .env file references. The MCP debugging docs flag this explicitly (MCP debugging).

3. Windows npx instead of npx.cmd. On Windows, "command": "npx" often produces "Cannot connect to MCP server." Use the full path: "command": "C:\\Program Files\\nodejs\\npx.cmd". Backslashes must be doubled in JSON, and paths containing spaces are unreliable on the filesystem MCP server (MCP servers issue #447).

4. JSON syntax errors in the config. A trailing comma or unescaped backslash silently breaks the whole file. Validate before restarting:

jq. ~/Library/Application\ Support/Claude/claude_desktop_config.json

5. Wrong config file on Windows MSIX installs. Claude Desktop's "Edit Config" button opens %APPDATA%\Claude\claude_desktop_config.json, but the MSIX-virtualized app reads from %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json. Edit the virtualized path directly (claude-code issue #26073).

6. Protocol-version mismatch between SDK and host. If your server hardcodes a protocol version and the host expects a newer one, you'll see error -32602 ("Invalid params") on the initialize exchange. Update your MCP SDK to the latest minor version, or let the SDK negotiate.

Test checklist before shipping an MCP server

Run all seven before you commit. They take under 5 minutes combined.

  1. Run the server command in a terminal. it should print nothing to stdout (only stderr).

  2. Connect via npx @modelcontextprotocol/inspector and verify tools/list returns your tools.

  3. Grep your codebase: console.log, print(, fmt.Println, println!. replace each with the stderr equivalent.

  4. Confirm dotenv (or your env loader) is silent or quieted. require('dotenv').config({ quiet: true }).

  5. Validate claude_desktop_config.json (or .cursor/mcp.json) with jq..

  6. Use absolute paths in command and args. no npx shorthand on Windows, use npx.cmd.

  7. Restart the host fully (close all windows, not just minimize) and check /mcp or the Connectors menu.

FAQ

Why does my MCP server work in the terminal but not in Claude?

Because Claude reads your server's stdout as JSON-RPC. When you run the command yourself in a terminal, any extra stdout output is just visible text. harmless. When the host runs it, that text is parsed as a protocol message, fails, and the handshake collapses. The fix is to route every log line to stderr instead of stdout, in every language with console.error (Node), print(..., file=sys.stderr) (Python), Fprintln(os.Stderr,...) (Go), or eprintln! (Rust).

What does MCP error -32000 mean?

-32000 is the JSON-RPC implementation-defined server error code; in MCP it almost always surfaces as "Connection closed". the server's stdio process died or its stdout stream became unparseable. The MCP Playground attributes 43% of these cases to stdout pollution and the rest to missing env vars, wrong paths, or unhandled exceptions during initialization (MCP Playground troubleshooting). Check stderr first. the real exception is almost always there.

Does this affect Streamable HTTP transport too?

No. The stdout/stderr rule is specific to the stdio transport. With Streamable HTTP, the server runs as an independent process and clients communicate over HTTP POST/GET; stderr is not captured by the host on this transport (MCP debugging docs). For HTTP servers, log to your own files or aggregator and inspect requests with curl or browser DevTools instead.

Can I see what Claude Code logs when MCP fails?

Yes. Claude Code surfaces failed servers in the /mcp panel and writes per-server connection logs you can tail. Claude Desktop writes to ~/Library/Logs/Claude/mcp*.log on macOS and %APPDATA%\Claude\logs\mcp*.log on Windows. Tail those during a restart to see the exact failure line. usually Invalid JSON-RPC message followed by the stray text from your server's stdout.

How do I capture stderr while running MCP Inspector?

The Inspector UI shows stderr in a separate pane below the JSON-RPC message log. You don't need to redirect anything. Inspector spawns the server as a subprocess and captures both streams independently. That separation is exactly what makes Inspector the fastest way to spot stdout pollution: legitimate logs appear in the stderr pane, stray protocol-breakers appear inline with the JSON-RPC stream as parse errors.

Will n8n-MCP hit the same issue?

n8n-MCP, launched May 5, 2026, runs as an MCP server that lets Claude Code, Claude Desktop, Cursor, and Windsurf author n8n workflows (n8n-MCP launch coverage). The published package follows MCP transport rules, so a default install should not have the issue. If you fork it or wire in a custom logger, apply the same stderr-only rule before testing.

Is this fixed in newer MCP SDKs?

The SDKs do not. and cannot. prevent application code from calling console.log or print in your own modules. The MCP TypeScript and Python SDKs default their internal logging to stderr correctly. The bug is application-level: anywhere in your codebase, a third-party dependency, or a debug statement you forgot to remove. Lint rules (no-console in ESLint, equivalent in Ruff) catch most regressions.

References

  1. Model Context Protocol. Transports specification. https://modelcontextprotocol.io/docs/concepts/transports

  2. Model Context Protocol. Debugging guide. https://modelcontextprotocol.io/docs/tools/debugging

  3. MCP Inspector (official repo). https://github.com/modelcontextprotocol/inspector

  4. MCP Playground, "MCP Server Not Working? Fix Error -32000". https://mcpplaygroundonline.com/blog/mcp-server-troubleshooting-common-errors-fix

  5. Claude Code MCP documentation. https://code.claude.com/docs/en/mcp

  6. Cursor changelog. https://cursor.com/changelog

  7. n8n-MCP launch coverage (AIToolly, 2026-05-05). https://aitoolly.com/ai-news/article/2026-05-05-n8n-mcp-new-model-context-protocol-enables-ai-assistants-to-build-n8n-workflows

  8. Anthropic, Claude Code GitHub Issue #26073 (Windows MSIX config path bug). https://github.com/anthropics/claude-code/issues/26073

Get the best new AI tools and guides, weekly

One short email a week. The tools worth trying, the guides worth reading, nothing else.

No spam. Unsubscribe anytime.

A

Aymen B

Contributing writer at Vantaige, covering the AI tools ecosystem.