Tell a stray print() from "MCP error -32000: Connection closed"
Reproduce three ways a print() in a Python stdio MCP server behaves, see what actually causes MCP error -32000, and move the logging to stderr.
MCP error -32000: Connection closed often sends people to the server's print() calls. Running it shows print is usually not the cause. It shows up next to the real one. The same server is connected from two kinds of client below.
[1] Create a one-tool server. A MODE environment variable switches the print style.
$ python3 -m venv .venv && .venv/bin/pip install mcpimport osfrom mcp.server.mcpserver import MCPServer mcp = MCPServer("hello")MODE = os.environ.get("MODE", "") @mcp.tool()def add(a: int, b: int) -> int: """Add two numbers""" if MODE == "flush": print(f"add called: {a} {b}", flush=True) if MODE == "noeol": print(f"add called: {a} {b}", end="", flush=True) return a + b if __name__ == "__main__": if MODE == "flush": print("starting hello server", flush=True) if MODE == "noeol": print("starting hello server", end="", flush=True) mcp.run(transport="stdio")[2] Read the rule first. The MCP specification's stdio section delimits messages with newlines, says the server "MUST NOT write anything to its stdout that is not a valid MCP message", and that it "MAY write UTF-8 strings to its standard error (stderr)" for logging.
[3] Connect it to Claude Code. Register it at project scope, then call the tool with claude -p (which uses the servers in --mcp-config without an approval prompt).
$ claude mcp add --scope project hello -- /srv/hello/.venv/bin/python /srv/hello/server.py$ claude -p "Use the add tool to compute 2+3 and answer with the number only" \ --mcp-config .mcp.json --strict-mcp-config --max-turns 3 --allowedTools mcp__hello__add5A server with a plain print and no MODE gave the same result. With stdout on a pipe, Python holds the text in a buffer until the process exits, so nothing is mixed in while the server runs. flush=True or PYTHONUNBUFFERED=1 changes that.
[4] Set MODE=flush to send newline-terminated print output immediately. claude -p still returned 5, and the debug log (--debug-file) showed no connection failure. A TypeScript SDK 1.32.1 client reports this:
transport error: Unexpected token 's', "starting h"... is not valid JSONresult: 5Lines that are not JSON are reported and skipped. The Python SDK client (2.3.0) prints Invalid JSON and also returns result: 5.
[5] MODE=noeol uses print(..., end=""), with no newline. The next JSON message gets glued onto that line and the whole line is unparseable. The Python SDK saw:
Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='starting hello server{"j..."hello","version":""}}}', ...The initialize response was swallowed by that line. The TypeScript SDK ended with MCP error -32001: Request timed out (not -32000), and the Python SDK raised an ExceptionGroup. Claude Code 2.1.302 got no reply on the first try, tried again, and connected (debug log: version negotiation probe timed out); connect time went from 1.3 s to 3.2 s.
[6] -32000 Connection closed means the server process exited. Reproduce it with a server that dies during startup. Here print only left one line before the crash.
import sysprint("loading config...")import nonexistent_moduletransport error: Unexpected token 'l', "loading config..." is not valid JSONERR: MCP error -32000: Connection closedClaude Code's debug log:
[ERROR] MCP server "hello" Connection failed (CONNECTION_CLOSED): Connection closedThe "is not valid JSON" line is a side effect. The real reason (ModuleNotFoundError) is only on stderr, which a client that discards stderr never shows.
[7] To see the real cause, run the server command in a terminal. With no input it just waits on stdin, so a startup error prints its traceback right away.
$ /srv/hello/.venv/bin/python /srv/hello/server_exit.pyloading config...Traceback (most recent call last): File "/srv/hello/server_exit.py", line 3, in <module> import nonexistent_moduleModuleNotFoundError: No module named 'nonexistent_module'[8] Send logs to stderr: add file=sys.stderr to each print, or use logging (logging.basicConfig writes to stderr by default).
import sysprint("add called", file=sys.stderr)Result with the stderr version, using a TypeScript SDK client with stderr attached to the terminal. No transport error, and the log appears separately:
starting hello serveradd called: 2 3result: 5A server that crashes on startup while logging to stderr still gives Connection closed, but the ModuleNotFoundError traceback is printed with it.