Artificial Intelligence

MCP Error -32000 “Connection Closed”: Why Your Server Exits, and How to Fix It

MCP error -32000 means your stdio server exited. Tested fixes for Claude Desktop, Cursor and VS Code: mcp 2.x imports, env vars, PATH, relative paths, stdout.

MCP error -32000: Connection closed means the client lost your server process. With the stdio transport, the client starts the server as a child process, and -32000 is what it reports when that process exits while a request is still waiting for a reply. So the real question is always why did the server exit? — and the answer is almost always in the server’s own error output, a few lines above the -32000 in the client’s log.

The causes below were each reproduced, with the exact error the client reported:

  1. A server written for the MCP Python SDK 1.x, running on 2.x (new since July 2026).
  2. An environment variable the client didn’t pass.
  3. A relative path, because the server doesn’t start in your project folder.
  4. The wrong Python, without your dependencies.
  5. node or npx missing from the client’s PATH.
  6. A script that ends instead of serving.
  7. A crash in the middle of a request.

Printing to stdout, the cause most guides put first, produced a different error in testing; it has its own section below.

Everything here was tested on 26 October 2026 with the MCP Python SDK 2.3.0 (and 1.30.0 for comparison), the MCP TypeScript SDK 1.32.2, MCP Inspector 2.12.0, uv 0.13.5, Python 3.13 and Node.js 22.14 on macOS 13. Each server was launched through the TypeScript SDK’s stdio client, the way a desktop app launches it. Claude Desktop, Cursor and VS Code themselves weren’t scripted, so their wording of an error can differ from the SDK’s.

What -32000 means, and the errors that look like it

MCP messages are JSON-RPC 2.0, which reserves the codes -32000 to -32099 for implementation-defined server errors. The official TypeScript SDK uses -32000 for ConnectionClosed: when the server process closes, every request still waiting for an answer fails with it. Three neighbouring messages point somewhere else, and telling them apart halves the search:

What the client reports What happened Where to look
MCP error -32000: Connection closed The server started, then exited while the client was waiting The server’s stderr: a traceback, KeyError, ModuleNotFoundError…
spawn npx ENOENT The command was never found, so no process started command, and the client’s PATH
MCP error -32001: Request timed out The server is running, but no reply the client could read arrived in time (60 seconds by default in the TypeScript SDK) Stray output on stdout, a slow first launch, a handler that hangs
Not connected The server died earlier, between requests; this is the next request failing The same as -32000

So -32000 is rarely a bug in Claude Desktop or Cursor: it is the client reporting that your server process went away. (Servers that use the Streamable HTTP transport aren’t child processes; when one of those fails, look at the process behind the URL, the URL itself and authentication instead. This article is about stdio.)

Find the cause in three steps

1. Read the server’s stderr in the client’s log

Clients record what your server writes to stderr. The last lines before the -32000 usually name the cause.

  • Claude Desktop: ~/Library/Logs/Claude/ on macOS, %APPDATA%\Claude\logs on Windows. mcp.log covers connections; mcp-server-<name>.log holds one server’s output. On macOS, tail -n 20 -F ~/Library/Logs/Claude/mcp*.log follows them. Recent versions also log each launch on a line starting Using MCP server command, together with the PATH the server was given.
  • Cursor: open the Output panel (⇧⌘U on macOS) and choose MCP Logs.
  • VS Code: run MCP: List Servers from the Command Palette, select the server, then Show Output.

2. Run the command the way the client does

A desktop app doesn’t start your server the way your terminal would. On macOS, servers launched by an app often start in /, an app opened from the Dock doesn’t read your shell’s startup files (so its PATH can be short), and the server receives only a few environment variables (cause 2). Reproduce that in a terminal, then send the first message a client sends:

Shell
cd /
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"terminal","version":"0"}}}' \
  | env -i HOME="$HOME" PATH=/usr/bin:/bin /Users/you/projects/weather/.venv/bin/python /Users/you/projects/weather/server.py

Use exactly the command and args from your config, and add any variables from its env block after env -i (as NAME=value). A healthy server prints one line of JSON — its reply to initialize — and exits when the input ends. A broken one prints a traceback instead, and stray output shows up in front of the JSON, like loading config... {"jsonrpc":"2.0",….

3. Run the Inspector with the same config file

The MCP Inspector can read your client’s config file and call the server for you:

Shell
npx @modelcontextprotocol/inspector --cli --config ~/Library/Application\ Support/Claude/claude_desktop_config.json --server weather --method tools/list

Replace weather with your server’s name in the config. Inspector 2.x starts servers with the same short list of environment variables as other clients, so a missing variable fails here too, and the server’s traceback is printed right above the error. (Inspector 1.x passed on your whole terminal environment, which is why servers used to “work in the Inspector but not in Claude Desktop”.) The working directory and PATH still come from your terminal, so for those, step 2 is the better test. Without --cli, the Inspector opens a web interface for the same checks.

Cause 1: a server written for mcp 1.x, running on 2.x

Version 2.0.0 of the MCP Python SDK, released on 28 July 2026, renamed FastMCP to MCPServer. Servers and tutorials that use the 1.x import now crash on their first line whenever an unpinned mcp installs as 2.x:

ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver or pin 'mcp<2' to keep running v1 code.

The client shows only MCP error -32000: Connection closed; the message above is in the server’s log. There are two fixes:

  • Keep 1.x code running: pin the SDK below 2 — pip install "mcp<2", or "mcp<2" in your pyproject.toml dependencies. The last 1.x release is 1.30.0.
  • Migrate: the basic import changes like this, and the migration guide lists the other API changes.
Python
# mcp 1.x
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")

# mcp 2.x
from mcp.server import MCPServer
mcp = MCPServer("weather")

Either way, pin the version you tested, so the next major release can’t do this again.

Cause 2: an environment variable the client didn’t pass

The most common startup crash is a missing API key: os.environ["WEATHER_API_KEY"] raises KeyError, and the process exits. What surprises people is how little the client passes on. The TypeScript SDK’s stdio client gives a server only HOME, LOGNAME, PATH, SHELL, TERM and USER (on Windows, a similar short list such as APPDATA, PATH, TEMP and USERPROFILE), plus whatever the config’s env block adds. In testing, a key exported in the very shell that started the client still didn’t reach the server:

KeyError: 'WEATHER_API_KEY'
MCP error -32000: Connection closed

Pass it in the config:

JSON
{
  "mcpServers": {
    "weather": {
      "command": "/Users/you/projects/weather/.venv/bin/python",
      "args": ["/Users/you/projects/weather/server.py"],
      "env": { "WEATHER_API_KEY": "your-key" }
    }
  }
}

And make the server say what is wrong, so the log explains itself next time:

Python
import os
import sys

API_KEY = os.environ.get("WEATHER_API_KEY")
if not API_KEY:
    sys.exit("WEATHER_API_KEY is not set: add it to the env block of your MCP config")

sys.exit with a message writes it to stderr, so it appears in the client’s log right above the -32000.

A config with a real key in it is a secret. Project-level configs (.cursor/mcp.json, .vscode/mcp.json, .mcp.json) are made to be committed, so keep keys out of them: Cursor, for example, can read them from a file with envFile.

Cause 3: a relative path

Because the server doesn’t start in your project folder, open("data/config.json") looks in the wrong place:

FileNotFoundError: [Errno 2] No such file or directory: 'data/config.json'
MCP error -32000: Connection closed

Use absolute paths in args, and resolve the server’s own files from the location of the script rather than the current directory. In Python:

Python
import json
from pathlib import Path

BASE = Path(__file__).resolve().parent
config = json.loads((BASE / "data" / "config.json").read_text())

In Node.js 20.11 or later:

JavaScript
import { readFileSync } from "node:fs";
import { join } from "node:path";

const config = JSON.parse(readFileSync(join(import.meta.dirname, "data", "config.json"), "utf8"));

Cause 4: the wrong Python

"command": "python3" runs whichever Python comes first on the client’s PATH, which is often not the virtual environment where you installed mcp:

ModuleNotFoundError: No module named 'mcp'
MCP error -32000: Connection closed

Point command at the virtual environment’s interpreter, as in the config in cause 2, or let uv choose the environment from the project:

JSON
{
  "mcpServers": {
    "weather": {
      "command": "/Users/you/.local/bin/uv",
      "args": ["--directory", "/Users/you/projects/weather", "run", "server.py"]
    }
  }
}

This worked in testing even with a minimal PATH and / as the working directory. which uv prints the path to use (where uv on Windows). Before you add it, read the section on slow first launches below.

Cause 5: node or npx isn’t on the client’s PATH

Tools installed with nvm, Homebrew or uv usually reach your PATH through your shell’s startup files, which an app opened from the Dock doesn’t read. This fails in two different ways:

  • "command": "npx" isn’t found at all. No process starts, and the client reports spawn npx ENOENT rather than -32000.
  • "command": "/Users/you/.nvm/versions/node/v22.11.0/bin/npx" is found, but npx is a script that begins with #!/usr/bin/env node, and env can’t find node. The server log says env: node: No such file or directory, and the client says MCP error -32000: Connection closed.

Give the absolute path, and give the server a PATH that contains node:

JSON
{
  "mcpServers": {
    "filesystem": {
      "command": "/Users/you/.nvm/versions/node/v22.11.0/bin/npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem@2.0.1", "/Users/you/Documents"],
      "env": { "PATH": "/Users/you/.nvm/versions/node/v22.11.0/bin:/usr/bin:/bin" }
    }
  }
}

Run which node and which npx in your terminal to get the real paths. In Claude Desktop, the Using MCP server command line in mcp.log shows which PATH the server actually got.

On Windows: npx is a .cmd file

This part wasn’t tested on Windows. There, npx is npx.cmd, which Node’s spawn can’t start without a shell. The TypeScript SDK has handled this since version 1.8.0 (March 2025) by using the cross-spawn package, so clients built on a current SDK can usually run npx directly. If yours reports that it can’t find or start npx, the common workaround is to start it through cmd:

JSON
{ "command": "cmd", "args": ["/c", "npx", "-y", "@modelcontextprotocol/server-filesystem@2.0.1", "C:\\Users\\you\\Documents"] }

Cause 6: the script ends instead of serving

A stdio server has to keep running and read requests from stdin. If the script registers its tools and then simply reaches the end, it exits at once — and there is no traceback, because nothing failed. In Python the usual reason is a missing run() call:

Python
if __name__ == "__main__":
    mcp.run()  # stdio by default; returns when the client disconnects

The Node.js equivalent is await server.connect(new StdioServerTransport()).

Cause 7: a crash in the middle of a request

An exception thrown inside a tool handler does not crash the server: both SDKs catch it and return a tool error (isError: true). In testing, Python SDK 2.3.0 sent the client Error executing tool raises and wrote the full traceback to stderr; the TypeScript SDK sent the exception’s message.

What does kill the process is an error outside the handler. In Node.js, the classic case is a promise nobody awaits: when it rejects, Node.js exits.

JavaScript
// Kills the process: nobody is waiting for this promise when it rejects
saveAuditLog(entry);

// Safe: the error is thrown inside the handler and becomes a tool error
await saveAuditLog(entry);

If the process dies while a request is waiting, that request fails with MCP error -32000: Connection closed; if it dies between requests, the next one fails with Not connected. In Python, the equivalents are a crash in native code, the operating system killing the process for using too much memory, or os._exit() — all reproduced as -32000. One surprise from testing: sys.exit() inside a tool on SDK 2.3.0 didn’t end the process cleanly. The server stopped answering, and the call timed out (-32001) instead.

Not -32000: printing to stdout

The MCP specification is strict: a stdio server “MUST NOT write anything to its stdout that is not a valid MCP message”. Breaking that rule is easy — a debugging print(), a console.log(), a library banner — and it is usually blamed for -32000. Testing shows what actually happens with the official TypeScript SDK client:

What the server wrote to stdout What the client did
A whole line, such as console.log("weather server starting") Logged Unexpected token 'w', "weather se"... is not valid JSON, skipped the line and carried on
Text without a newline, such as process.stdout.write("loading config... ") The text joined the next JSON message, so that message was lost: initialize failed with MCP error -32001: Request timed out
print() inside a tool, Python SDK 1.30 ... is not valid JSON, once the output was flushed
print() inside a tool, Python SDK 2.3 Nothing: while the server runs, the SDK points the process’s stdout at stderr, so the text lands in the log. Output written before mcp.run() still reaches the client

So stray output shows up as “not valid JSON” lines in the log, lost messages and timeouts, not as -32000 — at least with the official SDK. Some clients may handle it more strictly, and any of these can break a tool call, so the rule stands: log to stderr. Python’s logging module writes to stderr by default; naming the stream makes that obvious:

Python
import logging
import sys

logging.basicConfig(stream=sys.stderr, level=logging.INFO)
log = logging.getLogger("weather")

log.info("server started")          # stderr: safe
print("debug", file=sys.stderr)     # stderr: safe

In Node.js, console.error writes to stderr; console.log and process.stdout.write write to stdout:

JavaScript
console.error("weather server running on stdio"); // stderr: safe

Libraries can print banners, progress bars or warnings to stdout when they load. Step 2 of the routine above shows anything that appears before the JSON.

Not -32000 either: a slow first launch

npx -y and uv run install a server’s dependencies the first time they run, and that installation happens while the client is waiting for the reply to initialize — 60 seconds by default in the TypeScript SDK. In testing on an Intel Mac, a dependency of the MCP SDK (cryptography 50.0.2) had no prebuilt package for that platform and had to compile from source. The first launch hit the 60-second limit and failed with MCP error -32001: Request timed out. When the client then stopped the server, the build stopped with it, so the second launch compiled it again (41 seconds). Only the third launch was fast: 1.0 seconds.

So install first, and only then add the server to your client: run uv sync in the project, or run the exact npx command once in a terminal.

Before you add a server: it runs as you

A stdio server is a program that runs with your user account’s permissions, and its config entry is effectively a shell command. AI editors have already had serious vulnerabilities here:

  • CVE-2025-54136 (Cursor 1.2.4 and earlier): an MCP config that a user had approved in a shared repository could later be swapped for a malicious command without a new prompt.
  • CVE-2025-54135 (Cursor before 1.3.9): a prompt injection could create .cursor/mcp.json and run commands without the user’s approval.
  • CVE-2026-47751 (Claude Code Action before 1.0.74): a pull request containing a malicious .mcp.json could run code on the GitHub Actions runner and read the workflow’s secrets.

All three are fixed in current versions. The habits that protect you anyway:

  • Read a server’s source, or use one from a publisher you trust.
  • Pin versions (@modelcontextprotocol/server-filesystem@2.0.1, mcp==2.3.0) instead of whatever is latest.
  • Review changes to .mcp.json, .cursor/mcp.json and .vscode/mcp.json in pull requests as carefully as a shell script.
  • Keep secrets out of config files that are committed.

Checklist

  1. Read the server’s stderr in the client’s log; the last traceback usually names the cause.
  2. Tell the errors apart: -32000 means the server exited, ENOENT means it never started, -32001 means no readable reply arrived in time.
  3. Reproduce the launch from / with env -i, then with the Inspector CLI and your config file.
  4. Pin the Python SDK (mcp<2 for 1.x code) and every server package.
  5. Pass every environment variable the server needs in env.
  6. Use absolute paths in command and args, and resolve data files from the script’s location.
  7. Point command at the right interpreter, or use uv --directory … run.
  8. Make sure the script ends with mcp.run() or server.connect().
  9. Await every promise; write logs to stderr only.
  10. Install dependencies before the first launch.
  11. Review MCP configs like shell scripts.

References and further reading

  1. stdio transport, Model Context Protocol specification
  2. Debugging, Model Context Protocol documentation
  3. Migration guide: v1 to v2, MCP Python SDK documentation
  4. JSON-RPC 2.0 specification, JSON-RPC Working Group
  5. Model Context Protocol (MCP), Cursor documentation
  6. Manage MCP servers, Visual Studio Code documentation
  7. CVE-2025-54136, NIST National Vulnerability Database
  8. CVE-2025-54135, NIST National Vulnerability Database
  9. CVE-2026-47751, NIST National Vulnerability Database

Written by

Thanush BD

Software engineer in Bengaluru working where software meets environmental engineering. At Ecologic Engineering I build audit, compliance, monitoring and water-treatment software — mostly backend: data models, APIs, pipelines and scheduled jobs.