Skip to content

mcp: a handler error without a code reaches the peer as JSON-RPC error code 0 #1394

Open

Description

@talhayme

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.
  • ResourceNotFoundError is answered with -32602.
  • A typed ToolHandlerFor: its error becomes an isError result.

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 -32603 for 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:

  1. In toWireError: default to -32603 whenever no code is set. One change, covers every row above and future sites, but changes every such answer at once.
  2. At the call sites in mcp only. 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

  1. talhayme commented on Oct 12, 2026

    @talhayme
    ContributorAuthor

    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:

    handler Go v1.8.0 TypeScript 2.3.1 / 1.32.1 Python 2.3.0 Rust 3.5.1
    prompt 0 -32603 0 n/a
    resource 0 -32603 -32603 n/a
    resource template 0 -32603 -32603 n/a

    Rust is n/a because its handlers return an error type that cannot be built without a code. Python's 0 for 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.

  2. guglielmo-san commented on Oct 15, 2026

    @guglielmo-san
    Contributor

    @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

  3. talhayme commented on Oct 15, 2026

    @talhayme
    ContributorAuthor

    Thanks, agreed. That's a good place for it: wrapping only errors that carry no code, right after mh in handleReceive, 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, so errors.Is/errors.As on the original still work; and, like the method-not-found change, put it behind an MCPGODEBUG switch 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.

  4. guglielmo-san commented on Oct 16, 2026

    @guglielmo-san
    Contributor

    @talhayme yes, feel free to open the PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions