modelcontextprotocol / modelcontextprotocol/typescript-sdk

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.

bug P2 ready for work v1
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

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.

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 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.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.