Skip to content

Commit 64aaf70

Browse files
halter73Copilot
andcommitted
Add draft-mode tests and update XML docs
Adds round-trip tests for the new draft protocol revision and updates the XML documentation on ExperimentalProtocolVersion (client + server) to describe the full SEP-2575 + SEP-2567 behavior (sessionless, handshake-less, server/discover, MRTR-only server-to-client interactions, fallback to legacy initialize on unsupported-version responses). Tests added: - DiscoverProtocolTests / SubscriptionsListenProtocolTests / DraftErrorDataTests: JSON-serialization round-trip coverage for the new protocol types and error data payloads. - DraftConnectionTests: end-to-end client/server connection flow for draft client vs. draft server, draft client vs. legacy server (fallback), legacy client vs. draft server, and explicit server/discover invocation. - DraftHttpHandlerTests (AspNetCore): HTTP-level checks that draft requests don't emit Mcp-Session-Id, unsupported protocol versions return -32004 with the structured supportedVersions payload, draft requests carrying an Mcp-Session-Id route through the legacy lookup, and draft GET/DELETE are rejected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f15eb86 commit 64aaf70

5 files changed

Lines changed: 465 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using ModelContextProtocol.AspNetCore.Tests.Utils;
4+
using ModelContextProtocol.Protocol;
5+
using System.Net;
6+
using System.Text;
7+
using System.Text.Json;
8+
9+
namespace ModelContextProtocol.AspNetCore.Tests;
10+
11+
/// <summary>
12+
/// HTTP-level tests for the draft protocol revision (SEP-2575 + SEP-2567): verify that the server
13+
/// suppresses the <c>Mcp-Session-Id</c> header for draft requests and returns structured
14+
/// <see cref="McpErrorCode.UnsupportedProtocolVersion"/> errors instead of plain 400s.
15+
/// </summary>
16+
public class DraftHttpHandlerTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable
17+
{
18+
private const string DraftVersion = "2026-06-XX";
19+
20+
private WebApplication? _app;
21+
22+
private async Task StartAsync(bool experimentalServer)
23+
{
24+
#pragma warning disable MCPEXP001 // ExperimentalProtocolVersion is experimental
25+
Builder.Services.AddMcpServer(options =>
26+
{
27+
options.ServerInfo = new Implementation { Name = nameof(DraftHttpHandlerTests), Version = "1" };
28+
if (experimentalServer)
29+
{
30+
options.ExperimentalProtocolVersion = DraftVersion;
31+
}
32+
}).WithHttpTransport();
33+
#pragma warning restore MCPEXP001
34+
35+
_app = Builder.Build();
36+
_app.MapMcp();
37+
await _app.StartAsync(TestContext.Current.CancellationToken);
38+
39+
HttpClient.DefaultRequestHeaders.Accept.Add(new("application/json"));
40+
HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream"));
41+
}
42+
43+
public async ValueTask DisposeAsync()
44+
{
45+
if (_app is not null)
46+
{
47+
await _app.DisposeAsync();
48+
}
49+
base.Dispose();
50+
}
51+
52+
[Fact]
53+
public async Task DraftRequest_DoesNotEmitMcpSessionIdHeader()
54+
{
55+
await StartAsync(experimentalServer: true);
56+
57+
HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion);
58+
59+
// server/discover should succeed without creating a session.
60+
var content = new StringContent(
61+
"""{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}""",
62+
Encoding.UTF8, "application/json");
63+
using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken);
64+
65+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
66+
Assert.False(response.Headers.Contains("Mcp-Session-Id"), "Draft responses must not include Mcp-Session-Id");
67+
}
68+
69+
[Fact]
70+
public async Task RequestWithUnsupportedProtocolVersion_Returns_UnsupportedProtocolVersionError()
71+
{
72+
await StartAsync(experimentalServer: false);
73+
74+
HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", "2099-12-31");
75+
76+
var content = new StringContent(
77+
"""{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}""",
78+
Encoding.UTF8, "application/json");
79+
using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken);
80+
81+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
82+
83+
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
84+
var rpcMessage = JsonSerializer.Deserialize<JsonRpcMessage>(body, McpJsonUtilities.DefaultOptions);
85+
var rpcError = Assert.IsType<JsonRpcError>(rpcMessage);
86+
Assert.Equal((int)McpErrorCode.UnsupportedProtocolVersion, rpcError.Error.Code);
87+
88+
// Validate the structured data payload (SEP-2575 §"Unsupported Protocol Versions").
89+
var dataElement = (JsonElement)rpcError.Error.Data!;
90+
var errorData = dataElement.Deserialize<UnsupportedProtocolVersionErrorData>(McpJsonUtilities.DefaultOptions);
91+
Assert.NotNull(errorData);
92+
Assert.Equal("2099-12-31", errorData.Requested);
93+
Assert.NotEmpty(errorData.Supported);
94+
}
95+
96+
[Fact]
97+
public async Task DraftRequest_WithMcpSessionIdHeader_RoutesThroughLegacyPath()
98+
{
99+
// For back-compat with clients that opted into the experimental version on top of the legacy
100+
// stateful session model (MRTR-as-extension-on-initialize), draft-version requests that DO
101+
// include an Mcp-Session-Id are still accepted via the legacy session lookup path.
102+
await StartAsync(experimentalServer: true);
103+
104+
HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion);
105+
HttpClient.DefaultRequestHeaders.Add("Mcp-Session-Id", "non-existent-session-id");
106+
107+
var content = new StringContent(
108+
"""{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}""",
109+
Encoding.UTF8, "application/json");
110+
using var response = await HttpClient.PostAsync("", content, TestContext.Current.CancellationToken);
111+
112+
// Legacy path returns 404 for unknown sessions.
113+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
114+
}
115+
116+
[Fact]
117+
public async Task DraftGet_WithoutSessionId_IsRejected()
118+
{
119+
await StartAsync(experimentalServer: true);
120+
121+
HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion);
122+
123+
using var response = await HttpClient.GetAsync("", TestContext.Current.CancellationToken);
124+
125+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
126+
}
127+
128+
[Fact]
129+
public async Task DraftDelete_WithoutSessionId_IsRejected()
130+
{
131+
await StartAsync(experimentalServer: true);
132+
133+
HttpClient.DefaultRequestHeaders.Add("MCP-Protocol-Version", DraftVersion);
134+
135+
using var response = await HttpClient.DeleteAsync("", TestContext.Current.CancellationToken);
136+
137+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
138+
}
139+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModelContextProtocol.Client;
3+
using ModelContextProtocol.Protocol;
4+
using ModelContextProtocol.Server;
5+
using System.Text.Json;
6+
7+
namespace ModelContextProtocol.Tests.Client;
8+
9+
/// <summary>
10+
/// Tests for the draft protocol revision (SEP-2575 + SEP-2567) connection flow on
11+
/// <see cref="McpClient"/> — the client should call <c>server/discover</c> instead of
12+
/// <c>initialize</c> when <see cref="McpClientOptions.ExperimentalProtocolVersion"/> is set and
13+
/// the server supports the requested version, and it should fall back to the legacy
14+
/// <c>initialize</c> handshake otherwise.
15+
/// </summary>
16+
#pragma warning disable MCPEXP002 // ExperimentalProtocolVersion
17+
public class DraftConnectionTests : ClientServerTestBase
18+
{
19+
private const string DraftVersion = "2026-06-XX";
20+
private const string LatestStableVersion = "2025-11-25";
21+
22+
private bool _serverHasExperimental;
23+
24+
public DraftConnectionTests(ITestOutputHelper testOutputHelper)
25+
: base(testOutputHelper, startServer: false)
26+
{
27+
}
28+
29+
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
30+
{
31+
services.Configure<McpServerOptions>(options =>
32+
{
33+
options.ServerInfo = new Implementation { Name = nameof(DraftConnectionTests), Version = "1.0" };
34+
if (_serverHasExperimental)
35+
{
36+
options.ExperimentalProtocolVersion = DraftVersion;
37+
}
38+
});
39+
}
40+
41+
[Fact]
42+
public async Task DraftClient_ConnectingToDraftServer_NegotiatesExperimentalVersion()
43+
{
44+
_serverHasExperimental = true;
45+
StartServer();
46+
47+
var options = new McpClientOptions { ExperimentalProtocolVersion = DraftVersion };
48+
await using var client = await CreateMcpClientForServer(options);
49+
50+
Assert.Equal(DraftVersion, client.NegotiatedProtocolVersion);
51+
Assert.NotNull(client.ServerCapabilities);
52+
Assert.Equal(nameof(DraftConnectionTests), client.ServerInfo.Name);
53+
}
54+
55+
[Fact]
56+
public async Task DraftClient_ConnectingToLegacyServer_FallsBackToLegacyInitialize()
57+
{
58+
_serverHasExperimental = false;
59+
StartServer();
60+
61+
var options = new McpClientOptions { ExperimentalProtocolVersion = DraftVersion };
62+
await using var client = await CreateMcpClientForServer(options);
63+
64+
Assert.NotEqual(DraftVersion, client.NegotiatedProtocolVersion);
65+
Assert.Equal(LatestStableVersion, client.NegotiatedProtocolVersion);
66+
}
67+
68+
[Fact]
69+
public async Task LegacyClient_ConnectingToDraftServer_NegotiatesLegacyVersion()
70+
{
71+
_serverHasExperimental = true;
72+
StartServer();
73+
74+
await using var client = await CreateMcpClientForServer();
75+
76+
Assert.NotEqual(DraftVersion, client.NegotiatedProtocolVersion);
77+
}
78+
79+
[Fact]
80+
public async Task LegacyClient_CanCallServerDiscover_EvenWithoutDraftConfigured()
81+
{
82+
// server/discover is registered unconditionally, so a legacy client can probe it
83+
// (e.g., to learn capabilities without doing a second initialize).
84+
_serverHasExperimental = false;
85+
StartServer();
86+
87+
await using var client = await CreateMcpClientForServer();
88+
89+
var response = await client.SendRequestAsync(
90+
new JsonRpcRequest { Method = RequestMethods.ServerDiscover },
91+
TestContext.Current.CancellationToken);
92+
93+
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
94+
Assert.NotNull(discoverResult);
95+
Assert.NotEmpty(discoverResult.SupportedVersions);
96+
Assert.Contains(LatestStableVersion, discoverResult.SupportedVersions);
97+
Assert.Equal(nameof(DraftConnectionTests), discoverResult.ServerInfo.Name);
98+
}
99+
100+
[Fact]
101+
public async Task DraftServer_DiscoverIncludesExperimentalVersion()
102+
{
103+
_serverHasExperimental = true;
104+
StartServer();
105+
106+
await using var client = await CreateMcpClientForServer();
107+
108+
var response = await client.SendRequestAsync(
109+
new JsonRpcRequest { Method = RequestMethods.ServerDiscover },
110+
TestContext.Current.CancellationToken);
111+
112+
var discoverResult = JsonSerializer.Deserialize<DiscoverResult>(response.Result, McpJsonUtilities.DefaultOptions);
113+
Assert.NotNull(discoverResult);
114+
Assert.Contains(DraftVersion, discoverResult.SupportedVersions);
115+
}
116+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using ModelContextProtocol.Protocol;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
5+
namespace ModelContextProtocol.Tests.Protocol;
6+
7+
/// <summary>
8+
/// Serialization tests for the request/result types introduced by the draft protocol revision (SEP-2575).
9+
/// </summary>
10+
public static class DiscoverProtocolTests
11+
{
12+
[Fact]
13+
public static void DiscoverRequestParams_SerializationRoundTrip_WithMeta()
14+
{
15+
var original = new DiscoverRequestParams
16+
{
17+
Meta = new JsonObject
18+
{
19+
[NotificationMethods.ProtocolVersionMetaKey] = "2026-06-XX",
20+
[NotificationMethods.ClientInfoMetaKey] = new JsonObject
21+
{
22+
["name"] = "test-client",
23+
["version"] = "1.0",
24+
},
25+
[NotificationMethods.ClientCapabilitiesMetaKey] = new JsonObject(),
26+
},
27+
};
28+
29+
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
30+
var deserialized = JsonSerializer.Deserialize<DiscoverRequestParams>(json, McpJsonUtilities.DefaultOptions);
31+
32+
Assert.NotNull(deserialized);
33+
Assert.NotNull(deserialized.Meta);
34+
Assert.Equal("2026-06-XX", (string)deserialized.Meta[NotificationMethods.ProtocolVersionMetaKey]!);
35+
}
36+
37+
[Fact]
38+
public static void DiscoverResult_SerializationRoundTrip_PreservesAllProperties()
39+
{
40+
var original = new DiscoverResult
41+
{
42+
SupportedVersions = new List<string> { "2025-11-25", "2026-06-XX" },
43+
Capabilities = new ServerCapabilities
44+
{
45+
Tools = new ToolsCapability { ListChanged = true },
46+
},
47+
ServerInfo = new Implementation { Name = "test-server", Version = "2.0" },
48+
Instructions = "Use this server for testing.",
49+
};
50+
51+
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
52+
var deserialized = JsonSerializer.Deserialize<DiscoverResult>(json, McpJsonUtilities.DefaultOptions);
53+
54+
Assert.NotNull(deserialized);
55+
Assert.Equal(["2025-11-25", "2026-06-XX"], deserialized.SupportedVersions);
56+
Assert.NotNull(deserialized.Capabilities.Tools);
57+
Assert.True(deserialized.Capabilities.Tools.ListChanged);
58+
Assert.Equal("test-server", deserialized.ServerInfo.Name);
59+
Assert.Equal("Use this server for testing.", deserialized.Instructions);
60+
}
61+
62+
[Fact]
63+
public static void DiscoverResult_SerializationRoundTrip_WithMinimalProperties()
64+
{
65+
var original = new DiscoverResult
66+
{
67+
SupportedVersions = new List<string> { "2026-06-XX" },
68+
Capabilities = new ServerCapabilities(),
69+
ServerInfo = new Implementation { Name = "minimal-server", Version = "1.0" },
70+
};
71+
72+
var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
73+
var deserialized = JsonSerializer.Deserialize<DiscoverResult>(json, McpJsonUtilities.DefaultOptions);
74+
75+
Assert.NotNull(deserialized);
76+
Assert.Single(deserialized.SupportedVersions);
77+
Assert.Equal("2026-06-XX", deserialized.SupportedVersions[0]);
78+
Assert.Null(deserialized.Instructions);
79+
}
80+
}

0 commit comments

Comments
 (0)