You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A handler that throws McpError produces a double-prefixed message on the client #2786
A request handler that throws McpError produces a message the client shows with the prefix twice.
Reproduced with the SDK alone over InMemoryTransport on 1.24.3, and the code path is unchanged in the
current 1.30.0:
McpError's constructor calls super(`MCP error ${code}: ${message}`), so .message already carries
the prefix.
The server serialises a thrown error as message: error.message, so the prefix travels inside the
JSON-RPC error.message field.
Protocol._onresponse converts it back with McpError.fromError(response.error.code, response.error.message, response.error.data), whose default branch returns new McpError(code, message, data) and prefixes what is already prefixed. I confirmed at runtime that
this is the path a callTool rejection takes, by counting calls into fromError: exactly one.
_onresponse also holds a new McpError(...) conversion in its _requestResolvers branch, for queued
responses, which double-prefixes for the same reason.
Throwing McpError is the SDK's own mechanism and shared/protocol throws it in several places itself, so
this is the default result rather than a misuse.
What I expected
One prefix. Either the JSON-RPC error.message carries the bare message, or the client stops re-wrapping a
message that already has the prefix.
Not checked
Only InMemoryTransport, and only a callTool rejection. The one branch of fromError that does not take
the default path is UrlElicitationRequired carrying elicitations, which returns UrlElicitationRequiredError; that class calls super with the same code, so I would expect it to prefix
too, but I did not exercise it.
Confirmed on the published packages with Node 24.19.0: @modelcontextprotocol/[email protected] and the v2 client/server packages at 2.0.0. I recorded the server's error response at the public InMemoryTransport.send boundary and checked all four server/client combinations. All negotiated protocol version 2025-11-25.
For the -32601 error in the report:
Server → client
Prefixes in wire error.message
Prefixes in client .message
v1 → v1
1
2
v1 → v2
1
1
v2 → v1
0
1
v2 → v2
0
0
I also exercised the previously unchecked UrlElicitationRequiredError path (-32042): it has the same message behavior, while code, data.elicitations, and the reconstructed specialized error type are preserved. A plain Error control does not acquire a server-side prefix. These checks cover ordinary callTool rejection over in-memory transport, not task-queued responses or HTTP.
#1727 already addressed this in v2 by removing the constructor prefix, replacing #1578's separate wire-message approach. It explicitly identified the local .message change as breaking, so I would not assume the same constructor change is appropriate for v1.
Would a v1.x fix that preserves the locally constructed McpError.message format while serializing the original, unprefixed message be welcome, or would you prefer documenting the v2 behavior here? If a v1 patch is desired, I can work on it with wire/client regression coverage, including URL elicitation, without heuristically stripping text from arbitrary error messages.
I'd like to take this. From the trace, the client re-wraps the server's error message when it builds its own McpError, so the MCP error -32601: prefix gets applied twice — I'll look at skipping the prefix when the incoming message already carries one, and cover both branches if the v1/v2 code paths differ.
I'd like to work on this, could a maintainer assign it to me?
Proposed approach (v1.x branch, no breaking change to McpError.message): the v1 server should not put the constructor's MCP error <code>: prefix on the wire. When serializing a thrown McpError, send the original (unprefixed) message — the prefix is a local-presentation concern, and both v1 and v2 clients add it when reconstructing. Per @pj-workspace's matrix this fixes the v1→v1 double prefix (wire goes from 1 prefix to 0) and keeps v1→v2 clean; v2 rows are already correct. Will add a regression test asserting the wire message and the client-visible message for a handler that throws McpError.
Related, but not covered by #2881: on v1.x the prefix also reaches model-visible tool results. McpServer's tools/call catch passes error.message to createToolError (mcp.ts#L282-L289), so a handler-thrown McpError, an input validation failure (L346) and an unknown/disabled tool (L239-L242) all come back as isError text starting with MCP error -32602: .
This happens on the server alone, so #2881's wire and fromError changes don't affect it. McpServer + Client over InMemoryTransport on 1.32.1 (the #2881 preview build prints the same three lines):
handler throws McpError: isError=true text="MCP error -32602: No record x"
invalid input: isError=true text="MCP error -32602: Input validation error: Invalid arguments for tool lookup: Invalid input: expected string, received number at id"
unknown tool: isError=true text="MCP error -32602: Tool nope not found"
With #2881 applied, using error.rawMessage for McpError in that catch removed the prefix in all three cases (tried on a local copy of the preview build). I can open that as a small v1.x follow-up if you want it.. v2 already avoids this after #1727 and #1389. In my own server I work around it by throwing a plain Error.
Activity
Ben-Home commentedon Sep 11, 2026
pj-workspace commentedon Sep 11, 2026
Confirmed on the published packages with Node 24.19.0:
@modelcontextprotocol/[email protected]and the v2 client/server packages at2.0.0. I recorded the server's error response at the publicInMemoryTransport.sendboundary and checked all four server/client combinations. All negotiated protocol version2025-11-25.For the
-32601error in the report:error.message.messageI also exercised the previously unchecked
UrlElicitationRequiredErrorpath (-32042): it has the same message behavior, whilecode,data.elicitations, and the reconstructed specialized error type are preserved. A plainErrorcontrol does not acquire a server-side prefix. These checks cover ordinarycallToolrejection over in-memory transport, not task-queued responses or HTTP.#1727 already addressed this in v2 by removing the constructor prefix, replacing #1578's separate wire-message approach. It explicitly identified the local
.messagechange as breaking, so I would not assume the same constructor change is appropriate for v1.Would a
v1.xfix that preserves the locally constructedMcpError.messageformat while serializing the original, unprefixed message be welcome, or would you prefer documenting the v2 behavior here? If a v1 patch is desired, I can work on it with wire/client regression coverage, including URL elicitation, without heuristically stripping text from arbitrary error messages.LouisDeconinck commentedon Sep 11, 2026
I'd like to take this. From the trace, the client re-wraps the server's error message when it builds its own McpError, so the
MCP error -32601:prefix gets applied twice — I'll look at skipping the prefix when the incoming message already carries one, and cover both branches if the v1/v2 code paths differ.fszcd commentedon Sep 13, 2026
I'd like to work on this, could a maintainer assign it to me?
Proposed approach (v1.x branch, no breaking change to
McpError.message): the v1 server should not put the constructor'sMCP error <code>:prefix on the wire. When serializing a thrownMcpError, send the original (unprefixed) message — the prefix is a local-presentation concern, and both v1 and v2 clients add it when reconstructing. Per @pj-workspace's matrix this fixes the v1→v1 double prefix (wire goes from 1 prefix to 0) and keeps v1→v2 clean; v2 rows are already correct. Will add a regression test asserting the wire message and the client-visible message for a handler that throwsMcpError.errmakov commentedon Sep 27, 2026
I'd like to take this — could a maintainer assign it to me?
Majny commentedon Oct 18, 2026
Related, but not covered by #2881: on v1.x the prefix also reaches model-visible tool results.
McpServer'stools/callcatch passeserror.messagetocreateToolError(mcp.ts#L282-L289), so a handler-thrownMcpError, an input validation failure (L346) and an unknown/disabled tool (L239-L242) all come back asisErrortext starting withMCP error -32602:.This happens on the server alone, so #2881's wire and
fromErrorchanges don't affect it.McpServer+ClientoverInMemoryTransporton 1.32.1 (the #2881 preview build prints the same three lines):With #2881 applied, using
error.rawMessageforMcpErrorin that catch removed the prefix in all three cases (tried on a local copy of the preview build). I can open that as a small v1.x follow-up if you want it.. v2 already avoids this after #1727 and #1389. In my own server I work around it by throwing a plainError.