Skip to content

Commit 5f4735b

Browse files
author
Tarek Mahmoud Sayed
committed
Use numeric comparison for number-typed custom header validation
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.
1 parent bdcf448 commit 5f4735b

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ argForMissing is not null &&
748748
if (expectedHeaderValue is not null)
749749
{
750750
var decodedExpected = McpHeaderEncoder.DecodeValue(expectedHeaderValue);
751-
if (!string.Equals(decodedActual, decodedExpected, StringComparison.Ordinal))
751+
if (!ValuesMatch(decodedActual, decodedExpected, property.Value))
752752
{
753753
errorMessage = $"Header mismatch: {fullHeaderName} header value does not match body argument '{property.Name}'.";
754754
return false;
@@ -791,6 +791,41 @@ private static bool IsValidHeaderValue(string value)
791791
return true;
792792
}
793793

794+
/// <summary>
795+
/// Compares two decoded header values, using numeric comparison for number-typed
796+
/// parameters to handle cross-SDK representation differences (e.g., "42" vs "42.0").
797+
/// </summary>
798+
private static bool ValuesMatch(string? actual, string? expected, System.Text.Json.JsonElement propertySchema)
799+
{
800+
if (string.Equals(actual, expected, StringComparison.Ordinal))
801+
{
802+
return true;
803+
}
804+
805+
// JSON Schema defines two numeric types: "number" (any numeric value including
806+
// decimals like 3.14) and "integer" (whole numbers only like 42). Both produce
807+
// JsonValueKind.Number in the JSON body and are sent as numeric strings in headers.
808+
// We check for both because different SDKs may serialize them differently —
809+
// e.g., a client might send header "42.0" for an "integer" body value of 42,
810+
// or header "42" for a "number" body value of 42.0. Without handling both types,
811+
// valid cross-SDK requests would be incorrectly rejected.
812+
if (propertySchema.TryGetProperty("type", out var typeElement) &&
813+
typeElement.ValueKind == System.Text.Json.JsonValueKind.String &&
814+
typeElement.GetString() is "number" or "integer" &&
815+
actual is not null && expected is not null &&
816+
double.TryParse(actual, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var actualNum) &&
817+
double.TryParse(expected, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var expectedNum))
818+
{
819+
// Allow a small absolute tolerance for floating point representation differences.
820+
if (Math.Abs(actualNum - expectedNum) < 1e-9)
821+
{
822+
return true;
823+
}
824+
}
825+
826+
return false;
827+
}
828+
794829
private static string? ConvertJsonNodeToHeaderValue(System.Text.Json.Nodes.JsonNode node)
795830
{
796831
if (node is not System.Text.Json.Nodes.JsonValue jsonValue)

tests/ModelContextProtocol.AspNetCore.Tests/Sep2243HeaderTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,54 @@ public async Task Server_AcceptsLargeIntegerWithFullPrecision()
233233
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
234234
}
235235

236+
[Theory]
237+
[InlineData("42", 42)] // "42" header vs 42 body → exact integer match
238+
[InlineData("42.0", 42)] // "42.0" header vs 42 body → numeric equivalence
239+
[InlineData("42", 42.0)] // "42" header vs 42.0 body → numeric equivalence
240+
public async Task Server_AcceptsNumericEquivalentHeaderValues(string headerValue, double bodyValue)
241+
{
242+
await StartAsync();
243+
await InitializeWithDraftVersionAsync();
244+
245+
var callJson = CallTool("header_test", $$$"""{"region":"test","priority":{{{bodyValue.ToString(System.Globalization.CultureInfo.InvariantCulture)}}},"verbose":false,"emptyVal":""}""");
246+
247+
using var request = new HttpRequestMessage(HttpMethod.Post, "");
248+
request.Content = new StringContent(callJson, Encoding.UTF8, "application/json");
249+
request.Headers.Add("MCP-Protocol-Version", "DRAFT-2026-v1");
250+
request.Headers.Add("Mcp-Method", "tools/call");
251+
request.Headers.Add("Mcp-Name", "header_test");
252+
request.Headers.Add("Mcp-Param-Region", "test");
253+
request.Headers.Add("Mcp-Param-Priority", headerValue);
254+
request.Headers.Add("Mcp-Param-Verbose", "false");
255+
request.Headers.Add("Mcp-Param-EmptyVal", "");
256+
257+
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
258+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
259+
}
260+
261+
[Fact]
262+
public async Task Server_RejectsNonNumericMismatch_ForIntegerParam()
263+
{
264+
await StartAsync();
265+
await InitializeWithDraftVersionAsync();
266+
267+
// Header says "99" but body says priority:42 — must reject even with numeric comparison
268+
var callJson = CallTool("header_test", """{"region":"test","priority":42,"verbose":false,"emptyVal":""}""");
269+
270+
using var request = new HttpRequestMessage(HttpMethod.Post, "");
271+
request.Content = new StringContent(callJson, Encoding.UTF8, "application/json");
272+
request.Headers.Add("MCP-Protocol-Version", "DRAFT-2026-v1");
273+
request.Headers.Add("Mcp-Method", "tools/call");
274+
request.Headers.Add("Mcp-Name", "header_test");
275+
request.Headers.Add("Mcp-Param-Region", "test");
276+
request.Headers.Add("Mcp-Param-Priority", "99");
277+
request.Headers.Add("Mcp-Param-Verbose", "false");
278+
request.Headers.Add("Mcp-Param-EmptyVal", "");
279+
280+
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
281+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
282+
}
283+
236284
[Fact]
237285
public async Task Server_SkipsHeaderValidation_ForNonDraftVersion()
238286
{

0 commit comments

Comments
 (0)