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
|
|
@ -10,6 +10,13 @@ namespace ModelContextProtocol.AspNetCore.Tests;
|
|||
|
||||
public abstract partial class MapMcpTests
|
||||
{
|
||||
// Draft is sessionless (SEP-2567): the Streamable HTTP handler refuses a sessionless draft request
|
||||
// when the server opted into sessions (Stateless = false), so a draft-pinned client downgrades to
|
||||
// legacy instead of negotiating 2026-07-28. These draft MRTR tests therefore can't run on the
|
||||
// stateful Streamable HTTP fixture; the same coverage runs on the stateless and legacy-SSE fixtures.
|
||||
private const string DraftStatefulStreamableHttpSkipReason =
|
||||
"Draft is sessionless (SEP-2567); stateful Streamable HTTP refuses sessionless draft. Covered by the stateless and SSE fixtures.";
|
||||
|
||||
private ServerMessageTracker ConfigureServer(params Delegate[] tools)
|
||||
{
|
||||
var messageTracker = new ServerMessageTracker();
|
||||
|
|
@ -17,7 +24,7 @@ public abstract partial class MapMcpTests
|
|||
{
|
||||
options.ServerInfo = new Implementation { Name = "MrtrTestServer", Version = "1" };
|
||||
// Do not pin a protocol version - let it be negotiated based on what the client requests.
|
||||
// DRAFT-2026-v1 is in SupportedProtocolVersions, so an opt-in client gets it; others get
|
||||
// 2026-07-28 is in SupportedProtocolVersions, so an opt-in client gets it; others get
|
||||
// the latest non-draft.
|
||||
messageTracker.AddFilters(options.Filters.Message);
|
||||
})
|
||||
|
|
@ -30,11 +37,17 @@ public abstract partial class MapMcpTests
|
|||
ConnectAsync(configureClient: options =>
|
||||
{
|
||||
ConfigureMrtrHandlers(options);
|
||||
options.ProtocolVersion = "DRAFT-2026-v1";
|
||||
options.ProtocolVersion = "2026-07-28";
|
||||
});
|
||||
|
||||
private Task<McpClient> ConnectDefaultAsync() =>
|
||||
ConnectAsync(configureClient: ConfigureMrtrHandlers);
|
||||
// The default client now negotiates draft (2026-07-28). The legacy JSON-RPC MRTR back-compat
|
||||
// resolver only applies to legacy clients, so pin these to the latest non-draft version.
|
||||
private Task<McpClient> ConnectLegacyAsync() =>
|
||||
ConnectAsync(configureClient: options =>
|
||||
{
|
||||
ConfigureMrtrHandlers(options);
|
||||
options.ProtocolVersion = "2025-11-25";
|
||||
});
|
||||
|
||||
/// <summary>Configures elicitation, sampling, and roots handlers on client options.</summary>
|
||||
private static void ConfigureMrtrHandlers(McpClientOptions options)
|
||||
|
|
@ -79,7 +92,7 @@ public abstract partial class MapMcpTests
|
|||
|
||||
// =====================================================================
|
||||
// MRTR tests: experimental (native), backcompat (legacy JSON-RPC), and edge cases.
|
||||
// Each test creates its own server with DRAFT-2026-v1 enabled.
|
||||
// Each test creates its own server with 2026-07-28 enabled.
|
||||
// =====================================================================
|
||||
|
||||
[McpServerTool(Name = "mrtr-mixed")]
|
||||
|
|
@ -156,8 +169,15 @@ public abstract partial class MapMcpTests
|
|||
[InlineData(true)]
|
||||
public async Task Mrtr_MixedExceptionAndAwaitStyle(bool experimentalClient)
|
||||
{
|
||||
// The server always supports DRAFT-2026-v1 (it's in SupportedProtocolVersions). The
|
||||
// client opts in by pinning ProtocolVersion = "DRAFT-2026-v1"; otherwise it negotiates
|
||||
// The await-style portion of this tool calls server.SampleAsync/ElicitAsync on round 3,
|
||||
// which requires server-to-client requests - only available in stateful sessions. Under
|
||||
// the draft protocol revision (SEP-2567), Streamable HTTP is implicitly stateless, so the
|
||||
// experimental-client + HTTP combination cannot resolve the await-style portion. Stdio
|
||||
// coverage for this scenario lives in DraftProtocolBackcompatTests.
|
||||
Assert.SkipWhen(experimentalClient, "Await-style MRTR requires session affinity; draft protocol revision (SEP-2567) makes Streamable HTTP sessionless. See DraftProtocolBackcompatTests for stdio coverage.");
|
||||
|
||||
// The server always supports 2026-07-28 (it's in SupportedProtocolVersions). The
|
||||
// client opts in by pinning ProtocolVersion = "2026-07-28"; otherwise it negotiates
|
||||
// the latest non-draft version and the server falls back to the exception path with
|
||||
// legacy JSON-RPC resolution.
|
||||
var messageTracker = ConfigureServer(MrtrMixed);
|
||||
|
|
@ -167,8 +187,9 @@ public abstract partial class MapMcpTests
|
|||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Action<McpClientOptions> configureClient = experimentalClient
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "DRAFT-2026-v1"; }
|
||||
: ConfigureMrtrHandlers;
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; }
|
||||
// ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage.
|
||||
: options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; };
|
||||
|
||||
// The await-style portion of this tool calls server.SampleAsync/ElicitAsync on round 3.
|
||||
// In stateless mode, those calls succeed only when the request is still open on the same
|
||||
|
|
@ -180,6 +201,7 @@ public abstract partial class MapMcpTests
|
|||
// and no persistent server instance for the backcompat retry loop). The server returns
|
||||
// a JSON-RPC error.
|
||||
await using var client = await ConnectAsync(configureClient: configureClient);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpProtocolException>(() =>
|
||||
client.CallToolAsync("mrtr-mixed",
|
||||
cancellationToken: TestContext.Current.CancellationToken).AsTask());
|
||||
|
|
@ -202,7 +224,7 @@ public abstract partial class MapMcpTests
|
|||
// Stateful path - both client modes complete all 3 rounds.
|
||||
await using var statefulClient = await ConnectAsync(configureClient: configureClient);
|
||||
|
||||
Assert.Equal(experimentalClient ? "DRAFT-2026-v1" : "2025-11-25",
|
||||
Assert.Equal(experimentalClient ? "2026-07-28" : "2025-11-25",
|
||||
statefulClient.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await statefulClient.CallToolAsync("mrtr-mixed",
|
||||
|
|
@ -267,6 +289,10 @@ public abstract partial class MapMcpTests
|
|||
// Parallel awaits work with regular JSON-RPC but fail with MRTR because
|
||||
// MrtrContext only supports one exchange at a time (TrySetResult gate).
|
||||
Assert.SkipWhen(Stateless, "Await-style API requires handler suspension (stateful only).");
|
||||
// Under the draft protocol revision (SEP-2567), the server is implicitly stateless for draft
|
||||
// clients, so parallel-await MRTR can't reach its concurrency gate. Skip the experimental-client
|
||||
// case for the same reason as Mrtr_MixedExceptionAndAwaitStyle.
|
||||
Assert.SkipWhen(experimentalClient, "Await-style MRTR requires session affinity; draft protocol revision (SEP-2567) is sessionless.");
|
||||
|
||||
ConfigureServer(MrtrParallelAwait);
|
||||
await using var app = Builder.Build();
|
||||
|
|
@ -274,8 +300,9 @@ public abstract partial class MapMcpTests
|
|||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Action<McpClientOptions> configureClient = experimentalClient
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "DRAFT-2026-v1"; }
|
||||
: ConfigureMrtrHandlers;
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; }
|
||||
// ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage.
|
||||
: options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; };
|
||||
|
||||
await using var client = await ConnectAsync(configureClient: configureClient);
|
||||
|
||||
|
|
@ -283,7 +310,7 @@ public abstract partial class MapMcpTests
|
|||
{
|
||||
// MRTR active. Parallel awaits hit the MrtrContext concurrency gate and the second
|
||||
// call throws InvalidOperationException, which the tool catches and returns as text.
|
||||
Assert.Equal("DRAFT-2026-v1", client.NegotiatedProtocolVersion);
|
||||
Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-parallel-await",
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
|
@ -330,6 +357,8 @@ public abstract partial class MapMcpTests
|
|||
[Fact]
|
||||
public async Task Mrtr_Roots_CompletesViaMrtr()
|
||||
{
|
||||
Assert.SkipWhen(UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason);
|
||||
|
||||
var messageTracker = ConfigureServer(
|
||||
[McpServerTool(Name = "mrtr-roots")] (RequestContext<CallToolRequestParams> context) =>
|
||||
{
|
||||
|
|
@ -351,7 +380,7 @@ public abstract partial class MapMcpTests
|
|||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
await using var client = await ConnectExperimentalAsync();
|
||||
Assert.Equal("DRAFT-2026-v1", client.NegotiatedProtocolVersion);
|
||||
Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-roots",
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
|
@ -406,6 +435,8 @@ public abstract partial class MapMcpTests
|
|||
[InlineData(false)]
|
||||
public async Task Mrtr_MultiRoundTrip_Completes(bool experimentalClient)
|
||||
{
|
||||
Assert.SkipWhen(experimentalClient && UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason);
|
||||
|
||||
var messageTracker = ConfigureServer(MrtrMulti);
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
|
|
@ -413,8 +444,9 @@ public abstract partial class MapMcpTests
|
|||
|
||||
// Configure client - experimental or default based on parameter.
|
||||
Action<McpClientOptions> configureClient = experimentalClient
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "DRAFT-2026-v1"; }
|
||||
: ConfigureMrtrHandlers;
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; }
|
||||
// ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage.
|
||||
: options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; };
|
||||
await using var client = await ConnectAsync(configureClient: configureClient);
|
||||
|
||||
if (!experimentalClient && Stateless)
|
||||
|
|
@ -437,7 +469,7 @@ public abstract partial class MapMcpTests
|
|||
|
||||
if (experimentalClient)
|
||||
{
|
||||
Assert.Equal("DRAFT-2026-v1", client.NegotiatedProtocolVersion);
|
||||
Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion);
|
||||
messageTracker.AssertMrtrUsed();
|
||||
}
|
||||
else
|
||||
|
|
@ -452,6 +484,8 @@ public abstract partial class MapMcpTests
|
|||
[InlineData(false)]
|
||||
public async Task Mrtr_IsMrtrSupported(bool experimentalClient)
|
||||
{
|
||||
Assert.SkipWhen(experimentalClient && UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason);
|
||||
|
||||
ConfigureServer([McpServerTool(Name = "mrtr-check")] (McpServer server) => server.IsMrtrSupported.ToString());
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
|
|
@ -459,10 +493,11 @@ public abstract partial class MapMcpTests
|
|||
|
||||
// Configure client - experimental or default based on parameter.
|
||||
Action<McpClientOptions> configureClient = experimentalClient
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "DRAFT-2026-v1"; }
|
||||
: ConfigureMrtrHandlers;
|
||||
? options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2026-07-28"; }
|
||||
// ProtocolVersion null now defaults to draft, so pin the legacy client explicitly to keep dual-era coverage.
|
||||
: options => { ConfigureMrtrHandlers(options); options.ProtocolVersion = "2025-11-25"; };
|
||||
await using var client = await ConnectAsync(configureClient: configureClient);
|
||||
Assert.Equal(experimentalClient ? "DRAFT-2026-v1" : "2025-11-25", client.NegotiatedProtocolVersion);
|
||||
Assert.Equal(experimentalClient ? "2026-07-28" : "2025-11-25", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-check",
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
|
@ -515,6 +550,8 @@ public abstract partial class MapMcpTests
|
|||
[Fact]
|
||||
public async Task Mrtr_ConcurrentThreeInputs_ResolvedSimultaneously()
|
||||
{
|
||||
Assert.SkipWhen(UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason);
|
||||
|
||||
var messageTracker = ConfigureServer(MrtrConcurrentThree);
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
|
|
@ -526,7 +563,7 @@ public abstract partial class MapMcpTests
|
|||
|
||||
await using var client = await ConnectAsync(configureClient: options =>
|
||||
{
|
||||
options.ProtocolVersion = "DRAFT-2026-v1";
|
||||
options.ProtocolVersion = "2026-07-28";
|
||||
options.Handlers.ElicitationHandler = async (request, ct) =>
|
||||
{
|
||||
elicitCalled.TrySetResult();
|
||||
|
|
@ -553,7 +590,7 @@ public abstract partial class MapMcpTests
|
|||
};
|
||||
};
|
||||
});
|
||||
Assert.Equal("DRAFT-2026-v1", client.NegotiatedProtocolVersion);
|
||||
Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-concurrent-three",
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
|
@ -567,6 +604,8 @@ public abstract partial class MapMcpTests
|
|||
[Fact]
|
||||
public async Task Mrtr_LoadShedding_RequestStateOnly_CompletesViaMrtr()
|
||||
{
|
||||
Assert.SkipWhen(UseStreamableHttp && !Stateless, DraftStatefulStreamableHttpSkipReason);
|
||||
|
||||
var messageTracker = ConfigureServer(
|
||||
[McpServerTool(Name = "mrtr-loadshed")] (RequestContext<CallToolRequestParams> context) =>
|
||||
{
|
||||
|
|
@ -582,7 +621,7 @@ public abstract partial class MapMcpTests
|
|||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
await using var client = await ConnectExperimentalAsync();
|
||||
Assert.Equal("DRAFT-2026-v1", client.NegotiatedProtocolVersion);
|
||||
Assert.Equal("2026-07-28", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-loadshed",
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
|
@ -617,7 +656,7 @@ public abstract partial class MapMcpTests
|
|||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
await using var client = await ConnectDefaultAsync();
|
||||
await using var client = await ConnectLegacyAsync();
|
||||
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-roots-backcompat",
|
||||
|
|
@ -668,7 +707,7 @@ public abstract partial class MapMcpTests
|
|||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
await using var client = await ConnectDefaultAsync();
|
||||
await using var client = await ConnectLegacyAsync();
|
||||
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
|
||||
|
||||
var result = await client.CallToolAsync("mrtr-multi-input",
|
||||
|
|
@ -707,6 +746,7 @@ public abstract partial class MapMcpTests
|
|||
await using var client = await ConnectAsync(configureClient: options =>
|
||||
{
|
||||
ConfigureMrtrHandlers(options);
|
||||
options.ProtocolVersion = "2025-11-25";
|
||||
var originalHandler = options.Handlers.ElicitationHandler!;
|
||||
options.Handlers.ElicitationHandler = (request, ct) =>
|
||||
{
|
||||
|
|
@ -739,7 +779,7 @@ public abstract partial class MapMcpTests
|
|||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
await using var client = await ConnectDefaultAsync();
|
||||
await using var client = await ConnectLegacyAsync();
|
||||
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpProtocolException>(() =>
|
||||
|
|
@ -762,6 +802,7 @@ public abstract partial class MapMcpTests
|
|||
await using var client = await ConnectAsync(configureClient: options =>
|
||||
{
|
||||
ConfigureMrtrHandlers(options);
|
||||
options.ProtocolVersion = "2025-11-25";
|
||||
options.Handlers.ElicitationHandler = (request, ct) =>
|
||||
{
|
||||
throw new InvalidOperationException("Client-side elicitation failure");
|
||||
|
|
|
|||
Loading…
Reference in a new issue