DEV Community

Sukhpinder Singh
Sukhpinder Singh

Posted on

MCP x-mcp-header Validation: Keep Bad Tool Schemas Out of tools/list

MCP x-mcp-header validation is easy to miss because the annotation looks like ordinary JSON Schema metadata. On the 2026-07-28 Streamable HTTP transport, it is a wire contract: the client copies selected tool arguments into Mcp-Param-* headers, intermediaries can act on those headers, and the server checks them against the JSON-RPC body.

I treat that contract as something to test before a tool reaches tools/list. A bad suffix, an unsupported type, or an unreachable annotation makes the whole tool definition invalid. Silently accepting it only moves the failure to a harder place to diagnose.

Why the same value travels twice

The final Streamable HTTP specification mirrors request metadata into HTTP headers so a load balancer, gateway, or WAF does not need to parse JSON-RPC. A server can add x-mcp-header to a tool property:

{
  "type": "object",
  "properties": {
    "region": {
      "type": "string",
      "x-mcp-header": "Region"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A call with "region": "us-west1" then carries:

Mcp-Param-Region: us-west1
Enter fullscreen mode Exit fullscreen mode

The official C# SDK can generate that schema from a parameter attribute:

[McpServerTool]
public static string ExecuteSql(
    [McpHeader("Region")] string region,
    string query) => $"Queued for {region}";
Enter fullscreen mode Exit fullscreen mode

Current C# SDK v2 tool documentation describes both schema generation and automatic header projection. The feature is on the stable v2 line; it is not necessary to pin an earlier preview or release candidate.

MCP x-mcp-header validation rules

The final tool definition rules are deliberately narrow.

The annotation value must be a non-empty HTTP field-name token and must be unique without regard to case. Region and region therefore collide. Control characters, spaces, and separators such as a colon are not valid suffix characters.

Only string, integer, and boolean properties can be mirrored. JSON Schema number is excluded, and integer values must stay between -(2^53 - 1) and 2^53 - 1 so every conforming implementation can represent the value exactly.

Reachability is the rule most likely to surprise me. An annotated property can be nested, but the path from the schema root must pass only through properties. An annotation below items, $ref, oneOf, allOf, if, or another composition or conditional keyword is invalid. A Streamable HTTP client must exclude an invalid tool from the returned tools/list result and should log the reason. A stdio client may ignore these annotations because it has no HTTP headers to project.

Values have their own encoding rules. Plain visible ASCII can travel as-is. Non-ASCII text, control characters, leading or trailing whitespace, and strings that already look like the =?base64?...?= sentinel must be UTF-8/Base64 encoded inside that sentinel. Boolean values become lowercase true or false; mathematically integral JSON forms such as 42.0 normalize to decimal 42. If an optional argument is absent or explicitly null, the client omits its header.

Make schema drift fail offline

The sample draft PR turns those requirements into a dependency-free .NET 10 executable. It scans the relevant JSON Schema subschema locations, ignores annotation-shaped literal data under keywords such as default, records valid property paths, and fails malformed schemas before any network request.

using JsonDocument schema = JsonDocument.Parse(schemaJson);
using JsonDocument arguments = JsonDocument.Parse(argumentJson);

var headers = McpHeaderProjector.Project(
    schema.RootElement,
    arguments.RootElement);
Enter fullscreen mode Exit fullscreen mode

The deterministic verifier covers twelve cases, including nested primitive properties, absent and null arguments, non-ASCII and sentinel encoding, case-insensitive duplicates, the forbidden number type, annotations below items and oneOf, literal example data, invalid HTTP tokens, integral exponent notation, and both safe-integer boundaries.

I like this style of test because it catches two different regressions. A server refactor can accidentally move an annotation behind a $ref; a client refactor can stop encoding a padded or Unicode value. Both changes compile, but both break the transport contract.

At runtime, the server has another job. It must decode recognized Mcp-Param-* values and compare them with the body. A missing, malformed, or different value is HTTP 400 with JSON-RPC error -32020 (HeaderMismatch). When that mismatch suggests a stale schema, the client should refresh tools/list before retrying with the new definition.

Limits: routing metadata is not authorization

These headers help infrastructure route, meter, and observe requests. They do not prove that a caller may use the region, tenant, or resource named in the value. An attacker who can choose the body can usually choose the matching header too, so the application still needs normal authentication and authorization checks. A gateway enforcing policy on mirrored headers should reject an absent or older protocol version, where header/body validation is not guaranteed.

I would never mark a password, API key, access token, or personally identifiable value with x-mcp-header. Base64 is only an encoding, and headers are visible to intermediaries and often copied into logs.

The sample is also a focused conformance fixture, not a full JSON Schema 2020-12 engine or a replacement for the official SDK. Its value is keeping the sharp transport rules visible in tests. For production, use a current SDK, validate header/body equality on the server, and keep authorization tied to the authenticated principal.

Which malformed schema or encoding edge case would you add to this regression set?

Happy coding!

Top comments (1)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

I’d add a middlebox conformance layer, because the client and server can both be correct while a proxy changes the wire image. Test duplicate Mcp-Param-* fields, case variants, comma folding, reordered duplicates, maximum header size, Unicode/Base64 sentinel boundaries, and a gateway that strips unknown headers. The server should reject ambiguity rather than choosing “first” or “last.” I’d also bind the validated tool-schema digest/protocol version to the request trace so a HeaderMismatch can distinguish stale discovery from transport mutation. For routing, include mirrored fields in the gateway cache/partition key only after header/body equality succeeds; otherwise a stale or poisoned cache can cross the very region/tenant boundary the header was meant to express. And agreed: derive authorization scope from the authenticated principal, never from mirrored arguments.