MCP error codes are the numbers in messages such as “MCP error -32000: Connection closed”. The Model Context Protocol (MCP) sends JSON-RPC 2.0 messages, so most codes are JSON-RPC’s own (-32700 to -32603). The official SDKs use two more codes for failures on your side of the connection, -32000 when the connection closes and -32001 when a request times out. On top of those, the Streamable HTTP transport adds the HTTP statuses 400, 404, 405, 406 and 415. We reproduced these messages with the official TypeScript SDK, @modelcontextprotocol/sdk 1.32.2 (published 23 October 2026), against a small server with three tools. The -32603 behaviour and the client registration error are quoted from the SDK’s source code. Your host may word a message differently, but the code and the cause carry over.
MCP error codes at a glance
| Code | What the SDK says | Where it appears | Usual cause |
|---|---|---|---|
| -32700 | “Parse error: Invalid JSON” | HTTP 400 | The request body isn’t valid JSON (truncated, or rewritten by a proxy) |
| -32600 | “Invalid Request: Server already initialized” | HTTP 400 | A second initialize sent to a session-based server |
| -32601 | “Method not found” | Inside a normal response | A misspelled method, or one from a newer protocol revision |
| -32602 | “Tool get_invoices not found”, “Input validation error: Invalid arguments for tool get_invoice” | A tool result with isError: true |
An unknown tool name, or arguments that fail the tool’s input schema |
| -32603 | The message of the error your handler threw | A JSON-RPC error | Code in a request handler threw an error without a code of its own |
| -32000 | “Connection closed” | The client | A stdio server that exited, or closed its output |
| -32000 | “Bad Request: …”, “Not Acceptable: …”, “Method not allowed.”, “Unsupported Media Type: …” | HTTP 400, 405, 406, 415 | A request the Streamable HTTP transport rejects before any tool runs |
| -32001 | “Request timed out” | The client | A request that ran past the client’s timeout (60 seconds by default) |
| -32001 | “Session not found” | HTTP 404 | A session ID the server doesn’t hold any more |
JSON-RPC reserves -32000 to -32099 for “implementation-defined server-errors”, so the same number can mean different things in different SDKs, and even in one. The TypeScript SDK uses -32001 both for a client-side timeout and for an unknown session on the server. Read the message and the HTTP status, not just the number.
“MCP error -32000: Connection closed”
With the stdio transport, the client starts the server as a subprocess and they talk over its standard input and output. “Connection closed” means that process ended, or closed its output, before it answered. When we pointed the SDK client at a command that exits as soon as it starts, it reported exactly MCP error -32000: Connection closed. Check these causes in order:
- The server crashes on start. A missing environment variable or API key, the wrong Node.js or Python version, or a dependency that isn’t installed. Copy the command and arguments from your host’s configuration and run them in a terminal. If the process exits, its error message is the real problem.
- The command can’t be found. In the SDK client a missing command shows up as
spawn <command> ENOENTrather than -32000, but a desktop host that starts servers with a differentPATHfrom your shell may only tell you the connection closed. Use the absolute path to the executable in the host’s configuration. - The server writes logs to standard output. The specification is strict. It says “The server MUST NOT write anything to its
stdoutthat is not a valid MCP message.” Logs belong on standard error, which the server “MAY” use “for any logging purposes”. When our test server printed one startup line to standard output, the SDK client reportedUnexpected token 'S', "Server sta"... is not valid JSON. It carried on, but a stricter client may drop the connection. In Node.js useconsole.errorrather thanconsole.log, and in Python log tosys.stderr.
Remote servers fail differently. The Streamable HTTP transport answers with an HTTP status and a -32000 message, covered in the sections below.
“MCP error -32001: Request timed out”
The TypeScript SDK gives every request 60 seconds by default (DEFAULT_REQUEST_TIMEOUT_MSEC = 60000). When our client called a tool that takes three seconds with a one-second timeout, it reported MCP error -32001: Request timed out. Three fixes, best first:
- Make long tools quick. Start the work, return a job ID, and add a second tool that checks the job.
- Send progress notifications from the tool, and set
resetTimeoutOnProgressin the client’s request options so each notification restarts the clock (maxTotalTimeoutstill caps the total). - Raise the timeout in the client’s request options, if you control the client.
A timeout doesn’t stop the work on the server. The specification says a disconnection “SHOULD NOT be interpreted as the client cancelling its request”. A client that wants to cancel sends a cancellation notification.
“Bad Request: Mcp-Session-Id header is required” (HTTP 400)
In protocol revisions 2025-03-26 to 2025-11-25, a Streamable HTTP server may hand out a session ID with its reply to initialize. From then on the client “MUST include it in the MCP-Session-Id header on all of their subsequent HTTP requests”, and the server “SHOULD respond to requests without an MCP-Session-Id header (other than initialization) with HTTP 400 Bad Request”. Our SDK server answered {"code":-32000,"message":"Bad Request: Mcp-Session-Id header is required"} with HTTP 400. The usual causes:
- The client speaks revision 2026-07-28 and the server doesn’t. That revision removed
initializeand the session header (our MCP stateless migration checklist covers what changed). We sent a request in the 2026-07-28 style, with noinitializeand no session, to the SDK 1.32.2 server. With no earlier client it answeredBad Request: Server not initialized. Once another client had initialised it, the answer wasBad Request: Mcp-Session-Id header is required. With a session but a 2026-07-28 protocol header, it answeredBad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07). Version 1.32.2 of the SDK stops at 2025-11-25, but the version 2 packages support 2026-07-28. The fix is to serve both protocol eras from one endpoint while clients move over. - Something between client and server drops the header. Proxies, API gateways and load balancers can strip unknown headers. Log the headers that reach the server.
- A browser-based client can’t read the header. A browser hides custom response headers from cross-origin scripts unless the server lists them in
Access-Control-Expose-Headers. ExposeMcp-Session-Id, or the client never sees the session ID it should send back. - The client never stored the ID from the
initializeresponse. Check the client’s own logs for the response headers.
“Session not found” (HTTP 404)
The specification lets a server end a session at any time, “after which it MUST respond to requests containing that session ID with HTTP 404 Not Found”, and a client that gets the 404 “MUST start a new session by sending a new InitializeRequest without a session ID attached”. Our SDK server answered {"code":-32001,"message":"Session not found"} with HTTP 404. Three causes account for most of these:
- The server restarted and its sessions lived in memory.
- More than one server instance sits behind a load balancer, and the request reached an instance that never saw the session. Keep sessions in a shared store, route each session to one instance, or move to the stateless 2026-07-28 revision, which has no sessions.
- The session expired on the server’s own timer.
On the client side, the fix is in the specification. Treat 404 as “start again” and re-initialise. A client that shows the error to the user instead is the bug.
HTTP 406, 405 and 415 for requests the transport rejects
These come from hand-written curl tests, gateways that rewrite headers and older clients. None of them reaches your tools.
| Status | Message from SDK 1.32.2 | Fix |
|---|---|---|
| 406 | “Not Acceptable: Client must accept both application/json and text/event-stream” | On POST, send Accept: application/json, text/event-stream. The specification says the client “MUST include an Accept header, listing both” |
| 406 | “Not Acceptable: Client must accept text/event-stream” | On GET, which opens a stream of server messages, accept text/event-stream |
| 405 | “Method not allowed.” | Use POST for messages, GET for the stream and DELETE to end a session |
| 415 | “Unsupported Media Type: Content-Type must be application/json” | Send Content-Type: application/json |
Tool errors arrive as results, not as JSON-RPC errors
In SDK 1.32.2, a call to an unknown tool and a call whose arguments failed the input schema both came back as successful responses carrying a tool result with isError: true. The results read MCP error -32602: Tool get_invoices not found and MCP error -32602: Input validation error: Invalid arguments for tool get_invoice: Invalid at id. A tool that threw an exception came back the same way, with the exception’s message (“database unavailable”). Only a misspelled method, tools/run in our test, produced a JSON-RPC error (-32601, “Method not found”).
The specification splits it slightly differently. For revision 2026-07-28 it lists an unknown tool as a protocol error, a JSON-RPC -32602, and input validation errors as tool execution errors, reported “in tool results with isError: true” so that the model can “self-correct and retry”. Two consequences:
- Your monitoring has to count tool results with
isError: true. A dashboard that counts only JSON-RPC errors misses most tool failures. - Write tool error messages for the model. Say what was wrong and what a valid value looks like. “Invalid at id” tells a model less than “id must look like INV-0457”.
“Incompatible auth server: does not support dynamic client registration”
A remote server that needs sign-in uses OAuth, and the client needs a client ID at your authorization server before it can start. Revision 2026-07-28 prefers Client ID Metadata Documents (CIMD), where the client ID is a URL, and deprecates Dynamic Client Registration. Our MCP OAuth and CIMD guide covers both. The SDK client uses CIMD, with a URL as its client ID, when the authorization server’s metadata says client_id_metadata_document_supported: true and the client has a metadata document to point to. Otherwise it falls back to registration at registration_endpoint. If neither is available it throws Incompatible auth server: does not support dynamic client registration. Hosts word it in their own way, often as “dynamic client registration not supported”. On the server side, the fixes are:
- advertise CIMD support in your authorization server metadata and accept URL client IDs
- keep a registration endpoint while clients that only know Dynamic Client Registration still connect
- for a host that lets users enter a client ID, pre-register one for that host.
Check a remote server in a minute with curl
This sends an initialize request with the headers the transport requires and prints the response headers, including any session ID:
curl -si http://localhost:3001/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
Send the next request (tools/list, say) with the mcp-session-id header from that response and mcp-protocol-version: 2025-11-25. For a server on revision 2026-07-28, which has no initialize, use the raw 2026-07-28 request in our testing guide. The same guide shows how to run these checks in CI with the MCP Inspector command line.
What these messages won’t tell you
- Your host’s wording. Claude, ChatGPT, Cursor and VS Code each wrap errors in their own way, but the code and the HTTP status are what carry across.
- Other SDKs. We reproduced the TypeScript SDK only. The Python SDK and community SDKs use the same JSON-RPC codes but their own messages.
- Whether the server is safe. A server that returns no errors can still forward tokens or trust text a model shouldn’t. Our MCP security best practices guide covers what to check.
Our view
Most MCP errors we see trace back to two decisions: where the server writes its logs, and which protocol revision each side speaks. Put logs on standard error from day one, and while clients move from session-based revisions to 2026-07-28, answer both from one endpoint. A server that does both throws few of the errors on this page.
If you’d rather we did this
Our engineers build MCP servers for ChatGPT plugins and Claude connectors, and fix existing ones, tested on both protocol revisions. See MCP server development. For a security check of a server that already works, see our MCP security audit.
Sources
All pages opened on 5 November 2026.
- MCP specification, revision 2025-11-25: Transports (stdio rules, session management, the Accept and protocol version headers).
- MCP specification, revision 2026-07-28: Tools (error handling).
- JSON-RPC 2.0 specification, section 5.1, error object.
- MCP TypeScript SDK,
@modelcontextprotocol/sdkon npm, version 1.32.2, published 23 October 2026:ErrorCodeindist/esm/types.js, the Streamable HTTP server indist/esm/server/webStandardStreamableHttp.js, the request timeout indist/esm/shared/protocol.jsand client registration indist/esm/client/auth.js. The messages quoted are from our own runs against this version. - MDN Web Docs, Access-Control-Expose-Headers.