find_invalid_x_mcp_header: never repr a non-string annotation value#2989
Merged
Conversation
The token and primitive-type checks each fused "the value is not a string" and "the string is invalid" into one branch whose message repred the value either way, and repr() of an arbitrary schema value is not total: an int with more digits than sys.get_int_max_str_digits() raises ValueError out of ClientSession.list_tools() instead of the tool being dropped. That is reachable from the public API via the in-memory Client(server) path, which has no JSON decode step in front of the client-side filter. Each check is now two checks with two messages, so a value only reaches an interpolation once proven str, and the wrong-type messages name the type instead of pretending the value is a malformed token. Also pin the schema-position walk applicator keywords independently of the implementation: the reject test hand-picked 6 of the 19, so the other 13 (including the spec-named anyOf/not/then/else and additionalProperties) could be dropped from the walk without a failing test. The cases are now a literal keyword -> shape table covering all 19, plus an equality test against the walk keyword sets so an added keyword must gain a literal case; the hand-picked six are subsumed.
Kludex
approved these changes
Jun 26, 2026
Kludex
approved these changes
Jun 26, 2026
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.
Follow-up to #2974, on
find_invalid_x_mcp_header: make its reason strings total over anyinputSchemavalue, and pin the schema-position walk's applicator keywords independently of the implementation.Motivation and Context
find_invalid_x_mcp_headeris meant to return a reason string orNonefor any decodedinputSchema, but two of its checks fused two different failures — "the value isn't a string" and "the string is invalid" — into one branch whose message interpolated the value with!reither way.repr()of an arbitrary schema value is not total: anintwith more digits thansys.get_int_max_str_digits()(default 4300) raisesValueError. That propagates out ofClientSession.list_tools()instead of the offending tool being dropped, and it is reachable from the public API: the in-memoryClient(server)path has no JSON decode step in front of the client-side filter, so the value reaches it as a live Python object andawait client.list_tools()raises under default interpreter settings.Each fused check is now two checks with two messages, so a value only reaches an interpolation once the preceding branch has proven it is a
str(whererepris total). No guard and no helper — a non-string value structurally never reaches arepr— and the messages get more precise:x-mcp-header must be a string, not intrather than pretending an int is a malformed token.Separately, the reject test for "annotation under a non-
propertiesapplicator" hand-picked 6 of the walk's 19 applicator keywords, so the other 13 — including the chain-breakers the spec namesanyOf/not/then/else, plusadditionalProperties— could be removed from the walk without any test failing. The cases are now a literal keyword → shape table covering all 19 (deliberately not derived from the implementation's keyword sets, so dropping a keyword fails its case rather than shrinking the parametrization), with one equality test against those sets so an added keyword must gain a literal case. The hand-picked six are subsumed; one accept-side symmetry gap is filled (enumwas the only instance-data keyword named in the source comment with no lookalike test).How Has This Been Tested?
oversized-int-header,oversized-int-type) fail onmainwithValueError: Exceeds the limit (4300 digits) for integer string conversion— one at each message site — and pass here (checked by stashing thesrc/change and re-running them).Client(server).list_tools(). Onmainthat call raisesValueErrorout ofinbound.py; here it returns, logs one warning per dropped tool, and keeps the valid one. Reason strings for string-valued annotations are unchanged../scripts/test: full suite, 100% coverage,strict-no-coverclean.pyright: 0 errors. No newpragma/type: ignore/noqa.Breaking Changes
None. Accept/reject decisions are identical for every input; only the reason-string text for non-string values changes (e.g.
(got {'not': 'valid'})becomes(... the type keyword is dict, not a string)), and that string is only ever logged.Types of changes
Checklist
Additional context
The walk itself is untouched. A deeply nested container value (the other non-total
reprinput) cannot reach this code from any transport — the in-memory server's own result serialization and the HTTP JSON parser both reject it first — so the oversizedintis the one shape with a live path, and it is the one the regression tests pin.AI Disclaimer