Align x-mcp-header implementation with SEP-2243 spec clarifications#1619
Align x-mcp-header implementation with SEP-2243 spec clarifications#1619tarekgh wants to merge 6 commits into
Conversation
- 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
There was a problem hiding this comment.
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
tcharvalidation forx-mcp-header/[McpHeader]names and share that logic across client/server code. - Disallow
"number"/ floating-point header types forx-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. |
- 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.
halter73
left a comment
There was a problem hiding this comment.
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.
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.
0da4304 to
0be3778
Compare
|
@halter73 thanks for submitted feedback. I believe I have address them all. Let me know if you still have any more feedback. |
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
Remove "number" from allowed x-mcp-header types
Base64 sentinel collision check
Nested property support
Tests
28 new tests added:
All existing tests pass across net8.0, net9.0, net10.0, and net472.