A handler that throws McpError produces a double-prefixed message on the client

Author: chrikrahCreated Sep 10, 2026Updated Sep 13, 2026

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:

javascript
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:

  1. McpError's constructor calls super(`MCP error ${code}: ${message}`), so .message already carries the prefix.
  2. The server serialises a thrown error as message: error.message, so the prefix travels inside the JSON-RPC error.message field.
  3. 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.

Source: modelcontextprotocol/typescript-sdk