Skip to content

Align x-mcp-header implementation with SEP-2243 spec clarifications#1619

Open
tarekgh wants to merge 6 commits into
modelcontextprotocol:mainfrom
tarekgh:sep-2243-spec-compliance
Open

Align x-mcp-header implementation with SEP-2243 spec clarifications#1619
tarekgh wants to merge 6 commits into
modelcontextprotocol:mainfrom
tarekgh:sep-2243-spec-compliance

Conversation

@tarekgh
Copy link
Copy Markdown
Contributor

@tarekgh tarekgh commented Jun 1, 2026

Summary

Implements the spec clarifications from modelcontextprotocol/modelcontextprotocol#2772 to ensure the SDK fully complies with SEP-2243.

Changes

RFC 9110 tchar validation for header names

  • Replace loose ASCII range check with strict RFC 9110 Section 5.6.2 tchar character set
  • Use SearchValues on .NET 8+ for SIMD-accelerated matching
  • Use bitmap lookup (two ulong values covering ASCII 0-127) on netstandard2.0
  • Share validation logic between McpHeaderAttribute and McpHeaderExtractor via internal FindFirstNonTchar method

Remove "number" from allowed x-mcp-header types

  • Only "string", "integer", and "boolean" are permitted per the updated spec
  • Client-side: McpHeaderExtractor.ValidateToolSchema rejects tools with type "number" on x-mcp-header properties
  • Server-side: IsPrimitiveHeaderType no longer accepts float, double, or decimal for [McpHeader]

Base64 sentinel collision check

  • McpHeaderEncoder.RequiresBase64Encoding now detects values matching the =?base64?...?= pattern and forces base64 encoding to prevent round-trip confusion
  • DecodeValue changed from case-insensitive to case-sensitive prefix matching per spec requirement that sentinel markers must appear exactly as shown (lowercase)

Nested property support

  • Client-side header extraction and validation now recursively traverse nested "properties" objects at any depth, per the spec allowance for x-mcp-header at any nesting depth
  • Server-side StreamableHttpHandler adds recursive ValidateCustomParamHeadersFromProperties for header/body comparison on nested properties

Tests

28 new tests added:

  • 14 tchar validation tests (McpHeaderAttributeTests)
  • 7 sentinel collision tests (McpHeaderEncoderTests)
  • 7 integration tests for number rejection, nested properties, and tchar enforcement (McpHeaderExtractorValidationTests, new file)

All existing tests pass across net8.0, net9.0, net10.0, and net472.

- Enforce RFC 9110 tchar validation for header names using SearchValues on
  .NET 8+ and bitmap lookup on netstandard2.0
- Remove 'number' from allowed x-mcp-header types (only string, integer,
  boolean are permitted); reject float/double/decimal in [McpHeader]
- Add base64 sentinel collision check in McpHeaderEncoder.RequiresBase64Encoding
  so literal values resembling =?base64?...?= are encoded to avoid confusion
- Fix DecodeValue to use case-sensitive prefix matching per spec requirement
  that sentinel markers must appear exactly as shown (lowercase)
- Support nested property traversal for x-mcp-header validation and extraction
  on both client and server sides
- Share tchar validation logic between McpHeaderAttribute and McpHeaderExtractor
  via internal FindFirstNonTchar method
- Add recursive ValidateCustomParamHeadersFromProperties in StreamableHttpHandler
  for server-side nested property header/body comparison
- Add 28 new tests covering tchar validation, sentinel collision encoding,
  number type rejection, nested property handling, and case-sensitive decoding
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the SDK’s SEP-2243 x-mcp-header handling to match the latest spec clarifications, tightening header-name validation, restricting allowed header types, hardening Base64 sentinel parsing, and extending validation/extraction to nested schema properties.

Changes:

  • Enforce RFC 9110 tchar validation for x-mcp-header / [McpHeader] names and share that logic across client/server code.
  • Disallow "number" / floating-point header types for x-mcp-header (client-side schema rejection; server-side attribute/type checks).
  • Make Base64 sentinel decoding case-sensitive and add “sentinel collision” encoding safeguards; add nested-schema traversal for x-mcp-header.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/ModelContextProtocol.Tests/Server/McpHeaderAttributeTests.cs Expands test coverage for RFC 9110 tchar header-name validation.
tests/ModelContextProtocol.Tests/Client/McpHeaderExtractorValidationTests.cs Adds integration tests for number rejection, nested properties, and tchar enforcement.
tests/ModelContextProtocol.Tests/Client/McpHeaderEncoderTests.cs Adds sentinel-collision tests and asserts case-sensitive sentinel decoding.
src/ModelContextProtocol.Core/Server/McpHeaderAttribute.cs Switches header-name validation to RFC 9110 tchar via shared helper.
src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs Tightens [McpHeader] supported types to integer/string/boolean (removes float/double/decimal).
src/ModelContextProtocol.Core/Protocol/McpHeaderEncoder.cs Enforces case-sensitive sentinel parsing and prevents sentinel-collision ambiguity.
src/ModelContextProtocol.Core/Client/McpHeaderExtractor.cs Adds nested property traversal and stricter schema validation for x-mcp-header.
src/ModelContextProtocol.Core/Client/McpClient.Methods.cs Updates inline comments around x-mcp-header validation behavior.
src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Adds recursive server-side validation for nested x-mcp-header properties.

Comment thread src/ModelContextProtocol.Core/Client/McpClient.Methods.cs Outdated
Comment thread src/ModelContextProtocol.Core/Client/McpHeaderExtractor.cs Outdated
Comment thread tests/ModelContextProtocol.Tests/Client/McpHeaderEncoderTests.cs
- Reject x-mcp-header parameters whose JSON Schema type is a disallowed
  or malformed value, including non-string/union types. A union array is
  accepted only when it contains at least one of string/integer/boolean
  (with optional "null"); an absent type is still treated as unknown and
  allowed to avoid excluding valid enum/const/$ref schemas.
- Reword the rejection message: "number" is a JSON primitive but is not a
  permitted header type, so report it as an unsupported type rather than
  non-primitive.
- Clarify the ListToolsAsync comment to note that the client validates
  x-mcp-header annotations on all transports, while only Streamable HTTP
  is required to by SEP-2243.
- Drop the unused parameter from EncodeValue_NonSentinelPattern_NotBase64Encoded.
- Add tests for nullable union, disallowed union, null-only union, and
  missing-type schemas.
Copy link
Copy Markdown
Contributor

@halter73 halter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had AI Compare the state of this PR to the changes in modelcontextprotocol/modelcontextprotocol#2772, and it came up with this feedback. I looked over the four comments, and they make sense to me, but let me know if there's anything off.

Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
Comment thread src/ModelContextProtocol.Core/Protocol/McpHeaderEncoder.cs
Comment thread src/ModelContextProtocol.Core/Client/McpHeaderExtractor.cs
Comment thread src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs Outdated
Tarek Mahmoud Sayed added 4 commits June 4, 2026 11:38
Align x-mcp-header integer handling with the merged spec clarifications:

- Server (StreamableHttpHandler.ValuesMatch): compare integer header and
  body values numerically with exact precision, enforce the JavaScript safe
  integer range (-2^53+1 to 2^53-1), and reject out-of-range values with a
  distinct error. Detect integer union types (e.g. ["integer", "null"]) and
  accept decimal and exponent forms (42.0, 4.2e1). Removed the dead "number"
  branch and the tolerance-based double comparison.
- Client (McpHeaderExtractor): emit integer parameters in canonical decimal
  form (42.0 becomes "42") and reject non-whole or out-of-range values before
  sending.
- Server tool guard (IsPrimitiveHeaderType): drop ulong, whose domain exceeds
  the signed safe range.
- McpHeaderEncoder: remove the unused double overload and float/double/decimal
  conversions now that the number type is disallowed, and update docs.

Added tests covering canonical emission, safe-range and exponent rejection,
union integer matching, and ulong rejection at tool creation.
Replace the hand-rolled LooksLikeNumericLiteral scanner with a
double.TryParse gate. When a header/body value is a well-formed numeric
literal too large for decimal, double can still parse it, which lets us
flag it as out of the safe integer range. double is only used as a
yes/no numeric gate here; its loss of integer precision past 2^53 does
not matter because every in-range value is handled by the decimal parse.

This code only ships in ModelContextProtocol.AspNetCore (net8/9/10), so
the modern overflow-to-infinity parsing behavior is guaranteed.
Use long.TryParse for safe-integer validation on both the client
(McpHeaderExtractor) and server (StreamableHttpHandler) instead of
decimal. decimal silently rounds high-precision values, so non-integers
such as 42.0000000000000000000000000001 and 1e-100000 were accepted as
whole integers. long.TryParse inspects the actual digits (rejecting
non-integers without rounding) and fails fast on overflow (so a literal
such as 1e1000000 cannot allocate a large number).

On the server, classify each value as SafeInteger, OutOfRange,
NonInteger, or NotNumeric so a numeric-but-non-integer value for an
integer-typed parameter is rejected rather than slipping through the
ordinal comparison when the header and body strings are identical.

Decode base64-wrapped header values with a strict UTF-8 decoder so
malformed byte sequences are rejected instead of being replaced with
U+FFFD.

Remove the unused IsValidTcharString helper, and fix a numeric
equivalence test that passed the body value as a double (which
stringified 42.0 back to 42) so the decimal and exponent body forms are
actually exercised.

Add tests for non-integer rejection, high-precision fraction rejection,
canonical exponent emission, and invalid-UTF-8 base64 decoding.
@tarekgh tarekgh force-pushed the sep-2243-spec-compliance branch from 0da4304 to 0be3778 Compare June 4, 2026 21:18
@tarekgh
Copy link
Copy Markdown
Contributor Author

tarekgh commented Jun 4, 2026

@halter73 thanks for submitted feedback. I believe I have address them all. Let me know if you still have any more feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants