Skip to content

Commit 94ff334

Browse files
author
tarekgh
committed
Reject malformed per-request clientCapabilities with 400 InvalidParams
ValidateRequiredPerRequestMeta only null-checked _meta clientCapabilities, so a present-but-wrong-shape value passed the HTTP validation and later failed during deserialization as a generic -32603 internal error on a 200 response. Require it to be a JSON object so both missing and malformed values are rejected up front with -32602 and HTTP 400 as SEP-2575 requires.
1 parent 97146aa commit 94ff334

2 files changed

Lines changed: 57 additions & 5 deletions

File tree

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -709,9 +709,11 @@ internal static Task RunSessionAsync(HttpContext httpContext, McpServer session,
709709
/// <summary>
710710
/// Validates that a request riding a per-request-metadata protocol revision carries the required
711711
/// <c>_meta</c> fields beyond the protocol version (which <see cref="ValidateProtocolVersionEnvelope"/>
712-
/// already checked): <c>io.modelcontextprotocol/clientCapabilities</c>. Performed at the HTTP layer
713-
/// so the rejection can use 400 Bad Request as SEP-2575 requires. <c>clientInfo</c> is optional,
714-
/// so its absence is not rejected.
712+
/// already checked): <c>io.modelcontextprotocol/clientCapabilities</c>, which must be present and a
713+
/// JSON object. Performed at the HTTP layer so the rejection can use 400 Bad Request as SEP-2575
714+
/// requires; a malformed (non-object) value would otherwise fail later during deserialization as a
715+
/// generic internal error on a 200 response. <c>clientInfo</c> is optional, so its absence is not
716+
/// rejected.
715717
/// </summary>
716718
private static bool ValidateRequiredPerRequestMeta(
717719
HttpContext context,
@@ -723,12 +725,12 @@ private static bool ValidateRequiredPerRequestMeta(
723725
McpProtocolVersions.RequiresPerRequestMetadata(protocolVersionHeader) &&
724726
(requestParams is not JsonObject paramsObj ||
725727
paramsObj["_meta"] is not JsonObject metaObj ||
726-
metaObj[MetaKeys.ClientCapabilities] is null))
728+
metaObj[MetaKeys.ClientCapabilities] is not JsonObject))
727729
{
728730
errorDetail = new JsonRpcErrorDetail
729731
{
730732
Code = (int)McpErrorCode.InvalidParams,
731-
Message = $"Requests using protocol version '{protocolVersionHeader}' must include '_meta/{MetaKeys.ClientCapabilities}'.",
733+
Message = $"Requests using protocol version '{protocolVersionHeader}' must include '_meta/{MetaKeys.ClientCapabilities}' as a JSON object.",
732734
};
733735
return false;
734736
}

tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,56 @@ public async Task GetEndpoint_NotMapped_UnderDefaultStatelessConfiguration_Retur
413413
Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode);
414414
}
415415

416+
[Fact]
417+
public async Task July2026Post_MissingClientCapabilities_Returns400_WithInvalidParams()
418+
{
419+
await StartAsync();
420+
421+
var body =
422+
@"{""jsonrpc"":""2.0"",""id"":20,""method"":""server/discover"",""params"":{""_meta"":{" +
423+
@"""io.modelcontextprotocol/protocolVersion"":""" + McpProtocolVersions.July2026ProtocolVersion + @"""}}}";
424+
425+
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
426+
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
427+
request.Headers.Add("Mcp-Method", "server/discover");
428+
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
429+
430+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
431+
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
432+
Assert.Equal(20, json["id"]!.GetValue<long>());
433+
Assert.Equal((int)McpErrorCode.InvalidParams, json["error"]!["code"]!.GetValue<int>());
434+
Assert.Contains(MetaKeys.ClientCapabilities, json["error"]!["message"]!.GetValue<string>(), StringComparison.Ordinal);
435+
}
436+
437+
[Theory]
438+
[InlineData("5")]
439+
[InlineData("\"caps\"")]
440+
[InlineData("[]")]
441+
[InlineData("true")]
442+
public async Task July2026Post_MalformedClientCapabilities_Returns400_WithInvalidParams(string clientCapabilitiesJson)
443+
{
444+
await StartAsync();
445+
446+
var body =
447+
@"{""jsonrpc"":""2.0"",""id"":21,""method"":""server/discover"",""params"":{""_meta"":{" +
448+
@"""io.modelcontextprotocol/protocolVersion"":""" + McpProtocolVersions.July2026ProtocolVersion + @"""," +
449+
@"""io.modelcontextprotocol/clientInfo"":{""name"":""raw"",""version"":""1.0""}," +
450+
@"""io.modelcontextprotocol/clientCapabilities"":" + clientCapabilitiesJson + "}}}";
451+
452+
using var request = new HttpRequestMessage(HttpMethod.Post, "") { Content = JsonContent(body) };
453+
request.Headers.Add(ProtocolVersionHeader, McpProtocolVersions.July2026ProtocolVersion);
454+
request.Headers.Add("Mcp-Method", "server/discover");
455+
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
456+
457+
// A present-but-wrong-shape clientCapabilities must be rejected up front with -32602 / 400,
458+
// not surface later as a generic internal error on a 200 response.
459+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
460+
var json = await ReadJsonResponseAsync(response, TestContext.Current.CancellationToken);
461+
Assert.Equal(21, json["id"]!.GetValue<long>());
462+
Assert.Equal((int)McpErrorCode.InvalidParams, json["error"]!["code"]!.GetValue<int>());
463+
Assert.Contains(MetaKeys.ClientCapabilities, json["error"]!["message"]!.GetValue<string>(), StringComparison.Ordinal);
464+
}
465+
416466
[McpServerToolType]
417467
private sealed class CapabilityTools
418468
{

0 commit comments

Comments
 (0)