modelcontextprotocol / modelcontextprotocol/typescript-sdk
Client.callTool() throws -32602 after tools/list_changed replaces an in-flight call's output schema
Nobody has claimed this yet.
- 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
- List a tool whose output schema requires
{ generation: "old" }. - Start a delayed
callTool()for that tool. - While it is pending, refresh the tool list with a schema requiring
{ generation: "new" }. - 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
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 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