modelcontextprotocol / modelcontextprotocol/typescript-sdk
A handler that throws McpError produces a double-prefixed message on the client
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
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 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 returns
new 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at Protocol._onresponse and McpError.fromError, following both the _requestResolvers and normal response paths described in the report. Reproduce the callTool rejection over InMemoryTransport, then verify the error message is prefixed only once without regressing the UrlElicitationRequired branch. Done means thrown McpError responses reach the client with one MCP error prefix.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100