-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(server): align ProtocolError re-throw with spec error classification #1769
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@modelcontextprotocol/server': minor | ||
| --- | ||
|
|
||
| Re-throw all `ProtocolError` instances from `tools/call` handler as JSON-RPC errors instead of wrapping them in `isError: true` results. | ||
|
|
||
| **Breaking change:** Output validation failures (missing or schema-mismatched `structuredContent`) now surface as JSON-RPC `InternalError` rejections instead of `{ isError: true }` results. Input validation failures continue to return `{ isError: true }` per the MCP spec's tool-execution-error classification. | ||
|
|
||
| This also means tool handlers that deliberately `throw new ProtocolError(...)` will now propagate that as a JSON-RPC error, matching the python-sdk behavior. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -206,8 +206,8 @@ export class McpServer { | |
| await this.validateToolOutput(tool, result, request.params.name); | ||
| return result; | ||
| } catch (error) { | ||
| if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) { | ||
| throw error; // Return the error to the caller without wrapping in CallToolResult | ||
| if (error instanceof ProtocolError) { | ||
| throw error; | ||
| } | ||
| return this.createToolError(error instanceof Error ? error.message : String(error)); | ||
| } | ||
|
Comment on lines
206
to
213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 Pre-existing bug: Extended reasoning...What the bug isThe The specific code pathWhen a tool call arrives at the In contrast, the normal (non-task) execution path explicitly calls Step-by-step proof
ImpactA task handler could store Relationship to this PRThis bug is pre-existing -- the How to fixAdd a const result = (await ctx.task.store.getTaskResult(taskId)) as CallToolResult;
await this.validateToolOutput(tool, result, request.params.name);
return result; |
||
|
|
@@ -251,10 +251,9 @@ export class McpServer { | |
|
|
||
| const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {}); | ||
| if (!parseResult.success) { | ||
| throw new ProtocolError( | ||
| ProtocolErrorCode.InvalidParams, | ||
| `Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}` | ||
| ); | ||
| // Per spec, input validation failures are tool-execution errors (isError: true), | ||
| // not protocol errors — throw plain Error so the catch wraps it as a tool result. | ||
| throw new Error(`Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}`); | ||
| } | ||
|
|
||
| return parseResult.data as unknown as Args; | ||
|
|
@@ -279,7 +278,7 @@ export class McpServer { | |
|
|
||
| if (!result.structuredContent) { | ||
| throw new ProtocolError( | ||
| ProtocolErrorCode.InvalidParams, | ||
| ProtocolErrorCode.InternalError, | ||
| `Output validation error: Tool ${toolName} has an output schema but no structured content was provided` | ||
| ); | ||
| } | ||
|
|
@@ -288,7 +287,7 @@ export class McpServer { | |
| const parseResult = await validateStandardSchema(tool.outputSchema, result.structuredContent); | ||
| if (!parseResult.success) { | ||
| throw new ProtocolError( | ||
| ProtocolErrorCode.InvalidParams, | ||
| ProtocolErrorCode.InternalError, | ||
| `Output validation error: Invalid structured content for tool ${toolName}: ${parseResult.error}` | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 The
MethodNotFound(-32601) error code at line 186 for task-required-without-task is semantically incorrect per JSON-RPC:-32601means "the method does not exist," buttools/callexists and the tool was found — the request is just missing thetaskparameter. This is a pre-existing error code choice, but this PR makes it client-visible by broadening the catch block to re-throw allProtocolErrors. Consider usingInvalidParams(-32602), which is already used for analogous cases in this handler (tool not found at line 164, tool disabled at line 167).Extended reasoning...
What the bug is
At line 186,
ProtocolErrorCode.MethodNotFound(-32601) is used when a tool withtaskSupport: "required"is called without task augmentation. Per the JSON-RPC 2.0 spec,-32601means "The method does not exist / is not available." However, in this scenario thetools/callmethod absolutely exists, the handler was found, and the specific tool was located — the client simply omitted thetaskparameter from the request.Why this matters now
Before this PR, this error was caught by the old
catchblock (which only re-threwUrlElicitationRequired) and wrapped into anisError: truetool result. The JSON-RPC error code was irrelevant because it never reached the client as a protocol-level error. After this PR, the broadened catch at line 209 (if (error instanceof ProtocolError) { throw error; }) re-throws it as a JSON-RPC error response, making the-32601code directly visible to clients.Step-by-step proof
taskSupport: "required"viaregisterToolTask.tools/callfor that tool WITHOUT ataskfield in the request params.taskSupport === "required" && !isTaskRequestevaluates totrue.ProtocolErrorwithMethodNotFoundcode is thrown (line 185-188).ProtocolErrorand re-throws it.-32601 MethodNotFounderror response to the client.-32601may conclude that thetools/callmethod is not supported by this server, which is incorrect.Internal inconsistency
The same handler uses
InvalidParams(-32602) for analogous situations: "tool not found" (line 164) and "tool disabled" (line 167) both useProtocolErrorCode.InvalidParams. The task-required case is structurally the same — the request is valid but a required parameter is missing — yet uses a different error code.Impact
A client receiving
-32601could misinterpret it as the server not supportingtools/callat all, rather than understanding that the specific invocation is missing thetaskparameter. In practice, the error message text ("requires task augmentation") clarifies the issue, so clients parsing the message string will understand. But clients that dispatch on error codes alone will be misled.Mitigation
The error code at line 186 is pre-existing and was not introduced by this PR. The PR author explicitly documented this as
MethodNotFoundin the migration guides, suggesting it may be an intentional design choice (interpreting "not available" as "this invocation mode is not available"). However,InvalidParams(-32602, "Invalid method parameter(s)") would be more semantically correct and consistent with how the same handler treats other invalid-request scenarios. The fix would be changingProtocolErrorCode.MethodNotFoundtoProtocolErrorCode.InvalidParamsat line 186.