77using ModelContextProtocol . Server ;
88using ModelContextProtocol . Tests . Utils ;
99using Moq ;
10+ using System . Buffers ;
1011using System . IO . Pipelines ;
1112using System . Text . Json ;
1213using System . Text . Json . Serialization . Metadata ;
@@ -230,21 +231,24 @@ public async ValueTask DisposeAsync()
230231 _cts . Dispose ( ) ;
231232 }
232233
233- private async Task < IMcpClient > CreateMcpClientForServer ( )
234+ private async Task < IMcpClient > CreateMcpClientForServer (
235+ McpClientOptions ? options = null ,
236+ CancellationToken ? cancellationToken = default )
234237 {
235238 return await McpClientFactory . CreateAsync (
236- new McpServerConfig ( )
239+ new ( )
237240 {
238241 Id = "TestServer" ,
239242 Name = "TestServer" ,
240243 TransportType = "ignored" ,
241244 } ,
245+ clientOptions : options ,
242246 createTransportFunc : ( _ , _ ) => new StreamClientTransport (
243247 serverInput : _clientToServerPipe . Writer . AsStream ( ) ,
244248 serverOutput : _serverToClientPipe . Reader . AsStream ( ) ,
245249 LoggerFactory ) ,
246250 loggerFactory : LoggerFactory ,
247- cancellationToken : TestContext . Current . CancellationToken ) ;
251+ cancellationToken : cancellationToken ?? TestContext . Current . CancellationToken ) ;
248252 }
249253
250254 [ Fact ]
@@ -376,4 +380,125 @@ public async Task WithDescription_ChangesToolDescription()
376380 Assert . Equal ( "ToolWithNewDescription" , redescribedTool . Description ) ;
377381 Assert . Equal ( originalDescription , tool ? . Description ) ;
378382 }
383+
384+ [ Fact ]
385+ public async Task Can_Handle_Notify_Cancel ( )
386+ {
387+ // Arrange
388+ var token = TestContext . Current . CancellationToken ;
389+ TaskCompletionSource < JsonRpcNotification > clientReceived = new ( ) ;
390+ await using var client = await CreateMcpClientForServer (
391+ options : CreateClientOptions ( [ new ( NotificationMethods . CancelledNotification , notification =>
392+ {
393+ clientReceived . TrySetResult ( notification ) ;
394+ return clientReceived . Task ;
395+ } ) ] ) ,
396+ cancellationToken : token ) ;
397+ CancelledNotification rpcNotification = new ( )
398+ {
399+ RequestId = new ( "abc" ) ,
400+ Reason = "Cancelled" ,
401+ } ;
402+
403+ // Act
404+ await NotifyClientAsync (
405+ message : NotificationMethods . CancelledNotification ,
406+ parameters : rpcNotification ,
407+ token : token ) ;
408+ var notification = await clientReceived . Task
409+ . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , token ) ;
410+
411+ // Assert
412+ Assert . NotNull ( notification . Params ) ;
413+ // Parse the Params string back to a CancelledNotification
414+ var cancelled = JsonSerializer . Deserialize < CancelledNotification > ( notification . Params . ToString ( ) ) ;
415+ Assert . NotNull ( cancelled ) ;
416+ Assert . Equal ( rpcNotification . RequestId . ToString ( ) , cancelled . RequestId . ToString ( ) ) ;
417+ Assert . Equal ( rpcNotification . Reason , cancelled . Reason ) ;
418+ }
419+
420+ [ Fact ]
421+ public async Task Should_Not_Intercept_Sent_Notifications ( )
422+ {
423+ // Arrange
424+ var token = TestContext . Current . CancellationToken ;
425+ TaskCompletionSource < JsonRpcNotification > clientReceived = new ( ) ;
426+ await using var client = await CreateMcpClientForServer (
427+ options : CreateClientOptions ( [ new ( NotificationMethods . CancelledNotification , notification =>
428+ {
429+ var exception = new InvalidOperationException ( "Should not intercept sent notifications" ) ;
430+ clientReceived . TrySetException ( exception ) ;
431+ return clientReceived . Task ;
432+ } ) ] ) ,
433+ cancellationToken : token ) ;
434+
435+ // Act
436+ await client . NotifyCancelAsync (
437+ requestId : new ( "abc" ) ,
438+ reason : "Cancelled" ,
439+ cancellationToken : token ) ;
440+ await Assert . ThrowsAsync < TimeoutException > (
441+ async ( ) => await clientReceived . Task
442+ . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , token ) ) ;
443+ // Assert
444+ Assert . False ( clientReceived . Task . IsCompleted ) ;
445+ }
446+
447+ [ Fact ]
448+ public async Task Can_Notify_Cancel ( )
449+ {
450+ // Arrange
451+ var token = TestContext . Current . CancellationToken ;
452+ await using var client = await CreateMcpClientForServer (
453+ cancellationToken : token ) ;
454+ RequestId expectedRequestId = new ( "abc" ) ;
455+ var expectedReason = "Cancelled" ;
456+
457+ // Act
458+ await client . NotifyCancelAsync (
459+ requestId : expectedRequestId ,
460+ reason : expectedReason ,
461+ cancellationToken : token ) ;
462+
463+ // Assert
464+ var result = await _clientToServerPipe . Reader . ReadAsync ( token ) ;
465+ var copyBytes = new byte [ result . Buffer . Length ] ;
466+ result . Buffer . CopyTo ( copyBytes ) ;
467+ Utf8JsonReader reader = new ( copyBytes ) ;
468+ var jsonRpcNotification = JsonSerializer . Deserialize < JsonRpcNotification > ( ref reader ) ;
469+ Assert . NotNull ( jsonRpcNotification ) ;
470+ Assert . Equal ( NotificationMethods . CancelledNotification , jsonRpcNotification . Method ) ;
471+
472+ var parameters = jsonRpcNotification . Params ;
473+ Assert . NotNull ( parameters ) ;
474+ var cancelledNotification = JsonSerializer . Deserialize < CancelledNotification > ( parameters . ToString ( ) ) ;
475+ Assert . NotNull ( cancelledNotification ) ;
476+ Assert . Equal ( expectedRequestId . ToString ( ) , cancelledNotification . RequestId . ToString ( ) ) ;
477+ Assert . Equal ( expectedReason , cancelledNotification . Reason ) ;
478+ }
479+
480+ private McpClientOptions CreateClientOptions (
481+ IEnumerable < KeyValuePair < string , Func < JsonRpcNotification , Task > > > ? notificationHandlers = null )
482+ => new ( )
483+ {
484+ Capabilities = new ( )
485+ {
486+ NotificationHandlers = notificationHandlers ?? [ ] ,
487+ } ,
488+ } ;
489+
490+ private async Task NotifyClientAsync (
491+ string message , object ? parameters = null , CancellationToken token = default )
492+ => await NotifyPipeAsync ( _serverToClientPipe , message , parameters , token ) ;
493+ private async static Task NotifyPipeAsync (
494+ Pipe pipe , string message , object ? parameters = null , CancellationToken token = default )
495+ {
496+ var bytes = JsonSerializer . SerializeToUtf8Bytes ( new JsonRpcNotification
497+ {
498+ Method = message ,
499+ Params = parameters is not null ? JsonSerializer . Serialize ( parameters ) : null ,
500+ } ) ;
501+ await pipe . Writer . WriteAsync ( bytes , token ) ;
502+ await pipe . Writer . CompleteAsync ( ) ; // Signal the end of the message
503+ }
379504}
0 commit comments