Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567) (#1610)
This commit is contained in:
parent
3368bb7349
commit
6c323e783b
1 changed files with 5704 additions and 712 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
|
@ -27,7 +27,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
|
||||
private WebApplication? _app;
|
||||
|
||||
private async Task StartAsync()
|
||||
private async Task StartAsync(bool stateless = false)
|
||||
{
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
|
|
@ -36,7 +36,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
Name = nameof(StreamableHttpServerConformanceTests),
|
||||
Version = "73",
|
||||
};
|
||||
}).WithTools(Tools).WithHttpTransport();
|
||||
}).WithTools(Tools).WithHttpTransport(options => options.Stateless = stateless);
|
||||
|
||||
_app = Builder.Build();
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
options.IdleTimeout = TimeSpan.MinValue;
|
||||
});
|
||||
|
||||
var ex = await Assert.ThrowsAnyAsync<ArgumentOutOfRangeException>(StartAsync);
|
||||
var ex = await Assert.ThrowsAnyAsync<ArgumentOutOfRangeException>(() => StartAsync());
|
||||
Assert.Contains("IdleTimeout", ex.Message);
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
options.MaxIdleSessionCount = -1;
|
||||
});
|
||||
|
||||
var ex = await Assert.ThrowsAnyAsync<ArgumentOutOfRangeException>(StartAsync);
|
||||
var ex = await Assert.ThrowsAnyAsync<ArgumentOutOfRangeException>(() => StartAsync());
|
||||
Assert.Contains("MaxIdleSessionCount", ex.Message);
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +247,38 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
Assert.Contains("Stateless", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostMalformedJson_Returns400_InvalidRequest_WithNullId()
|
||||
{
|
||||
await StartAsync();
|
||||
|
||||
using var response = await HttpClient.PostAsync("", JsonContent("{ this is not valid json"), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
|
||||
// The server must emit a conformant JSON-RPC error envelope (not a raw 500). Because the request
|
||||
// id could not be read, the error carries id=null per JSON-RPC 2.0 §5.1 — and crucially it must
|
||||
// serialize as JSON null, not "" (regression guard for the RequestId write path).
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("id").ValueKind);
|
||||
Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostRequestWithExplicitNullId_Returns400_InvalidRequest_WithNullId()
|
||||
{
|
||||
await StartAsync();
|
||||
|
||||
// A request carrying an explicit `id:null` is malformed per the MCP base protocol ("the ID MUST
|
||||
// NOT be null") and must NOT be silently treated as a notification. The server rejects it with a
|
||||
// conformant 400 InvalidRequest error whose own id is null.
|
||||
using var response = await HttpClient.PostAsync("", JsonContent("""{"jsonrpc":"2.0","id":null,"method":"tools/list"}"""), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
|
||||
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("id").ValueKind);
|
||||
Assert.Equal((int)McpErrorCode.InvalidRequest, doc.RootElement.GetProperty("error").GetProperty("code").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetWithoutSessionId_Returns400_WithStatelessGuidance()
|
||||
{
|
||||
|
|
@ -886,7 +918,8 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
[Fact]
|
||||
public async Task DraftVersion_RejectsMissingMcpMethodHeader()
|
||||
{
|
||||
await StartAsync();
|
||||
// Draft is sessionless and served only on a stateless server (SEP-2567).
|
||||
await StartAsync(stateless: true);
|
||||
|
||||
// Initialize with draft version to enable header validation
|
||||
await CallInitializeWithDraftVersionAndValidateAsync();
|
||||
|
|
@ -894,7 +927,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
// Send a tools/call request without Mcp-Method header — should be rejected
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Content = JsonContent(CallTool("echo", """{"message":"test"}"""));
|
||||
request.Headers.Add("MCP-Protocol-Version", "DRAFT-2026-v1");
|
||||
request.Headers.Add("MCP-Protocol-Version", "2026-07-28");
|
||||
// Deliberately omit Mcp-Method header
|
||||
|
||||
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
|
@ -904,13 +937,13 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
[Fact]
|
||||
public async Task DraftVersion_RejectsMismatchedMcpMethodHeader()
|
||||
{
|
||||
await StartAsync();
|
||||
await StartAsync(stateless: true);
|
||||
await CallInitializeWithDraftVersionAndValidateAsync();
|
||||
|
||||
// Send a tools/call request but set Mcp-Method to wrong value
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Content = JsonContent(CallTool("echo", """{"message":"test"}"""));
|
||||
request.Headers.Add("MCP-Protocol-Version", "DRAFT-2026-v1");
|
||||
request.Headers.Add("MCP-Protocol-Version", "2026-07-28");
|
||||
request.Headers.Add("Mcp-Method", "resources/read"); // Wrong method
|
||||
|
||||
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
|
@ -920,13 +953,13 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
[Fact]
|
||||
public async Task DraftVersion_AcceptsCorrectMcpMethodHeader()
|
||||
{
|
||||
await StartAsync();
|
||||
await StartAsync(stateless: true);
|
||||
await CallInitializeWithDraftVersionAndValidateAsync();
|
||||
|
||||
// Send a tools/call request with correct Mcp-Method and Mcp-Name headers
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Content = JsonContent(CallTool("echo", """{"message":"hello"}"""));
|
||||
request.Headers.Add("MCP-Protocol-Version", "DRAFT-2026-v1");
|
||||
request.Headers.Add("MCP-Protocol-Version", "2026-07-28");
|
||||
request.Headers.Add("Mcp-Method", "tools/call");
|
||||
request.Headers.Add("Mcp-Name", "echo");
|
||||
|
||||
|
|
@ -956,19 +989,19 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Content = JsonContent(InitializeRequestDraft);
|
||||
request.Headers.Add("MCP-Protocol-Version", "DRAFT-2026-v1");
|
||||
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);
|
||||
var rpcResponse = await AssertSingleSseResponseAsync(response);
|
||||
AssertServerInfo(rpcResponse);
|
||||
|
||||
var sessionId = Assert.Single(response.Headers.GetValues("mcp-session-id"));
|
||||
SetSessionId(sessionId);
|
||||
// Draft protocol revision (SEP-2567) is sessionless; the server does not return mcp-session-id.
|
||||
// Subsequent requests carry MCP-Protocol-Version=2026-07-28 to opt back into the draft path.
|
||||
}
|
||||
|
||||
private static string InitializeRequestDraft => """
|
||||
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"DRAFT-2026-v1","capabilities":{},"clientInfo":{"name":"IntegrationTestClient","version":"1.0.0"}}}
|
||||
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"IntegrationTestClient","version":"1.0.0"}}}
|
||||
""";
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
Loading…
Reference in a new issue