What happens
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:
server.setRequestHandler(CallToolRequestSchema, async () => {
throw new McpError(ErrorCode.MethodNotFound, "Unknown tool: nope");
});
// ...
try { await client.callTool({ arguments: {}, name: "nope" }); }
catch (e) { console.log("client received:", e.message); }
server threw: MCP error -32601: Unknown tool: nope
client received: MCP error -32601: MCP error -32601: Unknown tool: nope
Why
Three steps, each defensible alone:
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.
What happens
A request handler that throws
McpErrorproduces a message the client shows with the prefix twice.Reproduced with the SDK alone over
InMemoryTransporton 1.24.3, and the code path is unchanged in thecurrent 1.30.0:
Why
Three steps, each defensible alone:
McpError's constructor callssuper(`MCP error ${code}: ${message}`), so.messagealready carriesthe prefix.
message: error.message, so the prefix travels inside theJSON-RPC
error.messagefield.Protocol._onresponseconverts it back withMcpError.fromError(response.error.code, response.error.message, response.error.data), whose default branch returnsnew McpError(code, message, data)and prefixes what is already prefixed. I confirmed at runtime thatthis is the path a
callToolrejection takes, by counting calls intofromError: exactly one._onresponsealso holds anew McpError(...)conversion in its_requestResolversbranch, for queuedresponses, which double-prefixes for the same reason.
Throwing
McpErroris the SDK's own mechanism andshared/protocolthrows it in several places itself, sothis is the default result rather than a misuse.
What I expected
One prefix. Either the JSON-RPC
error.messagecarries the bare message, or the client stops re-wrapping amessage that already has the prefix.
Not checked
Only
InMemoryTransport, and only acallToolrejection. The one branch offromErrorthat does not takethe default path is
UrlElicitationRequiredcarryingelicitations, which returnsUrlElicitationRequiredError; that class callssuperwith the same code, so I would expect it to prefixtoo, but I did not exercise it.