diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index f26e9ed5c..271b32b9d 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -1040,6 +1040,14 @@ internal static bool ValidateMcpHeaders(HttpContext context, JsonRpcMessage mess _ => null, }; var mcpNameInBody = GetJsonNodeStringProperty(bodyParams, routingNameParameter); + if (mcpNameInBody is null && + bodyParams is System.Text.Json.Nodes.JsonObject paramsObject && + paramsObject.TryGetPropertyValue(routingNameParameter, out var routingNameNode) && + routingNameNode is not null) + { + errorMessage = $"Header mismatch: body '{routingNameParameter}' value must be a string."; + return false; + } // Check that the header value matches the body value if the body value is present. if (!string.Equals(decodedMcpNameInHeader, mcpNameInBody, StringComparison.Ordinal)) @@ -1246,9 +1254,14 @@ argForMissing is not null && private static string? GetJsonNodeStringProperty(System.Text.Json.Nodes.JsonNode? node, string propertyName) { - if (node is System.Text.Json.Nodes.JsonObject obj && obj.TryGetPropertyValue(propertyName, out var value)) - { - return value?.GetValue(); + // A non-string value (number, bool, object, array) is a malformed request, not a server error, + // so report it as absent rather than letting GetValue() throw. + if (node is System.Text.Json.Nodes.JsonObject obj && + obj.TryGetPropertyValue(propertyName, out var value) && + value is System.Text.Json.Nodes.JsonValue jsonValue && + jsonValue.TryGetValue(out var stringValue)) + { + return stringValue; } return null; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs index 914b98db0..697e0a55d 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpHeaderConformanceTests.cs @@ -297,6 +297,35 @@ public async Task Server_RejectsHeaderMismatch_WhenEmptyHeaderDoesNotMatchBody() Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } + [Theory] + [InlineData("tools/call", "name", "42")] + [InlineData("tools/call", "name", "true")] + [InlineData("tools/call", "name", "{}")] + [InlineData("tools/call", "name", "[]")] + [InlineData("prompts/get", "name", "42")] + [InlineData("resources/read", "uri", "{}")] + public async Task Server_RejectsNonStringRoutingNameInBody_WithJsonRpcError(string method, string routingParameter, string bodyValue) + { + await StartAsync(); + await ProbeWithJuly2026ProtocolVersionAsync(); + + var callJson = "{\"jsonrpc\":\"2.0\",\"id\":77,\"method\":\"" + method + "\",\"params\":{\"" + routingParameter + "\":" + bodyValue + + @",""_meta"":{""io.modelcontextprotocol/protocolVersion"":""2026-07-28"",""io.modelcontextprotocol/clientInfo"":{""name"":""TestClient"",""version"":""1.0""},""io.modelcontextprotocol/clientCapabilities"":{}}}}"; + + using var request = new HttpRequestMessage(HttpMethod.Post, ""); + request.Content = new StringContent(callJson, Encoding.UTF8, "application/json"); + request.Headers.Add("MCP-Protocol-Version", "2026-07-28"); + request.Headers.Add("Mcp-Method", method); + request.Headers.Add("Mcp-Name", "42"); + + using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var json = JsonNode.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken))!; + Assert.Equal(77, json["id"]!.GetValue()); + Assert.Equal((int)McpErrorCode.HeaderMismatch, json["error"]!["code"]!.GetValue()); + } + [Fact] public async Task Server_AcceptsBase64EncodedHeaderWithControlChars() {