@@ -10,27 +10,15 @@ namespace ModelContextProtocol.AspNetCore.Tests;
1010
1111public abstract partial class MapMcpTests
1212{
13- private ServerMessageTracker ConfigureExperimentalServer ( params Delegate [ ] tools )
13+ private ServerMessageTracker ConfigureServer ( params Delegate [ ] tools )
1414 {
1515 var messageTracker = new ServerMessageTracker ( ) ;
1616 Builder . Services . AddMcpServer ( options =>
1717 {
18- options . ServerInfo = new Implementation { Name = "ExperimentalServer" , Version = "1" } ;
19- // Don't pin a protocol version here — let it be negotiated based on what the client
20- // requests. DRAFT-2026-v1 is in SupportedProtocolVersions, so an opt-in client gets it.
21- messageTracker . AddFilters ( options . Filters . Message ) ;
22- } )
23- . WithHttpTransport ( ConfigureStateless )
24- . WithTools ( tools . Select ( t => McpServerTool . Create ( t ) ) ) ;
25- return messageTracker ;
26- }
27-
28- private ServerMessageTracker ConfigureDefaultServer ( params Delegate [ ] tools )
29- {
30- var messageTracker = new ServerMessageTracker ( ) ;
31- Builder . Services . AddMcpServer ( options =>
32- {
33- options . ServerInfo = new Implementation { Name = "DefaultServer" , Version = "1" } ;
18+ options . ServerInfo = new Implementation { Name = "MrtrTestServer" , Version = "1" } ;
19+ // Do not pin a protocol version — let it be negotiated based on what the client requests.
20+ // DRAFT-2026-v1 is in SupportedProtocolVersions, so an opt-in client gets it; others get
21+ // the latest non-draft.
3422 messageTracker . AddFilters ( options . Filters . Message ) ;
3523 } )
3624 . WithHttpTransport ( ConfigureStateless )
@@ -164,101 +152,76 @@ private static async Task<string> MrtrMixed(McpServer server, RequestContext<Cal
164152 }
165153
166154 [ Theory ]
167- [ InlineData ( true , true ) ]
168- [ InlineData ( true , false ) ]
169- [ InlineData ( false , true ) ]
170- [ InlineData ( false , false ) ]
171- public async Task Mrtr_MixedExceptionAndAwaitStyle ( bool experimentalServer , bool experimentalClient )
155+ [ InlineData ( true ) ]
156+ [ InlineData ( false ) ]
157+ public async Task Mrtr_MixedExceptionAndAwaitStyle ( bool experimentalClient )
172158 {
173- // Configure server — experimental or default based on parameter.
174- var messageTracker = experimentalServer
175- ? ConfigureExperimentalServer ( MrtrMixed )
176- : ConfigureDefaultServer ( MrtrMixed ) ;
159+ // The server always supports DRAFT-2026-v1 (it's in SupportedProtocolVersions). The
160+ // client opts in by pinning ProtocolVersion = "DRAFT-2026-v1"; otherwise it negotiates
161+ // the latest non-draft version and the server falls back to the exception path with
162+ // legacy JSON-RPC resolution.
163+ var messageTracker = ConfigureServer ( MrtrMixed ) ;
177164
178165 await using var app = Builder . Build ( ) ;
179166 app . MapMcp ( ) ;
180167 await app . StartAsync ( TestContext . Current . CancellationToken ) ;
181168
182- // Configure client — experimental or default based on parameter.
183169 Action < McpClientOptions > configureClient = experimentalClient
184170 ? options => { ConfigureMrtrHandlers ( options ) ; options . ProtocolVersion = "DRAFT-2026-v1" ; }
185171 : ConfigureMrtrHandlers ;
186172
187- if ( experimentalServer )
188- {
189- // Success cases: both exception and await APIs complete.
190- // Skip stateless — await API requires handler suspension (stateful only).
191- Assert . SkipWhen ( Stateless , "Await-style API requires handler suspension (stateful only)." ) ;
192-
193- await using var client = await ConnectAsync ( configureClient : configureClient ) ;
194-
195- if ( experimentalClient )
196- {
197- // Both experimental — MRTR end-to-end.
198- Assert . Equal ( "DRAFT-2026-v1" , client . NegotiatedProtocolVersion ) ;
199- }
200- else
201- {
202- // Backcompat — server experimental, client default. Legacy JSON-RPC.
203- Assert . Equal ( "2025-11-25" , client . NegotiatedProtocolVersion ) ;
204- }
205-
206- var result = await client . CallToolAsync ( "mrtr-mixed" ,
207- cancellationToken : TestContext . Current . CancellationToken ) ;
208-
209- var text = Assert . IsType < TextContentBlock > ( Assert . Single ( result . Content ) ) . Text ;
210- Assert . True ( result . IsError is not true ) ;
211- var parts = text . Split ( '|' ) ;
212- Assert . Equal ( 3 , parts . Length ) ;
213-
214- // confirmation from round 2 elicitation
215- Assert . Equal ( "accept" , parts [ 0 ] ) ;
216- // greeting from await SampleAsync — our test handler returns "LLM:{prompt}"
217- Assert . StartsWith ( "LLM:" , parts [ 1 ] ) ;
218- // signoff from await ElicitAsync
219- Assert . Equal ( "accept" , parts [ 2 ] ) ;
220-
221- if ( experimentalClient )
222- {
223- messageTracker . AssertMrtrUsed ( ) ;
224- }
225- else
226- {
227- messageTracker . AssertMrtrNotUsed ( ) ;
228- }
229- }
230- else if ( Stateless )
173+ // The await-style portion of this tool calls server.SampleAsync/ElicitAsync on round 3.
174+ // In stateless mode, those calls succeed only when the request is still open on the same
175+ // SSE stream — which it is — so the tool runs end-to-end as long as the input requests
176+ // themselves can be resolved (MRTR client) or replayed via legacy JSON-RPC (stateful + legacy).
177+ if ( Stateless && ! experimentalClient )
231178 {
232- // Stateless + non-experimental: InputRequiredException cannot be resolved
233- // (no MRTR and no stateful backcompat). The server returns an error.
179+ // Stateless + legacy client: InputRequiredException cannot be resolved (no MRTR wire
180+ // and no persistent server instance for the backcompat retry loop). The server returns
181+ // a JSON-RPC error.
234182 await using var client = await ConnectAsync ( configureClient : configureClient ) ;
235-
236183 var ex = await Assert . ThrowsAsync < McpProtocolException > ( ( ) =>
237184 client . CallToolAsync ( "mrtr-mixed" ,
238185 cancellationToken : TestContext . Current . CancellationToken ) . AsTask ( ) ) ;
239186
240187 Assert . Equal ( McpErrorCode . InternalError , ex . ErrorCode ) ;
241188 Assert . Contains ( "stateless" , ex . Message , StringComparison . OrdinalIgnoreCase ) ;
242189 Assert . Contains ( "MRTR" , ex . Message ) ;
190+ return ;
243191 }
244- else
192+
193+ if ( Stateless && experimentalClient )
245194 {
246- // Stateful + non-experimental: backcompat resolves InputRequiredException
247- // via legacy JSON-RPC requests. The tool completes all 3 rounds.
248- await using var client = await ConnectAsync ( configureClient : configureClient ) ;
195+ // Stateless + MRTR client: the await-style portion (server.SampleAsync on round 3)
196+ // requires handler suspension across requests, which only works in stateful mode.
197+ // Skip this combination — the await API is documented as stateful-only.
198+ Assert . SkipWhen ( true , "Await-style API requires handler suspension (stateful only)." ) ;
199+ return ;
200+ }
249201
250- var result = await client . CallToolAsync ( "mrtr-mixed" ,
251- cancellationToken : TestContext . Current . CancellationToken ) ;
202+ // Stateful path — both client modes complete all 3 rounds.
203+ await using var statefulClient = await ConnectAsync ( configureClient : configureClient ) ;
252204
253- var text = Assert . IsType < TextContentBlock > ( Assert . Single ( result . Content ) ) . Text ;
254- Assert . True ( result . IsError is not true ) ;
255- var parts = text . Split ( '|' ) ;
256- Assert . Equal ( 3 , parts . Length ) ;
205+ Assert . Equal ( experimentalClient ? "DRAFT-2026-v1" : "2025-11-25" ,
206+ statefulClient . NegotiatedProtocolVersion ) ;
257207
258- Assert . Equal ( "accept" , parts [ 0 ] ) ;
259- Assert . StartsWith ( "LLM:" , parts [ 1 ] ) ;
260- Assert . Equal ( "accept" , parts [ 2 ] ) ;
208+ var result = await statefulClient . CallToolAsync ( "mrtr-mixed" ,
209+ cancellationToken : TestContext . Current . CancellationToken ) ;
261210
211+ var text = Assert . IsType < TextContentBlock > ( Assert . Single ( result . Content ) ) . Text ;
212+ Assert . True ( result . IsError is not true ) ;
213+ var parts = text . Split ( '|' ) ;
214+ Assert . Equal ( 3 , parts . Length ) ;
215+ Assert . Equal ( "accept" , parts [ 0 ] ) ;
216+ Assert . StartsWith ( "LLM:" , parts [ 1 ] ) ;
217+ Assert . Equal ( "accept" , parts [ 2 ] ) ;
218+
219+ if ( experimentalClient )
220+ {
221+ messageTracker . AssertMrtrUsed ( ) ;
222+ }
223+ else
224+ {
262225 messageTracker . AssertMrtrNotUsed ( ) ;
263226 }
264227 }
@@ -295,34 +258,28 @@ private static async Task<string> MrtrParallelAwait(McpServer server, Cancellati
295258 }
296259
297260 [ Theory ]
298- [ InlineData ( true , true ) ]
299- [ InlineData ( true , false ) ]
300- [ InlineData ( false , true ) ]
301- [ InlineData ( false , false ) ]
302- public async Task Mrtr_ParallelAwaits ( bool experimentalServer , bool experimentalClient )
261+ [ InlineData ( true ) ]
262+ [ InlineData ( false ) ]
263+ public async Task Mrtr_ParallelAwaits ( bool experimentalClient )
303264 {
304265 // Parallel awaits work with regular JSON-RPC but fail with MRTR because
305266 // MrtrContext only supports one exchange at a time (TrySetResult gate).
306267 Assert . SkipWhen ( Stateless , "Await-style API requires handler suspension (stateful only)." ) ;
307268
308- var messageTracker = experimentalServer
309- ? ConfigureExperimentalServer ( MrtrParallelAwait )
310- : ConfigureDefaultServer ( MrtrParallelAwait ) ;
269+ var messageTracker = ConfigureServer ( MrtrParallelAwait ) ;
311270 await using var app = Builder . Build ( ) ;
312271 app . MapMcp ( ) ;
313272 await app . StartAsync ( TestContext . Current . CancellationToken ) ;
314273
315- // Configure client — experimental or default based on parameter.
316274 Action < McpClientOptions > configureClient = experimentalClient
317275 ? options => { ConfigureMrtrHandlers ( options ) ; options . ProtocolVersion = "DRAFT-2026-v1" ; }
318276 : ConfigureMrtrHandlers ;
319277 await using var client = await ConnectAsync ( configureClient : configureClient ) ;
320278
321- if ( experimentalServer && experimentalClient )
279+ if ( experimentalClient )
322280 {
323- // Both experimental — MRTR active. Parallel awaits hit the MrtrContext
324- // concurrency gate and the second call throws InvalidOperationException,
325- // which the tool catches and returns as text.
281+ // MRTR active. Parallel awaits hit the MrtrContext concurrency gate and the second
282+ // call throws InvalidOperationException, which the tool catches and returns as text.
326283 Assert . Equal ( "DRAFT-2026-v1" , client . NegotiatedProtocolVersion ) ;
327284
328285 var result = await client . CallToolAsync ( "mrtr-parallel-await" ,
@@ -370,7 +327,7 @@ private static string MrtrElicit(RequestContext<CallToolRequestParams> context)
370327 [ Fact ]
371328 public async Task Mrtr_LowLevel_Roots_CompletesViaMrtr ( )
372329 {
373- var messageTracker = ConfigureExperimentalServer (
330+ var messageTracker = ConfigureServer (
374331 [ McpServerTool ( Name = "mrtr-roots" ) ] ( RequestContext < CallToolRequestParams > context ) =>
375332 {
376333 if ( context . Params ! . InputResponses is { } responses &&
@@ -446,7 +403,7 @@ private static string MrtrMulti(RequestContext<CallToolRequestParams> context)
446403 [ InlineData ( false ) ]
447404 public async Task Mrtr_MultiRoundTrip_Completes ( bool experimentalClient )
448405 {
449- var messageTracker = ConfigureExperimentalServer ( MrtrMulti ) ;
406+ var messageTracker = ConfigureServer ( MrtrMulti ) ;
450407 await using var app = Builder . Build ( ) ;
451408 app . MapMcp ( ) ;
452409 await app . StartAsync ( TestContext . Current . CancellationToken ) ;
@@ -492,7 +449,7 @@ public async Task Mrtr_MultiRoundTrip_Completes(bool experimentalClient)
492449 [ InlineData ( false ) ]
493450 public async Task Mrtr_IsMrtrSupported ( bool experimentalClient )
494451 {
495- ConfigureExperimentalServer ( [ McpServerTool ( Name = "mrtr-check" ) ] ( McpServer server ) => server . IsMrtrSupported . ToString ( ) ) ;
452+ ConfigureServer ( [ McpServerTool ( Name = "mrtr-check" ) ] ( McpServer server ) => server . IsMrtrSupported . ToString ( ) ) ;
496453 await using var app = Builder . Build ( ) ;
497454 app . MapMcp ( ) ;
498455 await app . StartAsync ( TestContext . Current . CancellationToken ) ;
@@ -555,7 +512,7 @@ private static string MrtrConcurrentThree(RequestContext<CallToolRequestParams>
555512 [ Fact ]
556513 public async Task Mrtr_ConcurrentThreeInputs_ResolvedSimultaneously ( )
557514 {
558- var messageTracker = ConfigureExperimentalServer ( MrtrConcurrentThree ) ;
515+ var messageTracker = ConfigureServer ( MrtrConcurrentThree ) ;
559516 await using var app = Builder . Build ( ) ;
560517 app . MapMcp ( ) ;
561518 await app . StartAsync ( TestContext . Current . CancellationToken ) ;
@@ -607,7 +564,7 @@ public async Task Mrtr_ConcurrentThreeInputs_ResolvedSimultaneously()
607564 [ Fact ]
608565 public async Task Mrtr_Experimental_LoadShedding_RequestStateOnly_CompletesViaMrtr ( )
609566 {
610- var messageTracker = ConfigureExperimentalServer (
567+ var messageTracker = ConfigureServer (
611568 [ McpServerTool ( Name = "mrtr-loadshed" ) ] ( RequestContext < CallToolRequestParams > context ) =>
612569 {
613570 if ( context . Params ! . RequestState is { } state )
@@ -637,7 +594,7 @@ public async Task Mrtr_Experimental_LoadShedding_RequestStateOnly_CompletesViaMr
637594 public async Task Mrtr_Backcompat_Roots_ResolvedViaLegacyJsonRpc ( )
638595 {
639596 Assert . SkipWhen ( Stateless , "Backcompat requires stateful server for legacy JSON-RPC." ) ;
640- var messageTracker = ConfigureExperimentalServer (
597+ var messageTracker = ConfigureServer (
641598 [ McpServerTool ( Name = "mrtr-roots-backcompat" ) ] ( RequestContext < CallToolRequestParams > context ) =>
642599 {
643600 if ( context . Params ! . InputResponses is { } responses &&
@@ -673,7 +630,7 @@ public async Task Mrtr_Backcompat_Roots_ResolvedViaLegacyJsonRpc()
673630 public async Task Mrtr_Backcompat_MultipleInputRequests_ResolvedViaLegacyJsonRpc ( )
674631 {
675632 Assert . SkipWhen ( Stateless , "Backcompat requires stateful server for legacy JSON-RPC." ) ;
676- var messageTracker = ConfigureExperimentalServer (
633+ var messageTracker = ConfigureServer (
677634 [ McpServerTool ( Name = "mrtr-multi-input" ) ] ( RequestContext < CallToolRequestParams > context ) =>
678635 {
679636 if ( context . Params ! . InputResponses is { } responses &&
@@ -726,7 +683,7 @@ public async Task Mrtr_Backcompat_AlwaysIncomplete_FailsAfterMaxRetries()
726683 Assert . SkipWhen ( Stateless , "Backcompat requires stateful server for legacy JSON-RPC." ) ;
727684 int elicitCallCount = 0 ;
728685
729- ConfigureExperimentalServer (
686+ ConfigureServer (
730687 [ McpServerTool ( Name = "mrtr-always-incomplete" ) ] ( RequestContext < CallToolRequestParams > context ) =>
731688 {
732689 // Always throw — never complete
@@ -769,7 +726,7 @@ public async Task Mrtr_Backcompat_AlwaysIncomplete_FailsAfterMaxRetries()
769726 public async Task Mrtr_Backcompat_EmptyInputRequests_FailsWithError ( )
770727 {
771728 Assert . SkipWhen ( Stateless , "Backcompat requires stateful server for legacy JSON-RPC." ) ;
772- ConfigureExperimentalServer (
729+ ConfigureServer (
773730 [ McpServerTool ( Name = "mrtr-empty-inputs" ) ] ( RequestContext < CallToolRequestParams > context ) =>
774731 {
775732 throw new InputRequiredException (
@@ -795,7 +752,7 @@ public async Task Mrtr_Backcompat_ClientHandlerThrows_PropagatesError()
795752 {
796753 Assert . SkipWhen ( Stateless , "Backcompat requires stateful server for legacy JSON-RPC." ) ;
797754
798- ConfigureExperimentalServer ( MrtrElicit ) ;
755+ ConfigureServer ( MrtrElicit ) ;
799756 await using var app = Builder . Build ( ) ;
800757 app . MapMcp ( ) ;
801758 await app . StartAsync ( TestContext . Current . CancellationToken ) ;
0 commit comments