Reject backend tools with invalid x-mcp-header - #6013
Merged
Conversation
JAORMX
requested review from
ChrisJBurns,
amirejaz,
blkt,
jerm-dro,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
July 27, 2026 13:03
5 tasks
rdimitrov
previously approved these changes
Jul 27, 2026
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6013 +/- ##
==========================================
+ Coverage 72.10% 72.14% +0.03%
==========================================
Files 720 721 +1
Lines 74745 74960 +215
==========================================
+ Hits 53898 54078 +180
- Misses 16991 17023 +32
- Partials 3856 3859 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
SEP-2243 lets an MCP server designate individual tool parameters for mirroring into HTTP request headers, via an x-mcp-header annotation in the tool's inputSchema. The extension constrains the annotation's value, and requires a Streamable HTTP client to reject any tool definition that violates those constraints. vMCP is its backends' client and had no such validation: nothing in the repo matched x-mcp-header at all. Left unchecked, an invalid annotation is not contained to the tool that carries it. vMCP republishes aggregated backend schemas to its own downstream clients, so a conformant downstream client would reject the entire aggregated tools/list, taking every other backend's tools down with it. A CRLF in an annotation is worse than cosmetic once mirroring lands, since the value becomes an outgoing header name. Validate at newCapabilityListFromMCP -- the single ingestion seam the Legacy and Modern paths share -- and drop only the offending tool, which is the narrower failure. The annotation's vocabulary and constraint checks live in one place, pkg/mcp, so the ingestion check and the call-time header derivation that follows cannot drift in their reading of the spec. The traversal is depth-capped: annotations are legal at any nesting depth and a backend can advertise any tool list it likes, so an unbounded recursive walk would be a stack-exhaustion vector. Refs #6002
SEP-2243 lets a server designate tool parameters, via x-mcp-header in its
inputSchema, whose values it also requires as Mcp-Param-{name} HTTP
headers -- and a server that designated a parameter and does not receive
its header rejects the call with -32020. vMCP never sent them, so any
Modern backend annotating a tool parameter was uncallable through vMCP,
surfacing only as a generic backend failure.
Deriving the headers needs the tool's inputSchema, which BackendTarget
does not carry and the routing table does not hold. The aggregated view in
the core does hold it, and the core is already on the tools/call path, so
the headers are derived there and passed explicitly to CallTool. The
alternative -- caching schemas inside the backend client -- was rejected:
it would add a staleness window of exactly the kind #5992 documents for
the revision cache, to avoid a lookup the core already has in hand.
Values come from the caller and are therefore untrusted. A control
character is refused rather than stripped, since the value would otherwise
forge headers on vMCP's outgoing request, and refusing fails the call
closed instead of letting the backend answer -32020 for a reason the
caller cannot see. Mirrored headers are also set before the protocol
headers, so no derived entry can displace Mcp-Method, Mcp-Name, or
MCP-Protocol-Version.
Legacy backends are untouched: SEP-2243 belongs to 2026-07-28, and a
Legacy backend neither expects the headers nor rejects their absence.
The legacy pkg/vmcp/server/adapter path holds no schema and so does not
mirror; that limitation is recorded at the call site. The live Serve path
routes tools/call through the core.
Refs #6002
JAORMX
force-pushed
the
validate-xmcp-header-annotations
branch
from
July 27, 2026 13:34
7937889 to
ad85feb
Compare
This was referenced Jul 27, 2026
rdimitrov
approved these changes
Jul 27, 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.
Summary
SEP-2243 ("HTTP standardization") lets an MCP server designate individual tool parameters for mirroring into HTTP request headers, via an
x-mcp-headerannotation inside the parameter's schema in the tool'sinputSchema. The extension constrains what that annotation's value may be, and requires a Streamable HTTP client to reject any tool definition that violates those constraints. vMCP is its backends' client and had no such validation —x-mcp-headerhad zero matches anywhere in the repo.Why this is not contained to the tool that carries it. vMCP republishes aggregated backend schemas to its own downstream clients. An invalid annotation forwarded in the aggregated
tools/listwould make a conformant downstream client reject the entire list, taking every other backend's tools down with it. Dropping the one offending tool is the narrower failure. Separately, a CRLF in an annotation stops being cosmetic the moment mirroring lands (the stacked PR), because the value becomes an outgoing header name.newCapabilityListFromMCP— the single ingestion seam the Legacy and Modern paths share, so neither can regress independently.pkg/mcp/xmcpheader.go), so this ingestion check and the call-time header derivation that follows cannot drift in their reading of the spec. This is deliberately the mistake called out for the reserved_metakeys in Strip reserved io.modelcontextprotocol/* keys from backend response _meta (shared Legacy+Modern helper) #5986 ("a third independent copy").Constraints enforced, per SEP-2243: non-empty; valid HTTP field-name token (RFC 9110
tchar, which also excludes control characters and whitespace); case-insensitively unique across the wholeinputSchema(HTTP field names are case-insensitive, so two spellings collide on the wire); and permitted only on a parameter declaringstring,integer, orboolean— notably notnumber, which the SEP excludes because a float has no canonical wire spelling.Type of change
Test plan
task test) — full suite; the only failures arepkg/apiandpkg/secrets/keyring, both environmental (no available runtime found, keyring unavailable) and verified identical onmaintask lint-fix) — 0 issuespkg/mcp/xmcpheader_test.gocovers each constraint in both directions, plus the traversal cases that make the walk non-trivial: nesting inside object properties, arrayitems, andoneOf/anyOf/allOfbranches (those carry real schemas in this repo's tool sets — #5976 fixed ingestion dropping them — so an annotation inside a branch would otherwise go unvalidated).pkg/vmcp/client/xmcpheader_ingestion_test.goasserts the rejection at the seam.Manual: ran
go vet ./...as a separate gate. Worth knowing for other PRs:task test's formatter prints only failing packages and silently swallows build failures, so a package whose tests do not compile reads as green. That is how I initially misread this branch.go vet ./...catches it.API Compatibility
v1beta1API.No operator API surface is touched — no CRD, no
api/v1*type.Does this introduce a user-facing change?
Yes, though it is unreachable in practice today. A backend tool whose
x-mcp-headerannotation violates SEP-2243 is no longer advertised through vMCP, and a WARN naming the backend, the tool, and the violation is logged. No tool in any catalog currently uses the annotation (zero matches repo-wide), so no existing deployment changes behaviour.Special notes for reviewers
The judgement call worth challenging is treating a missing or non-string
typeon an annotated parameter as a violation. SEP-2243 permits the annotation only on primitive parameters, and an undeclared type is not a declared primitive; mirroring also cannot serialize a value whose type it does not know. So this rejects rather than guessing a spelling. It is the strict reading and it is documented as such atParamHeaders. Note the constraint applies only to parameters that are annotated — an unannotated parameter with notypeis untouched, so the strictness cannot leak onto ordinary tools.This is deliberately narrower than #6002 item 2, which also asks for the egress mirroring. That half needs the
inputSchemaattools/calltime and so changesBackendClient.CallTool; it is the stacked follow-up rather than being crammed in here.Also note #6002 item 1 is already implemented on
main(it landed in #5993 itself) — details here.Refs #6002
Generated with Claude Code