Repository navigation
mcp: a handler error without a code reaches the peer as JSON-RPC error code 0 #1394
Description
What happens
When a handler returns an ordinary Go error, the peer receives it with "code": 0. Measured on main at b5d319b:
| side | where the plain error comes from | response |
|---|---|---|
| server | prompts/get handler |
{"code":0,"message":"prompt boom"} |
| server | resources/read handler (resource) |
{"code":0,"message":"resource boom"} |
| server | resources/read handler (template) |
{"code":0,"message":"template boom"} |
| server | tools/call, raw ToolHandler from Server.AddTool |
{"code":0,"message":"raw boom"} |
| server | CompletionHandler |
{"code":0,"message":"complete boom"} |
| server | SubscribeHandler / UnsubscribeHandler |
{"code":0,"message":"subscribe boom"} |
| server | receiving middleware | {"code":0,"message":"middleware boom"} |
| server | handler added with AddReceivingCustomMethod |
{"code":0,"message":"custom boom"} |
| server | second initialize on a legacy session (SDK's own error) |
{"code":0,"message":"duplicate \"initialize\" received"} |
| client | CreateMessageHandler |
{"code":0,"message":"sampling boom"} |
| client | ElicitationHandler |
{"code":0,"message":"elicit boom"} |
| client | receiving middleware | {"code":0,"message":"client middleware boom"} |
The server rows give the same answer over IOTransport on 2025-11-25, over IOTransport on 2026-07-28, and over stateless Streamable HTTP on 2026-07-28 (subscribe/unsubscribe and the second initialize on 2025-11-25 only). The client rows were run on 2025-11-25 over IOTransport. context.DeadlineExceeded returned from a handler is answered the same way.
Not affected:
- A handler that returns a
*jsonrpc.Error, or an error wrapping one, keeps its code. ResourceNotFoundErroris answered with-32602.- A typed
ToolHandlerFor: its error becomes anisErrorresult.
Why
Same cause as #1367. toWireError in internal/jsonrpc2/messages.go copies a code only from an error that is or wraps a WireError, so a plain error keeps the zero value. #1393 fixed the one site that issue was about.
What the specification says
- Prompts, Error Handling (2025-11-25 and 2026-07-28): "Internal errors:
-32603(Internal error)". - Resources, 2026-07-28: "Servers SHOULD return
-32603for internal errors." 2025-11-25 lists "Internal errors:-32603". - Tools, Error Handling: protocol errors are "standard JSON-RPC errors for issues like … Server errors".
For comparison, the TypeScript SDK (1.32.1, shared/protocol.js) answers with -32603 when a handler throws an error that has no integer code.
Proposal
Answer with -32603 (jsonrpc.CodeInternalError) when an error that carries no code is about to be sent, keeping the message as it is. For the duplicate initialize, -32600, matching #1393.
Two ways to do it, and I would like your preference before writing the patch:
- In
toWireError: default to-32603whenever no code is set. One change, covers every row above and future sites, but changes every such answer at once. - At the call sites in
mcponly. Leaves the jsonrpc2 default alone, but it is a dozen sites today and each new handler type needs the same care.
Given how many sites there are, I lean towards 1. Either way gated behind an MCPGODEBUG option and documented under 1.9.0, as in #1393. I can send the PR for whichever you choose.
The only test I found that expects 0 is internal/jsonrpc2/wire_test.go, which builds the error with code 0 explicitly, so it would not change.
AI disclosure: investigated and drafted with Claude Code; I reproduced and reviewed it.
Activity
talhayme commentedon Oct 12, 2026
For comparison, I ran the same fixture server on the other official SDKs over stdio (2025-11-25) and recorded the code each one answers with when a handler fails with an ordinary error:
0-3260300-32603-326030-32603-32603Rust is n/a because its handlers return an error type that cannot be built without a code. Python's
0for a failing prompt function also appears in the table of python-sdk#3669; that issue is about the unknown-name and missing-argument cases and does not ask for this row to change.The fixtures, the cases and the raw results are in https://github.com/talhayme/mcp-sdk-diff (set
error-codes).AI disclosure: measured and drafted with Claude Code; I reviewed it.
guglielmo-san commentedon Oct 15, 2026
@talhayme thank you for the detailed analysis, I think another place to introduce the -32603 in plain errors is in @shared.go right after the invocation of the mh handler
talhayme commentedon Oct 15, 2026
Thanks, agreed. That's a good place for it: wrapping only errors that carry no code, right after
mhinhandleReceive, turns them into -32603 and leaves coded errors and the message text as they are. Two details I'd keep: wrap the error rather than replace it, soerrors.Is/errors.Ason the original still work; and, like the method-not-found change, put it behind anMCPGODEBUGswitch for anyone matching on code 0. Happy to send a PR if the maintainers agree with the direction.AI disclosure: drafted with Claude Code; I reviewed it.
guglielmo-san commentedon Oct 16, 2026
@talhayme yes, feel free to open the PR