diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index f26e9ed5c..7efaaf593 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -187,7 +187,24 @@ await WriteJsonRpcErrorAsync(context, } InitializeSseResponse(context); - var wroteResponse = await session.Transport.HandlePostRequestAsync(message, context.Response.Body, onResponseStarting, context.RequestAborted); + + bool wroteResponse; + try + { + wroteResponse = await session.Transport.HandlePostRequestAsync(message, context.Response.Body, onResponseStarting, context.RequestAborted); + } + catch (JsonException) when (!context.Response.HasStarted) + { + // The initialize handshake eagerly deserializes its params inside the transport, before the + // message reaches the session's JSON-RPC error handling. A structurally valid envelope whose + // params are invalid (e.g. a missing required field like clientInfo.version) would otherwise + // bubble up as an opaque 500. Surface a conformant JSON-RPC error that echoes the request id. + await WriteJsonRpcErrorAsync(context, + "Bad Request: The request parameters were invalid.", + StatusCodes.Status400BadRequest, (int)McpErrorCode.InvalidParams, requestId); + return; + } + if (!wroteResponse) { // We wound up writing nothing, so there should be no Content-Type response header. diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index dd051e4d3..e6f2c1864 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -318,6 +318,24 @@ public async Task PostMalformedJson_Returns400_InvalidRequest_WithNullId() Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32()); } + [Fact] + public async Task PostInitializeWithMissingRequiredParam_Returns400_InvalidParams_EchoesRequestId() + { + await StartAsync(); + + // A well-formed JSON-RPC envelope whose initialize params omit a required field + // (here clientInfo.version) must not surface as an opaque 500. The server should + // reject it with a conformant JSON-RPC error (InvalidParams) and echo the request id. + using var response = await HttpClient.PostAsync("", + JsonContent("""{"jsonrpc":"2.0","id":7,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"IntegrationTestClient"}}}"""), + TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + Assert.Equal(7, doc.RootElement.GetProperty("id").GetInt64()); + Assert.Equal((int)McpErrorCode.InvalidParams, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32()); + } + [Fact] public async Task PostRequestWithExplicitNullId_Returns400_InvalidRequest_WithNullId() {