Client.listTools() corrupts its cached tool metadata when an output schema fails to compile
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- typescript
- Domain
- api
Research direction
Start in src/client/index.ts at cacheToolMetadata() and run the reproduction against the client. Verify that a failed later output-schema compilation leaves the previous output validator and task metadata available, so the subsequent callTool() still rejects the mismatched result.
Written by the indexing model from the issue text.
Description
Description
Client.listTools() clears its existing output-validator and task-metadata caches before every schema in the replacement catalog has compiled successfully.
If compilation of a later output schema throws, listTools() rejects as expected, but the previously valid metadata has already been erased or partially replaced. Subsequent callTool() operations may
therefore skip output validation, and cached task-support information may also be lost.
A failed catalog refresh should leave the previous complete metadata generation unchanged.
This is separate from the concurrent callTool()/listTools() validator-generation race reported in: #2612
Reproduction
Tested with @modelcontextprotocol/[email protected].
import assert from "node:assert/strict";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
const client = new Client({
name: "metadata-cache-repro",
version: "1.0.0",
});
const validCatalog = ListToolsResultSchema.parse({
tools: [{
name: "versioned",
inputSchema: {
type: "object",
additionalProperties: false,
},
outputSchema: {
type: "object",
properties: {
generation: { const: "old" },
},
required: ["generation"],
additionalProperties: false,
},
}],
});
// The MCP result schema accepts this catalog because the output schema has a
// valid object root. AJV later rejects the invalid nested `type`.
const invalidCatalog = ListToolsResultSchema.parse({
tools: [{
name: "invalid",
inputSchema: {
type: "object",
additionalProperties: false,
},
outputSchema: {
type: "object",
properties: {
value: { type: "not-a-json-schema-type" },
},
},
}],
});
const catalogs = [validCatalog, invalidCatalog];
client.request = async ({ method }) => {
if (method === "tools/list") {
return catalogs.shift();
}
if (method === "tools/call") {
return {
content: [{ type: "text", text: "new" }],
structuredContent: { generation: "new" },
isError: false,
};
}
throw new Error(`Unexpected method: ${method}`);
};
// Installs the validator requiring generation === "old".
await client.listTools();
// Compilation throws, which is expected for the invalid schema.
await assert.rejects(() => client.listTools());
// This should still use the validator from the last successful catalog and
// reject generation === "new". Instead, it resolves because that validator
// was cleared before the failed replacement compiled.
await assert.rejects(
() => client.callTool({
name: "versioned",
arguments: {},
}),
/does not match the tool's output schema/,
);
The final assertion fails with:
AssertionError: Missing expected rejection
Expected behavior
Metadata replacement should be failure-atomic:
- Compile all output validators and collect all task metadata into temporary collections.
- Publish the new collections only after the entire catalog succeeds.
- If any schema compilation throws, preserve the previous complete collections.
Actual behavior
cacheToolMetadata() clears the current collections before compilation begins:
this._cachedToolOutputValidators.clear();
this._cachedKnownTaskTools.clear();
this._cachedRequiredTaskTools.clear();
A later compilation error therefore leaves the client with empty or partially replaced metadata.
Suggested fix
One possible failure-atomic fix is to build replacement Map and Set instances locally, then publish them only after every tool has been processed and every output schema has compiled successfully.
diff --git a/src/client/index.ts b/src/client/index.ts
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@
private cacheToolMetadata(tools: Tool[]): void {
- this._cachedToolOutputValidators.clear();
- this._cachedKnownTaskTools.clear();
- this._cachedRequiredTaskTools.clear();
+ // Compile the complete replacement before publishing it so a late schema
+ // failure leaves the previous successful metadata generation intact.
+ const toolOutputValidators = new Map<string, JsonSchemaValidator<unknown>>();
+ const knownTaskTools = new Set<string>();
+ const requiredTaskTools = new Set<string>();
for (const tool of tools) {
// If the tool has an outputSchema, create and cache the validator
if (tool.outputSchema) {
const toolValidator = this._jsonSchemaValidator.getValidator(
tool.outputSchema as JsonSchemaType
);
- this._cachedToolOutputValidators.set(tool.name, toolValidator);
+ toolOutputValidators.set(tool.name, toolValidator);
}
// If the tool supports task-based execution, cache that information
const taskSupport = tool.execution?.taskSupport;
if (taskSupport === 'required' || taskSupport === 'optional') {
- this._cachedKnownTaskTools.add(tool.name);
+ knownTaskTools.add(tool.name);
}
if (taskSupport === 'required') {
- this._cachedRequiredTaskTools.add(tool.name);
+ requiredTaskTools.add(tool.name);
}
}
+
+ this._cachedToolOutputValidators = toolOutputValidators;
+ this._cachedKnownTaskTools = knownTaskTools;
+ this._cachedRequiredTaskTools = requiredTaskTools;
}
- Dominant language
- TypeScript
- Stars
- 13.5k
- Forks
- 2.2k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 45
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing 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.
More from modelcontextprotocol/typescript-sdk
-
v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
modelcontextprotocol/typescript-sdk#2946 ·
Maintainers usually reply within 1 day
-
[v2] URI template reserved expansions encode existing %HH sequences againPossibly taken @takagibit18 claimed this 2 days ago. Openv1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
modelcontextprotocol/typescript-sdk#2920 · 1 comment ·
Maintainers usually reply within 1 day
-
[v2] URI template strict expansions leave !'()* unencodedPossibly taken @takagibit18 claimed this 2 days ago. Openv1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
modelcontextprotocol/typescript-sdk#2919 · 1 comment ·
Maintainers usually reply within 1 day
-
Malformed params on spec request methods return -32603 Internal error instead of -32602 Invalid paramsPossibly taken A pull request linked to this issue is open or already merged. Openv1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
modelcontextprotocol/typescript-sdk#2916 · 3 comments ·
Maintainers usually reply within 1 day
-
Unconditional `prompt=consent` (when `offline_access` in scope) blocks OAuth in Entra tenants with user consent disabled + admin consent grantedPossibly taken @dasjideepak claimed this 8 days ago. Openv1 v2
Difficulty 1/5 Under an hour Newbie friendliness 90/100
modelcontextprotocol/typescript-sdk#2867 · 1 comment ·
Maintainers usually reply within 1 day
All issues in modelcontextprotocol/typescript-sdk
Similar issues
-
bug ready-for-agent
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
sleeyax/paseo-plugins#112 ·
Maintainers usually reply within 2 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
rossoctl/serverless-harness#428 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
typesense/typesense-js#376 ·
-
ready sonnet zone:app
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
wheresrhys/totf#1103 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
prime-radiant-inc/evener#3726 ·
Maintainers usually reply within 1 day