Cache compiled output-schema validators on ClientSession#3134
Open
jlowin wants to merge 1 commit into
Open
Conversation
`validate_tool_result` called one-shot `jsonschema.validate()` on every tool result, which rebuilt the validator class, re-ran `check_schema`, and re-crawled `$ref`s each time. Compile the validator once per schema instead, keyed on the identity of the cached schema object so a relisted tool can never reuse a validator built from its previous schema. Refs modelcontextprotocol#3133.
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/session.py">
<violation number="1" location="src/mcp/client/session.py:367">
P2: A complete tool relist removes stale output schemas but leaves their compiled validators in this new cache. Sessions connected to servers whose tool set changes over time will retain every removed tool's schema and validator indefinitely, even though those entries can no longer be used. Consider pruning `_tool_output_validators` with the same `names` set when `complete` is true.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| self._tool_output_schemas: dict[str, dict[str, Any] | None] = {} | ||
| # Compiled output-schema validators, each paired with the schema object it was built | ||
| # from so a re-listed tool can't be served a validator for its previous schema. | ||
| self._tool_output_validators: dict[str, tuple[dict[str, Any], Validator]] = {} |
There was a problem hiding this comment.
P2: A complete tool relist removes stale output schemas but leaves their compiled validators in this new cache. Sessions connected to servers whose tool set changes over time will retain every removed tool's schema and validator indefinitely, even though those entries can no longer be used. Consider pruning _tool_output_validators with the same names set when complete is true.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/session.py, line 367:
<comment>A complete tool relist removes stale output schemas but leaves their compiled validators in this new cache. Sessions connected to servers whose tool set changes over time will retain every removed tool's schema and validator indefinitely, even though those entries can no longer be used. Consider pruning `_tool_output_validators` with the same `names` set when `complete` is true.</comment>
<file context>
@@ -357,6 +362,9 @@ def __init__(
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
+ # Compiled output-schema validators, each paired with the schema object it was built
+ # from so a re-listed tool can't be served a validator for its previous schema.
+ self._tool_output_validators: dict[str, tuple[dict[str, Any], Validator]] = {}
self._x_mcp_header_maps: dict[str, dict[tuple[str, ...], str]] = {}
self._initialize_result: types.InitializeResult | None = None
</file context>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3133.
ClientSession.validate_tool_resultcalled one-shotjsonschema.validate()on everytool result. That rebuilds the validator class, re-runs
check_schema(), and re-crawls$refs on each call, then throws the whole thing away. Compiling costs ~60x whatvalidating costs, so on an in-memory
call_toolit was the dominant term. This compilesthe validator once and reuses it.
Measured end-to-end on an in-memory
call_tool(median of 2000 calls after warm-up):$defs/$refBehavior is unchanged. The cache stores the schema object it compiled alongside the
validator and only reuses it on an identity match —
_absorb_tool_listingassigns afreshly parsed schema object on every listing, so a server that changes a tool's output
schema forces a rebuild rather than being served the stale validator. A failed
check_schema()is never cached, so an invalid schema still raisesRuntimeError("Invalid schema for tool ...")on every call, as before. Validation errorsstill go through
best_match, which is whatjsonschema.validate()used internally, soerror messages are identical. The
jsonschemaimport stays inside the method to keep it(and its
attrs/referencingtree) off the cold-start path for clients that nevervalidate.
Tests cover the reuse (the regression guard) and the changed-schema rebuild (the
correctness guard), plus the invalid-schema path — which was previously marked
# pragma: no cover; those pragmas are now removed.AI assistance disclosure: this change was written with Claude Code. I reviewed the design
and the diff, ran the suite, coverage,
pyright, andrufflocally, and can speak to it.