using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.AspNetCore.Tests.Utils; using ModelContextProtocol.Protocol; using System.Net; using System.Text; using System.Text.Json; namespace ModelContextProtocol.AspNetCore.Tests; /// /// HTTP-level tests for the 2026-07-28 protocol revision (SEP-2575 + SEP-2567): verify that the server /// does not issue Mcp-Session-Id for those requests and returns structured /// errors instead of plain 400s. /// public class July2026ProtocolHttpHandlerTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable { private WebApplication? _app; private async Task StartAsync(bool stateless = false) { Builder.Services.AddMcpServer(options => { options.ServerInfo = new Implementation { Name = nameof(July2026ProtocolHttpHandlerTests), Version = "1" }; }).WithHttpTransport(options => { // SessionMode = HttpServerSessionMode.Stateful maps the GET/DELETE endpoints and opts the author into sessions. Starting with // the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions, so such a request is // refused on a session-enabled server. SessionMode = HttpServerSessionMode.Stateless (the default) serves them natively. options.SessionMode = stateless ? HttpServerSessionMode.Stateless : HttpServerSessionMode.Stateful; }); _app = Builder.Build(); _app.MapMcp(); await _app.StartAsync(TestContext.Current.CancellationToken); HttpClient.DefaultRequestHeaders.Accept.Add(new("application/json")); HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream")); } public async ValueTask DisposeAsync() { if (_app is not null) { await _app.DisposeAsync(); } base.Dispose(); } [Fact] public async Task Request_OnStatelessServer_Succeeds_WithoutMcpSessionIdHeader() { await StartAsync(stateless: true); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); // On a stateless server, server/discover succeeds without creating a session. var content = new StringContent( DiscoverRequestJuly2026Protocol, Encoding.UTF8, "application/json"); using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.False(response.Headers.Contains("Mcp-Session-Id"), "Responses on the 2026-07-28 revision must not include Mcp-Session-Id"); } [Fact] public async Task Request_OnStatefulServer_IsRefused_WithUnsupportedProtocolVersionError() { // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567), // so the server cannot honor it when configured with sessions (SessionMode = HttpServerSessionMode.Stateful). The server refuses that // version with UnsupportedProtocolVersion (excluding it from Supported) so a dual-path client falls back // to the initialize handshake. await StartAsync(stateless: false); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); var content = new StringContent( DiscoverRequestJuly2026Protocol, Encoding.UTF8, "application/json"); using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.False(response.Headers.Contains("Mcp-Session-Id")); var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); var rpcMessage = JsonSerializer.Deserialize(body, McpJsonUtilities.DefaultOptions); var rpcError = Assert.IsType(rpcMessage); Assert.Equal((int)McpErrorCode.UnsupportedProtocolVersion, rpcError.Error.Code); var dataElement = (JsonElement)rpcError.Error.Data!; var errorData = dataElement.Deserialize(McpJsonUtilities.DefaultOptions); Assert.NotNull(errorData); Assert.Equal(McpProtocolVersions.July2026ProtocolVersion, errorData.Requested); // The 2026-07-28 protocol version is excluded from Supported so the client downgrades to an initialize-capable version. Assert.NotEmpty(errorData.Supported); Assert.DoesNotContain(McpProtocolVersions.July2026ProtocolVersion, errorData.Supported); } [Fact] public async Task RequestWithUnsupportedProtocolVersion_Returns_UnsupportedProtocolVersionError() { await StartAsync(stateless: true); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", "2099-12-31"); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); var content = new StringContent( DiscoverRequestJuly2026Protocol, Encoding.UTF8, "application/json"); using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); var rpcMessage = JsonSerializer.Deserialize(body, McpJsonUtilities.DefaultOptions); var rpcError = Assert.IsType(rpcMessage); Assert.Equal((int)McpErrorCode.UnsupportedProtocolVersion, rpcError.Error.Code); // Validate the structured data payload (SEP-2575 ยง"Unsupported Protocol Versions"). var dataElement = (JsonElement)rpcError.Error.Data!; var errorData = dataElement.Deserialize(McpJsonUtilities.DefaultOptions); Assert.NotNull(errorData); Assert.Equal("2099-12-31", errorData.Requested); Assert.NotEmpty(errorData.Supported); } [Fact] public async Task Request_WithMcpSessionIdHeader_IgnoresHeader_AndDoesNotEchoSessionId() { // Starting with the 2026-07-28 protocol revision, Streamable HTTP no longer supports sessions (SEP-2567): // a request carrying an Mcp-Session-Id is ignored, and the server must not mint or echo session IDs. await StartAsync(stateless: true); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Method", "server/discover"); HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id"); var content = new StringContent( DiscoverRequestJuly2026Protocol, Encoding.UTF8, "application/json"); using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.False(response.Headers.Contains("Mcp-Session-Id")); } [Fact] public async Task Get_WithoutSessionId_IsRejected() { await StartAsync(); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); using var response = await HttpClient.GetAsync("", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); Assert.Equal(["POST"], response.Content.Headers.Allow); } [Fact] public async Task Get_WithSessionId_IsRejected() { await StartAsync(); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id"); using var response = await HttpClient.GetAsync("", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); Assert.Equal(["POST"], response.Content.Headers.Allow); } [Fact] public async Task Delete_WithoutSessionId_IsRejected() { await StartAsync(); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); using var response = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); Assert.Equal(["POST"], response.Content.Headers.Allow); } [Fact] public async Task Delete_WithSessionId_IsRejected() { await StartAsync(); HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", McpProtocolVersions.July2026ProtocolVersion); HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id"); using var response = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); Assert.Equal(["POST"], response.Content.Headers.Allow); } private static string DiscoverRequestJuly2026Protocol => """ {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"July2026HttpHandlerTestClient","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}} """; }