MCP error -32000 is not in the MCP specification
18 August 2026
MCP stdio Session Debugger The print statement that broke the stream. Runs in your browser.If you search for MCP error -32000 you will find a great deal of advice and
very little agreement, because the code is not defined by the Model Context
Protocol at all. -32000 is an SDK error code meaning the transport closed,
and knowing that is most of the diagnosis: it is never “the server is slow” and
it is never “the model refused”. It is “the process on the other end of the pipe
is gone”.
Here is the enum from the published @modelcontextprotocol/sdk package, with
the SDK’s own comments left in:
// SDK error codes
ConnectionClosed = -32000
RequestTimeout = -32001
// Standard JSON-RPC error codes
ParseError = -32700
InvalidRequest = -32600
MethodNotFound = -32601
InvalidParams = -32602
InternalError = -32603
// MCP-specific error codes
UrlElicitationRequired = -32042
Three groups, and the code everybody searches for is in the first one. That
grouping is the answer to a question people ask constantly: no, you will not
find -32000 in the MCP specification, and no, your server is not required to
return it. JSON-RPC 2.0 reserves the range -32000 to -32099 for
implementation-defined server errors, and the SDK used the first slot in that
range for “the connection went away”.
What it actually means
The client launched your server as a subprocess, connected to its stdin and stdout, and started speaking JSON-RPC. Then the pipe ended. Either the process exited, or it wrote something to stdout that was not a JSON-RPC message and the framing broke.
So the useful reframing is: -32000 is a process problem, not a protocol
problem. Nothing about the message tells you which process problem, which is
why the error is so unhelpful on its own and why the same code shows up in
threads with completely unrelated fixes.
Its neighbour is worth knowing for contrast. -32001 is RequestTimeout, and
that one really does mean the server is alive and did not answer in time. If you
are getting -32001, stop reading the stdout advice below: your problem is
duration, not death.
How it shows up
In Claude Desktop, the server appears in the config and does not appear in the tool list, or appears and immediately disappears. In Claude Code and Cursor, you get the error text with a server name attached and no further detail. In every case the tools simply are not there, and the model behaves as if the server was never configured, because from its point of view it was not.
The confusing part is that it is often intermittent. A server that writes a banner to stdout only on first run, or only when a cache is cold, will connect perfectly most days.
Why the first instinct is wrong
The first instinct is to restart the client, and the second is to raise a timeout. Restarting genuinely does fix a class of these, which is exactly what makes it a bad habit: it teaches you that the error is flaky rather than that your server is.
Raising a timeout does nothing at all for -32000. A timeout is -32001. If
the connection closed, waiting longer for it is waiting for a process that has
already exited.
The third instinct, and the most expensive, is to start rewriting the server’s
tool definitions. Nothing in -32000 is about the content of your messages. The
handshake did not fail on semantics, it failed on transport.
The causes, in the order they actually occur
1. Something wrote to stdout that was not a protocol message
This is the big one, and it is the reason the stdio transport catches people who have written network services for years.
On stdio, stdout is the wire. It is not a log stream, it is not a console,
it is the transport. Every byte your process writes there has to be a framed
JSON-RPC message. A single print("starting up"), a console.log left in a
handler, a dependency that prints a deprecation notice, a progress bar, a
banner: any of them appears in the middle of the stream, the client cannot parse
it, and the connection dies.
The traps that catch people who already know this rule:
- A library printing on import, before any of your code runs.
- A Python warning, which goes to stderr, and a
printinside a dependency, which does not. - Node’s
console.login a callback, long after startup, so the server connects fine and dies twenty minutes later. - A debugger or profiler attached through an environment variable.
npmoruvoutput when the command in your config is a package runner rather than the server itself, and it decides to install something.
The fix is one line of discipline: every diagnostic goes to stderr. Clients
capture stderr and put it in their logs, so you lose nothing by moving it there.
In Python, print(..., file=sys.stderr) or a logging handler bound to stderr. In
Node, console.error, and check that no dependency is writing to stdout on
import.
If you want to see this rather than take my word for it, the stdio session debugger reads a captured session and points at the frame where the stream stopped being JSON-RPC.
2. The command in the config is not runnable the way the client runs it
Your client does not launch the server from your shell, with your shell’s
environment, in your project directory. It launches it from wherever it happens
to be, usually with a much smaller PATH and often as a GUI process with no
login shell.
That produces three failures that all look identical:
- A relative path in
commandorargs. It works when you test it in the project root, and the client is not in your project root. - A binary that is on your
PATHonly because your shell profile puts it there. Version managers for Node and Python are the usual culprits, and a GUI application never sourced your profile. - A command that prompts.
npxwill offer to install a package it does not have, and there is no terminal to answer, so it waits and then fails.
Use an absolute path, or a command that exists on the system PATH without
help. If you are using a package runner, pass the flag that makes it refuse to
install rather than prompt.
3. The process started and then exited on its own
Missing environment variable, unreadable config file, database that is not running, port already bound. From the outside all of these look the same as the first two.
The difference is that this class leaves evidence, and the evidence is in stderr. Your client keeps those logs, and reading them is faster than any amount of guessing.
4. Windows, which fails in its own ways
Worth separating out, because the advice above is written by people on macOS and half of it does not transfer.
- The command is not the command. Node package runners are batch files on
Windows, so the executable a client needs to spawn is
npx.cmd, notnpx. A spawn without shell resolution finds nothing and the process never starts. - Spaces in the path.
C:\Program Files\and any user profile with a space in the account name break a command that was quoted for a POSIX shell. - Backslashes in JSON. A Windows path in a config file needs escaping, and a config that fails to parse gives you a server that never appears rather than a syntax error you can see.
The diagnosis is the same as everywhere else, but step one becomes decisive: run the exact command, exactly as written in the config, from a directory that is not your project.
Which symptom means which cause
The error text is identical in every case, so match on behaviour instead.
| What you observe | Most likely cause | Fastest test |
|---|---|---|
| Fails every time, immediately | Command not runnable as the client runs it | Run the command by hand from another directory |
| Fails every time, after a pause | Server starts, then exits on a missing dependency | Read the client’s stderr log for that server |
| Works alone, fails with other servers configured | Client-side startup contention | Remove the others and reintroduce one at a time |
| Works for a while, dies later | A console.log on a code path that is not startup | Pipe stdout to a file and search for a line that is not JSON |
| Only in one client, fine in the Inspector | The client, not your server | Report it upstream with the Inspector result attached |
-32001 rather than -32000 | Not this problem at all | It is a timeout, so look at duration |
An ordered way to diagnose it
Do these in order. Each step eliminates a class rather than testing a guess.
- Run the exact command from the config, by hand, from a different directory. Not from your project root. If it fails here, you have your answer in the first ten seconds and none of the rest matters.
- Watch what it prints on stdout. Anything that is not JSON-RPC is the bug.
Pipe stdout to a file and look at the first bytes: if the file does not start
with
{, stop and fix that. - Check the client’s stderr log for that server. Every major client keeps one per server. This is where the stack trace is.
- Reproduce it with the MCP Inspector, which speaks the same stdio transport and shows you the exit code and every frame. It removes the client from the picture entirely, which tells you which side to keep debugging.
- Only now, look at your handshake. If the process stays up and the frames are clean, the problem is protocol, and it is a different error class with a different code.
What changed recently, and what did not
Two things worth knowing if you are diagnosing this in 2026.
Some -32000 reports are client-side races rather than server bugs. There
are open issues against major clients where a server that starts correctly is
torn down during initialisation under specific conditions, particularly with
many servers configured at once. If your server passes step 1 and step 4 above
cleanly and only fails inside one client, you may be looking at the client. That
is worth knowing before you spend a day on your own code.
The transport situation is more forgiving than it was, and stdio is not. Remote servers have moved to Streamable HTTP, which fails in visible ways: an HTTP status, a CORS error, a 404. Stdio has no status codes. It has a pipe that either carries JSON-RPC or does not, and that has not changed and will not.
When this advice is wrong
If your server is remote, most of this does not apply. There is no subprocess and no stdout to corrupt. A closed connection there is a network, proxy or authentication problem, and the useful diagnosis is HTTP-shaped: check the status code, check whether the path is the one your client expects, check the token audience. The two transports share an error code and share almost nothing else.
If you are getting -32000 from a server you did not write and cannot run,
skip the diagnosis and check the version pinning. A package runner that resolves
to a new release on every start is a moving target, and the fastest fix is to
pin the version and see if the error follows.
And if a print statement is genuinely the most useful debugging tool you have, do not give it up. Keep printing, print to stderr, and read the client’s log for that server. The rule is not “do not log”. The rule is that on stdio, stdout belongs to the protocol.