MCP 2026-07-28 (4/11): x-mcp-header — Mcp-Param-* headers, tool validation, HeaderMismatch retry - #227
Open
simonx1 wants to merge 7 commits into
Open
MCP 2026-07-28 (4/11): x-mcp-header — Mcp-Param-* headers, tool validation, HeaderMismatch retry#227simonx1 wants to merge 7 commits into
simonx1 wants to merge 7 commits into
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
simonx1
force-pushed
the
mcp-2026/x-mcp-header
branch
from
September 1, 2026 19:35
dfce337 to
48fedad
Compare
2 tasks
simonx1
force-pushed
the
mcp-2026/streamable-http-modern
branch
from
September 1, 2026 19:55
f5bc6d6 to
39ae5ba
Compare
simonx1
force-pushed
the
mcp-2026/x-mcp-header
branch
2 times, most recently
from
September 1, 2026 19:58
bc796b9 to
d516b8c
Compare
simonx1
force-pushed
the
mcp-2026/streamable-http-modern
branch
from
September 5, 2026 00:19
0e00bfa to
725e88f
Compare
simonx1
force-pushed
the
mcp-2026/x-mcp-header
branch
from
September 5, 2026 00:22
4efbb6d to
422b5c8
Compare
MCP 2026-07-28 Streamable HTTP "Custom Headers from Tool Parameters":
- MCPClient::HeaderParams validates x-mcp-header annotations (non-empty
HTTP field-name token, case-insensitively unique, only on
string/integer/boolean properties, only statically reachable through
properties keys) and extracts Mcp-Param-{name} values with the spec's
type conversion and Base64 sentinel encoding; unsafe integers and
non-primitive values fail the call locally.
- Modern HTTP transports exclude tools with invalid annotations from
tools/list (warning with the tool name), attach the headers on
tools/call (fetching the tool list on demand), and after a HeaderMismatch
refresh tools/list and retry once.
- stdio ignores the annotation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
simonx1
force-pushed
the
mcp-2026/streamable-http-modern
branch
from
September 5, 2026 12:32
5817253 to
8f28115
Compare
Codex + grok findings: - Only schema-bearing keywords are walked: instance data (default, examples, enum, const) inside inputSchema is never an annotation. - Mirroring is a MUST: a call whose tool list cannot be fetched fails instead of going out without Mcp-Param headers. - Transport list caches follow list_changed notifications; a HeaderMismatch refresh is announced upward as tools/list_changed so Client's cache and the in-flight structured-content validation use the refreshed definition; a generation counter stops a stale concurrent fetch from overwriting a refreshed list; a failed refresh re-raises the HeaderMismatch. - Peer-controlled text in the new log lines (error messages, property names) is escaped and capped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
… list fetches Grok + codex second-round findings on x-mcp-header: - Integral Floats (42.0) encode as decimal integers; a single-element type array names a primitive type; contentSchema and draft-07 dependencies are walked for misplaced annotations. - A list fetch that was invalidated while in flight is never cached, not even into an empty cache, and is re-fetched (bounded); tool names in the rejection warning are capped like the rest of the peer text. - The client re-resolves the tool after a call only when the transport's tool-list generation changed during the call, independent of its own cache state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
Codex verification findings on x-mcp-header: - A result was re-resolved against the transport's *current* tool list whenever the list generation moved during the call, so a tools/list_changed that merely raced the call (on the response stream or from another thread) decided what the result was validated against -- inventing ValidationErrors in :strict mode, and letting a looser replacement pass a result the answering definition forbade. The transport now records the definition each tools/call request derives its Mcp-Param-* headers from (MCPClient::CalledToolDefinition), and the client validates against that one: the retry's refreshed definition after a HeaderMismatch, and otherwise the definition in force when the request was sent. A call host code nests inside another -- from a notification listener or a handler for a server-initiated request -- records into a slot of its own. - Computed parameter headers were applied after the configured ones and nothing cleared a configured header the extraction omitted, so a configured Mcp-Param-Region survived a call that omitted `region` and no tools/list refresh could correct it. On a modern session the Mcp-Param-* namespace is now cleared before the computed headers are attached, case-insensitively; legacy sessions, where it has no protocol meaning, keep sending it. New examples pin both, plus what the earlier ones left unpinned: the mirrored headers agree with the arguments actually serialized into the body on both POSTs of a HeaderMismatch recovery, the request _meta survives the re-send, one listed annotated tool exercises the standard and parameter headers together, and the safe-integer range is checked at both ends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
The four methods that implement "Custom Headers from Tool Parameters" — deriving the Mcp-Param-* headers, clearing that reserved namespace, fetching the list they are derived from, and the refresh-and-retry-once recovery — are a self-contained piece of this transport. They move to HttpTransportBase::ParamHeaders, which keeps the module under the length cop once the branch above this one adds to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
…s on plain HTTP Review round 3 found two defects in the x-mcp-header work. The HeaderMismatch refresh-and-retry ran inside a `rescue` clause, so an exception its re-send raised bypassed the sibling clause that handles a broken response stream — and vice versa. Both orderings were reproducible: a HeaderMismatch retry whose stream closed surfaced as ResponseStreamClosedError instead of being re-issued, and a re-issue that was rejected for its headers surfaced as HeaderMismatchError without a tools/list refresh. Both re-sends now `retry` one guarded block, so each recovery still covers the other's re-send. Each is spent once, so a call is sent at most three times. The plain HTTP dispatcher invoked host code without opening a CalledToolDefinition slot of its own, unlike the Streamable HTTP one. A tools/call nested by a progress listener replaced the outer call's slot, so the answering definition was lost and :strict validation ran against the pre-refresh schema. It now wraps the dispatch the same way. Coverage, driven by four mutations that survived the previous examples: HeaderMismatch recovery is now pinned as modern-only, argument encoding is pinned on the wire (sentinel-shaped values, empty strings, control characters, false, zero, integral floats), modern structured-output rejection has an example of its own, and the two concurrency examples use queue barriers and assert the stale fetch's own return value rather than only the cache. Also added: the recovery, local-rejection, error-detail and list_changed-invalidation examples run against both HTTP transports; paginated and shrinking tool lists; more schema-traversal keywords; and positive assertions on the warnings that were only checked for absence. The client-level re-resolution example now changes a real annotation instead of stubbing HeaderParams.headers_for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
A modern call_tool reads tools/list first, to derive the call's Mcp-Param-* headers. A fixture that broke every request therefore lost the list, and the loss surfaced before the call the example is about ever went out. Only tools/call breaks now, so the example still pins what it names: one re-issue, then the loss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7
simonx1
force-pushed
the
mcp-2026/x-mcp-header
branch
from
September 5, 2026 12:39
1342c25 to
3f8903c
Compare
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.
Summary
Fourth PR of the MCP 2026-07-28 series (stacked on #226). Implements Custom Headers from Tool Parameters (SEP-2243).
MCPClient::HeaderParams(new) —validate_schemaenforces every constraint onx-mcp-headervalues (non-empty,1*tchar, no CR/LF, case-insensitively unique, only oninteger/string/booleanproperties, only statically reachable via a chain ofpropertieskeys — never throughitems,oneOf/anyOf/allOf/not,if/then/else,$defs,$refor the root).annotationslists the reachable annotated paths;headers_forreads the argument at each exact path (String or Symbol keys), omits absent/nullvalues, converts per type (decimal integers within the IEEE754 safe range, lowercase booleans, strings with the=?base64?…?=sentinel when not header-safe) and raisesValidationErrorfor anything that cannot be mirrored.tools/listwith a warning naming the tool and the reason ("Rejection means the client MUST exclude the invalid tool"), attachMcp-Param-{name}headers totools/call(fetching the tool list on demand when a tool is called first), and on a-32020 HeaderMismatchrefreshtools/listand retry once with recomputed headers. Legacy sessions are untouched; stdio ignores the annotation as the spec allows.Test plan
bundle exec rspec— 1869 examples, 0 failures (28 new inx_mcp_header_2026_spec.rb)bundle exec rubocop— clean🤖 Generated with Claude Code
https://claude.ai/code/session_01MoErzDypnq7hhuFBtueML7