A handler that throws McpError produces a double-prefixed message on the client
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: nopeWhy
Three steps, each defensible alone:
McpError's constructor callssuper(`MCP error ${code}: ${message}`), so.messagealready carries the prefix.- The server serialises a thrown error as
message: error.message, so the prefix travels inside the JSON-RPCerror.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 that this is the path acallToolrejection takes, by counting calls intofromError: 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.
Source: modelcontextprotocol/typescript-sdk