Skip to content

Implementation for SEP-2243 Http Standardization#1553

Open
mikekistler wants to merge 27 commits into
mainfrom
mdk/sep-2243-implementation
Open

Implementation for SEP-2243 Http Standardization#1553
mikekistler wants to merge 27 commits into
mainfrom
mdk/sep-2243-implementation

Conversation

@mikekistler
Copy link
Copy Markdown
Contributor

@mikekistler mikekistler commented May 1, 2026

Motivation and Context

Implements SEP-2243 (HTTP Header Standardization) for both client and server Streamable HTTP transports. This enables network infrastructure (load balancers, proxies, gateways) to make routing and filtering decisions based on MCP request metadata without parsing the JSON-RPC body.

Fixes #1541.

Changes

Protocol & Constants

  • Add McpHttpHeaders static class with standard header name constants (Mcp-Method, Mcp-Name, Mcp-Param-*, etc.)
  • Add DRAFT-2026-v1 as a supported protocol version for header enforcement
  • Add McpErrorCode.HeaderMismatch (-32001) for header validation failures

Client-side (ModelContextProtocol.Core)

  • StreamableHttpClientSessionTransport emits Mcp-Method, Mcp-Name, and Mcp-Param-* headers on all outgoing POST requests
  • McpHeaderEncoder handles value encoding (plain ASCII pass-through, Base64 wrapping with =?base64?...?= sentinel for non-ASCII/control characters/leading-trailing whitespace)
  • McpHeaderExtractor (internal) reads x-mcp-header annotations from tool schemas and adds corresponding Mcp-Param-{Name} headers
  • Client validates tool schemas from tools/list and rejects tools with invalid x-mcp-header annotations (empty names, invalid characters, case-insensitive duplicates, non-primitive types)

Server-side (ModelContextProtocol.AspNetCore)

  • StreamableHttpHandler validates incoming Mcp-Method and Mcp-Name headers match the JSON-RPC body (enforced only for DRAFT-2026-v1+)
  • Validates Mcp-Param-* custom headers against tool schema x-mcp-header annotations and body argument values
  • Returns 400 Bad Request with HeaderMismatch (-32001) JSON-RPC error on validation failure

Tool Authoring (ModelContextProtocol.Core)

  • [McpHeader("Name")] attribute marks tool parameters for header mirroring
  • AIFunctionMcpServerTool emits x-mcp-header extension in the tool's JSON schema for annotated parameters
  • Validates at registration time: primitive types only, case-insensitive unique names, valid ASCII characters

Protocol Version Gating

Header validation is only enforced when the client's MCP-Protocol-Version header is DRAFT-2026-v1 or later. Clients on older protocol versions are unaffected. The client always sends headers on Streamable HTTP transport regardless of version (additive, non-breaking).

New Public API Surface

ModelContextProtocol.Protocol.McpHttpHeaders (static class)

public static class McpHttpHeaders
{
    // Version gating
    public static readonly string MinVersionForStandardHeaders; // "DRAFT-2026-v1"
    public static bool SupportsStandardHeaders(string? protocolVersion);

    // Standard header name constants
    public const string SessionId = "Mcp-Session-Id";
    public const string ProtocolVersion = "MCP-Protocol-Version";
    public const string LastEventId = "Last-Event-ID";
    public const string Method = "Mcp-Method";
    public const string Name = "Mcp-Name";
    public const string ParamPrefix = "Mcp-Param-";
}

ModelContextProtocol.Protocol.McpHeaderEncoder (static class)

public static class McpHeaderEncoder
{
    // Typed overloads for compile-time safety
    public static string? EncodeValue(string? value);
    public static string EncodeValue(bool value);
    public static string EncodeValue(long value);
    public static string EncodeValue(double value);

    // General-purpose overload (supports all numeric types)
    public static string? EncodeValue(object? value);

    // Decoding (case-insensitive Base64 prefix detection)
    public static string? DecodeValue(string? headerValue);
}

ModelContextProtocol.Server.McpHeaderAttribute (attribute)

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public sealed class McpHeaderAttribute : Attribute
{
    public McpHeaderAttribute(string name);
    public string Name { get; }
}

ModelContextProtocol.McpErrorCode (enum addition)

public enum McpErrorCode
{
    HeaderMismatch = -32001,  // New
    // ... existing values unchanged
}

How Has This Been Tested?

The PR includes comprehensive tests covering:

  • Unit tests: McpHeaderEncoderTests — encode/decode round-trips, Base64 edge cases, type conversion, empty/null handling
  • Unit tests: McpHeaderAttribute validation and schema generation
  • Conformance tests: Sep2243HeaderTests — server-side header validation (method/name mismatch, missing headers, Base64 encoding, version gating, custom parameter headers)
  • Integration tests: End-to-end client→server header flow via in-memory transport

Conformance tests are aligned with conformance repo PR #259.

All existing tests continue to pass (~8,000+ across net8.0, net9.0, net10.0, net472).

Breaking Changes

No breaking changes.

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

mikekistler and others added 6 commits May 1, 2026 09:30
Address multiple specification compliance issues identified during review:

Source fixes:
- Fix tab (0x09) encoding: tabs now trigger Base64 encoding per spec
- Add client-side tool schema validation: tools with invalid x-mcp-header
  annotations (non-object schemas, missing properties) are filtered out
- Remove server empty-header early-return that skipped validation
- Fix numeric precision loss: use GetRawText()/ToJsonString() instead of
  GetDouble()/GetValue<double>() for header value extraction
- Implement proper version gating: only DRAFT-2026-v1 requires header
  validation on server side (client always sends headers unconditionally)
- Add server-side invalid character validation for header values

Test additions:
- 22 new integration tests in Sep2243HeaderTests.cs covering encoding,
  validation, tool filtering, and end-to-end header scenarios
- 4 new server conformance tests for draft version header validation
- 2 new client conformance tests for tool filtering and header sending
- 2 new unit tests for tab encoding behavior
- 1 new theory for SupportsStandardHeaders version gating
- Fix ConformanceClient to handle toolCalls array format and add
  prompts/list + prompts/get calls for http-standard-headers scenario

Other:
- Add *.lscache to .gitignore
The http-standard-headers, http-custom-headers, and http-invalid-tool-headers
client scenarios and http-header-validation, http-custom-header-server-validation
server scenarios require conformance package >= 0.1.16 which is not yet
published to npm.

Add NodeHelpers.GetConformanceVersion() that runs 'conformance --version'
and NodeHelpers.IsConformanceVersionAtLeast() for runtime version checks.
Tests auto-skip when the harness is too old and auto-run once the package
is updated. Works with both npm-published and local file: references.
Tarek Mahmoud Sayed and others added 7 commits May 10, 2026 12:00
…rash

The Node.js conformance runner can crash during cleanup on Windows with a
libuv assertion (UV_HANDLE_CLOSING) that produces a non-zero exit code even
when all conformance checks passed. This change adds a fallback that parses
the 'Test Results:' summary in stdout to determine success when the process
exit code is non-zero.

Also updates package-lock.json with latest dependency versions.
McpHeaderEncoder is a shared utility used by both client-side (McpHeaderExtractor)
and server-side (StreamableHttpHandler) code. Placing it in the Client namespace
forced server code to reference ModelContextProtocol.Client, creating a misleading
dependency. Move it to ModelContextProtocol.Protocol alongside McpHttpHeaders where
it logically belongs as a transport-level concern.
- Add typed EncodeValue overloads (string?, bool, long, double) to McpHeaderEncoder
  for stronger compile-time safety and non-nullable return types
- Change MinVersionForStandardHeaders from const to static readonly to prevent
  const-inlining across assemblies when the version string changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per SEP-2243, servers MUST accept extra whitespace around header values
and compare the trimmed value to the request body. Added .Trim() calls
when reading Mcp-Method, Mcp-Name, and Mcp-Param-* header values in
server-side validation. Added two tests for whitespace handling.
For parameters with JSON Schema type 'number' or 'integer', compare
header values numerically instead of as exact strings. This handles
cross-SDK representation differences where different SDKs may serialize
the same number differently (e.g., '42' vs '42.0', or minor floating
point drift). Uses an absolute tolerance of 1e-9 for decimal values,
matching the conformance test approach.
The comment incorrectly stated the case-insensitive prefix check was
per the SEP. The spec defines lowercase '=?base64?' only; the SDK
intentionally matches case-insensitively for robustness against
non-conforming senders.
Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Outdated
Comment thread tests/ModelContextProtocol.AspNetCore.Tests/Sep2243HeaderTests.cs Outdated
Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Outdated
Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Outdated
}

// Only validate for JSON-RPC requests and notifications, not responses.
if (!(message is JsonRpcRequest || message is JsonRpcNotification))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given the new revision of the protocol, shouldn't we be rejecting all client-to-server responses and errors?

Suggested change
if (!(message is JsonRpcRequest || message is JsonRpcNotification))
if (message is not JsonRpcRequest or not JsonRpcNotification)
{
// Reject if draft protocol version entirely
}

We should probably just move this down into McpServerImpl, and then we wouldn't need the check in StreamableHttpHandler at all.

Copy link
Copy Markdown

@tarekgh tarekgh May 13, 2026

Choose a reason for hiding this comment

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

@halter73

The SEP-2243 doesn't require it; the spec only defines header behavior for requests/notifications.

Moving rejection to McpServerImpl affects all transports (stdio, SSE, etc.), not just HTTP, which potentionaly could break existing behavior?

Client responses are still valid as in MCP, clients can send responses to server-initiated requests (e.g., sampling/createMessage responses). Rejecting them would break that flow?

The suggested code has a bug? message is not JsonRpcRequest or not JsonRpcNotification is always true?

}

// Look up the tool to check for x-mcp-header annotations in the schema
var toolName = GetJsonNodeStringProperty(bodyParams, "name");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This does seem pretty fragile to me to parse the Params twice. I wonder if we could flow the headers down to McpServerImpl so we could do the header validation there. I would hate it if we ended up looking at a different primitive or a different JSON property due to some subtle difference in parsing at each layer.

Copy link
Copy Markdown

@tarekgh tarekgh May 13, 2026

Choose a reason for hiding this comment

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

Flowing the headers down to McpServerImpl will have a trade-off:

  • Pro: Single source of truth for params; McpServerImpl uses the same deserialized CallToolRequestParams for both dispatch and validation. No divergence risk.
  • Con: Header validation now happens deeper in the pipeline (after session creation, after message routing), so invalid headers get further before being rejected. Error responses go through the JSON-RPC layer rather than being a simple HTTP 400.
  • Con: Couples McpServerImpl (in Core, transport-agnostic) to HTTP header concepts. We'll need to abstract the validation behind an interface or delegate to keep Core clean.

I suggest we can discuss this more and address it outside this PR.

Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Outdated
Comment thread tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs Outdated

CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion);

AddMcpRequestHeaders(httpRequestMessage.Headers, message);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if we should force you to opt into sending the HTTP standardization headers by selecting the DRAFT-2026-v1 ProtocolVersion. This would be similar to what I have in my MRTR prototype at #1458. That adds a new ExperimentalProtocolVersion property to both the server and client options to opt-in to the draft behavior.

HTTP standardization is different from MRTR, because it's not breaking to just send the headers from the client even if you don't opt into the new protocol version, and it's fine for the server to only require them if the client tries to use the new draft protocol.

However, I still think we probably should just gate all the new behavior on opting into the new protocol version though. I think if we do this, that will probably make cross-version compatibility testing easier.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I would suggest not using ExperimentalProtocolVersion for now, and revisiting it later.

My reasoning is:

  • The headers are purely additive on the client side, so sending extra headers should not break anything.
  • Server-side validation can remain lenient by validating only when the headers are present, rather than requiring explicit opt-in.

For old client → new server: with the current implementation, the server only validates MCP headers when the client sends Mcp-Protocol-Version: DRAFT-2026-v1. Older clients that either omit the header or send 2025-11-05 will pass through without validation or rejection, so compatibility should be fine.

For new client → old server: this should also be fine, since older servers will simply ignore headers, they do not recognize.

So, the cross-version compatibility concern already appears to be addressed.

I’m open to discussing this further, but I’d prefer to get #1458 completed first, and then evaluate whether introducing ExperimentalProtocolVersion would provide enough benefit.

Comment thread src/ModelContextProtocol.Core/Client/McpClient.Methods.cs Outdated
Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Outdated
Comment thread src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Comment thread src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs Outdated
Tarek Mahmoud Sayed added 2 commits May 13, 2026 15:30
The @modelcontextprotocol/conformance@0.1.16 package uses fs.globSync
which requires Node.js 22+.
@tarekgh
Copy link
Copy Markdown

tarekgh commented May 13, 2026

@halter73 I have updated the CI Node.js from 20 to 22 because package.json pins @modelcontextprotocol/conformance at 0.1.16, which requires fs.globSync (added in Node 22).

Let me know if there is any concern with that.

The http-standard-headers, http-custom-headers, http-invalid-tool-headers,
http-header-validation, and http-custom-header-server-validation scenarios
are not yet published in @modelcontextprotocol/conformance. Gate these tests
on HasSep2243Scenarios which checks for package version >= 0.2.0.
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.

SEP-2243: HTTP Standardization

4 participants