feat: SEP-2243 custom half — Mcp-Param header codec, client mirroring, server validation

Merged
FE
4mo ago
+1956-46

SEP-2243 Mcp-Param-* headers — codec, client-side mirroring, and server-side validation (protocol revision 2026-07-28).

Motivation and Context

SEP-2243 lets a server designate selected tool parameters (via an x-mcp-header annotation in the tool's inputSchema) to be mirrored into Mcp-Param-{Name} HTTP request headers, so HTTP intermediaries can route, filter, or audit on them without parsing the JSON-RPC body. The spec defines a value encoding (with a =?base64?…?= sentinel for values that are not safe plain-ASCII field values), a 5-step client algorithm, a constraint table for x-mcp-header declarations, and a server-behavior table that requires 400 Bad Request / JSON-RPC -32001 (HeaderMismatch) when the headers and body disagree.

This PR implements all three halves on the modern (2026-07-28) Streamable HTTP path:

  • packages/core — the codec module: scan an inputSchema for x-mcp-header declarations, encode/decode the sentinel, build the per-request header set, and validate a request's Mcp-Param-* headers against its body arguments.
  • packages/client — Client.callTool() mirrors designated arguments into Mcp-Param-{Name} headers (from the most recent tools/list result, or a caller-supplied toolDefinition); on a -32001 rejection it refreshes the definition cache once and retries. Client.listTools() excludes tool definitions whose declarations violate the spec's constraints, logging a warning. The Streamable HTTP transport sets Mcp-Name (sentinel-encoded) alongside Mcp-Method, surfaces an HTTP 400 carrying a JSON-RPC error response in-band as a ProtocolError, and skips the reserved standard/auth header names in the per-request TransportSendOptions.headers carrier.
  • packages/server — createMcpHandler validates Mcp-Param-* headers against the named tool's declarations before dispatch and rejects with 400 / -32001 on a missing/disagreeing/malformed header. McpServer.registerTool warns at registration time on an invalid declaration.

The legacy-era serving paths and the client's legacy-era callTool/listTools are unchanged.

How Has This Been Tested?

  • Codec: 39-row fixture corpus mirroring the spec's encoding-examples, constraint, and server-behavior tables (including the two manifest checks the published conformance suite leaves globally untested).
  • Client: mirroring from cached definitions and the toolDefinition escape hatch, paginated tools/list accumulation, _resetConnectionState cache-clear, era-parity (legacy callTool/listTools byte-untouched), the one-refresh-on--32001 retry, sentinel-encoded Mcp-Name, the reserved-header guard, and 400 in-band delivery.
  • Server: over-HTTP rejection corpus through createMcpHandler.
  • Conformance (published referee): http-custom-headers 18/0, http-invalid-tool-headers 11/0, http-custom-header-server-validation 9/0.
  • Full workspace test suite green, e2e matrix unchanged at 2544 + 205 xfail.

Breaking Changes

None on the 2025-era serving paths. On a 2026-07-28 Streamable HTTP connection: Client.listTools() excludes tool definitions whose x-mcp-header declarations violate the spec's constraints (logged); createMcpHandler rejects a tools/call whose Mcp-Param-* headers are missing for a present body value, malformed, or disagree with the body. Browser clients skip mirroring (dynamically named headers cannot be statically allow-listed for credentialed CORS), so calling an x-mcp-header tool with a non-null designated argument from a browser against a server that enforces SEP-2243 validation is a known limitation.

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

Additive options: CallToolRequestOptions.toolDefinition (escape hatch so mirroring runs without a prior tools/list) and TransportSendOptions.headers (per-request HTTP headers; the Streamable HTTP transport skips authorization/mcp-protocol-version/mcp-method/mcp-name/mcp-session-id/content-type; transports that share a single channel — stdio, in-memory — ignore it).

Consumes the same -32001 (HeaderMismatch) shape the inbound classifier already emits for the standard-header cross-checks; the new param-header-validation rung sits at order 8 in the inbound validation ladder, evaluated pre-dispatch against the resolved tool's schema.

Spec text: a type: "number" x-mcp-header is accepted alongside integer/string/boolean because the published conformance referee ships fixtures that require it; one constant (PERMITTED_X_MCP_HEADER_TYPES) flips this strict if upstream aligns.