modelcontextprotocol / modelcontextprotocol/typescript-sdk

tools/call has no path to a protocol-level "Server errors" response, even for unhandled exceptions

Open
#2,656 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement needs design v1 v2
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

The Tools spec defines two error mechanisms: Protocol Errors (unknown tool, malformed requests, server errors) returned as a JSON-RPC error, and Tool Execution Errors (API failures, input validation, business logic) returned as a normal result with isError: true. "Server errors" is explicitly listed under Protocol Errors.

In McpServer's CallToolRequestSchema handler (server/mcp.js), the entire tool invocation is wrapped in a single try/catch:

catch (error) {
    if (error instanceof McpError) {
        if (error.code === ErrorCode.UrlElicitationRequired) {
            throw error; // escapes as a real protocol error
        }
    }
    return this.createToolError(error instanceof Error ? error.message : String(error));
}

createToolError always returns { content: [...], isError: true }. Every thrown error — regardless of McpError code, regardless of whether it represents a business-level failure or a genuine unhandled/internal exception — is funneled into a Tool Execution Error. The only value that ever escapes this catch and becomes a real JSON-RPC error is McpError with code UrlElicitationRequired, and that's for an unrelated reason (the multi-round-trip input flow).

To Reproduce

server.registerTool("example", { description: "..." }, async () => {
  throw new Error("something actually crashed, unrelated to tool args or business logic");
});

Calling this tool always returns isError: true over a normal (e.g. HTTP 200 on Streamable HTTP) response — never a top-level JSON-RPC error, no matter what's thrown or what code an McpError carries.

Expected behavior
Some way for an error to legitimately escape as a Protocol Error / "Server errors" response, since the spec defines that case explicitly. Right now there's no way to distinguish, at the protocol level, "the tool intentionally reported a failure" from "the handler crashed" — everything becomes isError: true.

Related: #1429 (same catch-all is why raw internal error messages can leak to the client) and #2162 / #1956 (same catch-all also suppresses a protocol-error path for invalid-argument errors specifically). This issue is about the general case — there's no way to reach the spec's "server errors" protocol-error case at all, for any kind of unhandled exception.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in server/mcp.js at McpServer's CallToolRequestSchema handler and reproduce the registered-tool failure described in the issue. Compare the handler's catch behavior with the Tools spec's Protocol Errors and Tool Execution Errors sections. Done should include a defined path for genuine unhandled server failures to produce a top-level JSON-RPC error while preserving intentional tool execution errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.