Skip to content

find_invalid_x_mcp_header: never repr a non-string annotation value#2989

Merged
Kludex merged 2 commits into
mainfrom
x-mcp-header-validator-followups
Jun 26, 2026
Merged

find_invalid_x_mcp_header: never repr a non-string annotation value#2989
Kludex merged 2 commits into
mainfrom
x-mcp-header-validator-followups

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

Follow-up to #2974, on find_invalid_x_mcp_header: make its reason strings total over any inputSchema value, and pin the schema-position walk's applicator keywords independently of the implementation.

Motivation and Context

find_invalid_x_mcp_header is meant to return a reason string or None for any decoded inputSchema, 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 !r either way. repr() of an arbitrary schema value is not total: an int with more digits than sys.get_int_max_str_digits() (default 4300) raises ValueError. That propagates out of ClientSession.list_tools() instead of the offending tool being dropped, and it is reachable from the public API: the in-memory Client(server) path has no JSON decode step in front of the client-side filter, so the value reaches it as a live Python object and await 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 (where repr is total). No guard and no helper — a non-string value structurally never reaches a repr — and the messages get more precise: x-mcp-header must be a string, not int rather than pretending an int is a malformed token.

Separately, the reject test for "annotation under a non-properties applicator" hand-picked 6 of the walk's 19 applicator keywords, so the other 13 — including the chain-breakers the spec names anyOf / not / then / else, plus additionalProperties — 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 (enum was the only instance-data keyword named in the source comment with no lookalike test).

How Has This Been Tested?

  • Two new reject params (oversized-int-header, oversized-int-type) fail on main with ValueError: Exceeds the limit (4300 digits) for integer string conversion — one at each message site — and pass here (checked by stashing the src/ change and re-running them).
  • End to end at the public surface: a lowlevel server advertising those schemas, driven through Client(server).list_tools(). On main that call raises ValueError out of inbound.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-cover clean. pyright: 0 errors. No new pragma / 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The walk itself is untouched. A deeply nested container value (the other non-total repr input) 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 oversized int is the one shape with a live path, and it is the one the regression tests pin.

AI Disclaimer

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.
@maxisbey maxisbey marked this pull request as ready for review June 26, 2026 09:42

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

@Kludex Kludex enabled auto-merge (squash) June 26, 2026 09:52
@Kludex Kludex merged commit 9dc8c5f into main Jun 26, 2026
31 of 32 checks passed
@Kludex Kludex deleted the x-mcp-header-validator-followups branch June 26, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants