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

> Source: <https://dev.to/ssukhpinder/mcp-x-mcp-header-validation-keep-bad-tool-schemas-out-of-toolslist-3j3d>
> Published: 2026-08-19 18:33:55+00:00

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.

The final [Streamable HTTP specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) 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"
    }
  }
}
```

A call with `"region": "us-west1"`

then carries:

```
Mcp-Param-Region: us-west1
```

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}";
```

Current [C# SDK v2 tool documentation](https://csharp.sdk.modelcontextprotocol.io/v2/concepts/tools/tools.html) 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.

The [final tool definition rules](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#x-mcp-header) 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.

The [sample draft PR](https://github.com/ssukhpinder/dev-to-code-samples/pull/16) 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);
```

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.

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!
