Follow-ups to #1610: de-draft 2026-07-28 terminology, renumber error codes, ProtocolVersion floor (#1671)

This commit is contained in:
Stephen Halter 2026-06-25 14:07:52 -07:00 • committed by GitHub
commit d6b1615988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -114,7 +114,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsUnionIntegerCanonicalForm()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Union-typed (["integer","null"]) parameter: header carries canonical "42" while the body
// carries the decimal form 42.0. The server must treat the union type as integer and match.
@ -135,7 +135,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_RejectsUnionIntegerOutsideSafeRange()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
var callJson = CallTool("union_test", """{"priority":9007199254740993}""");
@ -154,7 +154,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsExponentBodyMatchingDecimalHeader()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Body carries the integer in exponent form (1e2 = 100); header carries the decimal "100".
var callJson = CallTool("header_test", """{"region":"test","priority":1e2,"verbose":false,"emptyVal":""}""");
@ -177,7 +177,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsWhitespaceAroundMcpNameHeaderValue()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Per SEP-2243: servers MUST accept extra whitespace around header values
// and compare the trimmed value to the request body.
@ -201,7 +201,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsWhitespaceAroundMcpMethodHeaderValue()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Per SEP-2243: servers MUST accept extra whitespace around header values
var callJson = CallTool("header_test", """{"region":"us-west1","priority":42,"verbose":false,"emptyVal":""}""");
@ -224,7 +224,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_ValidatesEmptyStringHeaderValue_AgainstBodyValue()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Send a tools/call with an empty string param that has an x-mcp-header.
// The header should be present with an empty value, matching the body's empty string.
@ -248,7 +248,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_RejectsHeaderMismatch_WhenEmptyHeaderDoesNotMatchBody()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Send a tools/call where the body has a non-empty value but the header is empty
var callJson = CallTool("header_test", """{"region":"us-west1","priority":42,"verbose":false,"emptyVal":"some-value"}""");
@ -271,7 +271,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsBase64EncodedHeaderWithControlChars()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Encode a value with a newline control character using Base64
var valueWithNewline = "line1\nline2";
@ -297,7 +297,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsMaxSafeIntegerWithFullPrecision()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// The maximum safe integer (2^53 - 1) must be accepted, and compared exactly without
// losing precision through a double conversion.
@ -326,7 +326,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_RejectsIntegerOutsideSafeRange(string outOfRangeValue)
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Per SEP-2243 integer values MUST be within the JavaScript safe integer range.
// A matching header and body that are both outside the range must still be rejected.
@ -355,7 +355,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_AcceptsNumericEquivalentHeaderValues(string headerValue, string bodyValue)
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// bodyValue is inserted as a raw JSON numeric literal so that forms such as "42.0" and
// "4.2e1" are preserved in the body exactly as another SDK might serialize them.
@ -382,7 +382,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_RejectsNonIntegerValue_EvenWhenHeaderAndBodyMatch(string nonIntegerValue)
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// For an integer-typed parameter a non-whole numeric value is invalid and must be rejected
// even when the header and body strings are byte-for-byte identical (it must not slip through
@ -407,7 +407,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_RejectsNonNumericMismatch_ForIntegerParam()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Header says "99" but body says priority:42 — must reject even with numeric comparison
var callJson = CallTool("header_test", """{"region":"test","priority":42,"verbose":false,"emptyVal":""}""");
@ -427,17 +427,17 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
}
[Fact]
public async Task Server_SkipsHeaderValidation_ForNonDraftVersion()
public async Task Server_SkipsHeaderValidation_ForLegacyVersion()
{
await StartAsync();
await InitializeWithNonDraftVersionAsync();
await InitializeWithLegacyVersionAsync();
// With non-draft version, Mcp-Param-* headers are NOT validated even if mismatched
// With the legacy version, Mcp-Param-* headers are NOT validated even if mismatched
var callJson = CallTool("header_test", """{"region":"us-west1","priority":42,"verbose":false,"emptyVal":""}""");
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = new StringContent(callJson, Encoding.UTF8, "application/json");
// Send the WRONG header value — this should still succeed because version is non-draft
// Send the WRONG header value. This should still succeed because the version is legacy.
request.Headers.Add("MCP-Protocol-Version", "2025-11-25");
request.Headers.Add("Mcp-Method", "tools/call");
request.Headers.Add("Mcp-Name", "header_test");
@ -451,7 +451,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
public async Task Server_RejectsInvalidUtf8EncodedHeaderValue()
{
await StartAsync();
await InitializeWithDraftVersionAsync();
await InitializeWithJuly2026ProtocolVersionAsync();
// Create a separate HttpClient that sends raw UTF-8 bytes in Mcp-* headers
// instead of properly base64-encoding non-ASCII values.
@ -570,32 +570,32 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
#region Helpers
private async Task InitializeWithDraftVersionAsync()
private async Task InitializeWithJuly2026ProtocolVersionAsync()
{
HttpClient.DefaultRequestHeaders.Remove("mcp-session-id");
using var request = new HttpRequestMessage(HttpMethod.Post, "");
request.Content = JsonContent(InitializeRequestDraft);
request.Content = JsonContent(InitializeRequestJuly2026Protocol);
request.Headers.Add("MCP-Protocol-Version", "2026-07-28");
request.Headers.Add("Mcp-Method", "initialize");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Draft protocol revision (SEP-2567) is sessionless: the server does not return a
// Starting with the 2026-07-28 protocol revision (SEP-2567), Streamable HTTP does not return a
// mcp-session-id header. Subsequent requests carry MCP-Protocol-Version=2026-07-28
// to route through the sessionless path.
// so each one is handled independently.
}
private async Task InitializeWithNonDraftVersionAsync()
private async Task InitializeWithLegacyVersionAsync()
{
HttpClient.DefaultRequestHeaders.Remove("mcp-session-id");
using var response = await HttpClient.PostAsync("", JsonContent(InitializeRequest), TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Server is stateless by default (SEP-2567), so initializing with the non-draft protocol does not return
// a mcp-session-id header. Subsequent requests are independent, just like the draft path.
// Server is stateless by default (SEP-2567), so initializing with the legacy protocol does not return
// a mcp-session-id header. Subsequent requests are independent, just like requests on the 2026-07-28 revision.
}
private static StringContent JsonContent(string json) => new(json, Encoding.UTF8, "application/json");
@ -614,7 +614,7 @@ public class HttpHeaderConformanceTests(ITestOutputHelper outputHelper) : Kestre
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"TestClient","version":"1.0"}}}
""";
private static string InitializeRequestDraft => """
private static string InitializeRequestJuly2026Protocol => """
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"TestClient","version":"1.0"}}}
""";