Skip to content

Commit 4f3a74e

Browse files
updated tests
1 parent eae1730 commit 4f3a74e

File tree

6 files changed

+190
-245
lines changed

6 files changed

+190
-245
lines changed

tests/ModelContextProtocol.Tests/CancelledNotificationTests.cs

Lines changed: 0 additions & 99 deletions
This file was deleted.

tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using ModelContextProtocol.Server;
88
using ModelContextProtocol.Tests.Utils;
99
using Moq;
10+
using System.Buffers;
1011
using System.IO.Pipelines;
1112
using System.Text.Json;
1213
using 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
}

tests/ModelContextProtocol.Tests/McpEndpointTestFixture.cs

Lines changed: 0 additions & 80 deletions
This file was deleted.

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,4 +685,50 @@ await transport.SendMessageAsync(new JsonRpcNotification
685685
await server.DisposeAsync();
686686
await serverTask;
687687
}
688+
689+
[Fact]
690+
public async Task NotifyCancel_Should_Be_Handled()
691+
{
692+
// Arrange
693+
TaskCompletionSource<JsonRpcNotification> notificationReceived = new();
694+
TaskCompletionSource notificationIntercepted = new();
695+
await using TestServerTransport transport = new();
696+
transport.OnMessageSent = (message) =>
697+
{
698+
if (message is JsonRpcNotification notification && notification.Method == NotificationMethods.CancelledNotification)
699+
notificationReceived.TrySetResult(notification);
700+
};
701+
var options = CreateOptions(new()
702+
{
703+
NotificationHandlers = [new(NotificationMethods.CancelledNotification, notification =>
704+
{
705+
InvalidOperationException exception = new("The sender of a notification shouldn't handle the notification.");
706+
notificationIntercepted.TrySetException(exception);
707+
return notificationIntercepted.Task;
708+
})],
709+
});
710+
await using var server = McpServerFactory.Create(transport, options, LoggerFactory);
711+
712+
// Act
713+
var token = TestContext.Current.CancellationToken;
714+
Task serverTask = server.RunAsync(token);
715+
await server.NotifyCancelAsync(
716+
requestId: new("abc"),
717+
reason: "Cancelled",
718+
cancellationToken: token);
719+
await server.DisposeAsync();
720+
await serverTask.WaitAsync(TimeSpan.FromSeconds(1), token);
721+
var notification = await notificationReceived.Task.WaitAsync(TimeSpan.FromSeconds(1), token);
722+
723+
// Assert
724+
var cancelled = JsonSerializer.Deserialize<CancelledNotification>(notification.Params);
725+
Assert.NotNull(cancelled);
726+
Assert.Equal("abc", cancelled.RequestId.ToString());
727+
Assert.Equal("Cancelled", cancelled.Reason);
728+
729+
Assert.Throws<TimeoutException>(() => notificationIntercepted.Task
730+
.Wait(TimeSpan.FromSeconds(5), token));
731+
Assert.False(notificationIntercepted.Task.IsCompleted,
732+
"Notifications should not be intercepted by the sender of the notification.");
733+
}
688734
}

0 commit comments

Comments
 (0)