Client.callTool() throws -32602 after tools/list_changed replaces an in-flight call's output schema

Open Beginner friendly
#2,612 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
84/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
typescript
Domain
api

Research direction

Start in src/client/index.ts and inspect Client.callTool(), especially the validator lookup relative to the awaited request. Reproduce the race by refreshing tools while a delayed call is pending, then verify that the original result is checked against the schema active when the call began. Done means the old-generation result succeeds without the -32602 validation error.

Written by the indexing model from the issue text.

Description

bug P2 ready for work v1
Description

Client.callTool() retrieves the cached output validator after awaiting the tools/call response.

If listTools() runs while that request is pending, cacheToolMetadata() clears and replaces the validator cache. The completed call can therefore be validated against a newer schema than the one active when the call started.

Reproduction
  1. List a tool whose output schema requires { generation: "old" }.
  2. Start a delayed callTool() for that tool.
  3. While it is pending, refresh the tool list with a schema requiring
    { generation: "new" }.
  4. Complete the original call with { generation: "old" }.

Actual result:
McpError -32602: Structured content does not match the tool's output schema

Expected result:* The call is validated using the schema generation active when the call began.

Proposed fix

Capture the validator before yielding to the request:

const validator = this.getToolOutputValidator(params.name);
const result = await this.request(
  { method: "tools/call", params },
  resultSchema,
  options,
);

This is consistent with the existing required-task check, which is also performed before the request.

diff --git a/src/client/index.ts b/src/client/index.ts
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@
         if (this.isToolTaskRequired(params.name)) {
             throw new McpError(
                 ErrorCode.InvalidRequest,
                 `Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`
             );
         }

-        const result = await this.request({ method: 'tools/call', params }, resultSchema, options);
-
-        // Check if the tool has an outputSchema
+        // A concurrent listTools() can replace the cache while this request is
+        // in flight. Capture the validator used when the call is dispatched.
         const validator = this.getToolOutputValidator(params.name);
+        const result = await this.request({ method: 'tools/call', params }, resultSchema, options);

         if (validator) {
             // If tool has outputSchema, it MUST return structuredContent (unless it's an error)
             if (!result.structuredContent && !result.isError) {
Affected versions

Reproduced with @modelcontextprotocol/sdk 1.30.0.

Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 12h
Merged PRs (30d)
3

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.

More from modelcontextprotocol/typescript-sdk

All issues in modelcontextprotocol/typescript-sdk

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.