using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; using System.Runtime.InteropServices; using System.Text.Json; namespace ModelContextProtocol.Tests.Server; /// /// Tests for McpServer methods that query tasks on the client (Phase 4 implementation). /// public class McpServerTaskMethodsTests : LoggedTest { private readonly McpServerOptions _options; public McpServerTaskMethodsTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { #if !NET Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "https://github.com/modelcontextprotocol/csharp-sdk/issues/587"); #endif _options = CreateOptions(); } private static McpServerOptions CreateOptions(ServerCapabilities? capabilities = null) { return new McpServerOptions { ProtocolVersion = "2024", InitializationTimeout = TimeSpan.FromSeconds(30), Capabilities = capabilities, }; } #region SampleAsTaskAsync Tests [Fact] public async Task SampleAsTaskAsync_ThrowsException_WhenClientDoesNotSupportSampling() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.SampleAsTaskAsync( new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, new McpTaskMetadata(), CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task SampleAsTaskAsync_ThrowsException_WhenClientDoesNotSupportTaskAugmentedSampling() { // Arrange - Client supports sampling but NOT task-augmented sampling await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Sampling = new SamplingCapability(), // Note: No Tasks capability }, TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.SampleAsTaskAsync( new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, new McpTaskMetadata(), CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task SampleAsTaskAsync_SendsRequest_WhenClientSupportsTaskAugmentedSampling() { // Arrange await using var transport = new TestServerTransport(); // Configure transport to return a task result for sampling transport.MockTask = new McpTask { TaskId = "sample-task-123", Status = McpTaskStatus.Working, CreatedAt = DateTimeOffset.UtcNow, LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Sampling = new SamplingCapability(), Tasks = new McpTasksCapability { Requests = new RequestMcpTasksCapability { Sampling = new SamplingMcpTasksCapability { CreateMessage = new CreateMessageMcpTasksCapability() } } } }, TestContext.Current.CancellationToken); // Act var task = await server.SampleAsTaskAsync( new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(5) }, TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("sample-task-123", task.TaskId); Assert.Equal(McpTaskStatus.Working, task.Status); // Verify the request was sent with task metadata var samplingRequest = transport.SentMessages.OfType() .FirstOrDefault(r => r.Method == RequestMethods.SamplingCreateMessage); Assert.NotNull(samplingRequest); var requestParams = JsonSerializer.Deserialize( samplingRequest.Params, McpJsonUtilities.DefaultOptions); Assert.NotNull(requestParams?.Task); await transport.DisposeAsync(); await runTask; } #endregion #region ElicitAsTaskAsync Tests [Fact] public async Task ElicitAsTaskAsync_ThrowsException_WhenClientDoesNotSupportElicitation() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.ElicitAsTaskAsync( new ElicitRequestParams { Message = "test", RequestedSchema = new() }, new McpTaskMetadata(), CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task ElicitAsTaskAsync_ThrowsException_WhenClientDoesNotSupportTaskAugmentedElicitation() { // Arrange - Client supports elicitation but NOT task-augmented elicitation await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Elicitation = new ElicitationCapability { Form = new() }, // Note: No Tasks capability }, TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.ElicitAsTaskAsync( new ElicitRequestParams { Message = "test", RequestedSchema = new() }, new McpTaskMetadata(), CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task ElicitAsTaskAsync_SendsRequest_WhenClientSupportsTaskAugmentedElicitation() { // Arrange await using var transport = new TestServerTransport(); // Configure transport to return a task result for elicitation transport.MockTask = new McpTask { TaskId = "elicit-task-456", Status = McpTaskStatus.Working, CreatedAt = DateTimeOffset.UtcNow, LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Elicitation = new ElicitationCapability { Form = new() }, Tasks = new McpTasksCapability { Requests = new RequestMcpTasksCapability { Elicitation = new ElicitationMcpTasksCapability { Create = new CreateElicitationMcpTasksCapability() } } } }, TestContext.Current.CancellationToken); // Act var task = await server.ElicitAsTaskAsync( new ElicitRequestParams { Message = "Please provide input", RequestedSchema = new() }, new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(10) }, TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("elicit-task-456", task.TaskId); Assert.Equal(McpTaskStatus.Working, task.Status); await transport.DisposeAsync(); await runTask; } #endregion #region GetTaskAsync Tests [Fact] public async Task GetTaskAsync_ThrowsException_WhenClientDoesNotSupportTasks() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.GetTaskAsync("task-id", CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task GetTaskAsync_SendsRequest_AndReturnsTask() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "client-task-789", Status = McpTaskStatus.Completed, StatusMessage = "Task completed successfully", CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act var task = await server.GetTaskAsync("client-task-789", TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("client-task-789", task.TaskId); Assert.Equal(McpTaskStatus.Completed, task.Status); // Verify the request was sent var taskRequest = transport.SentMessages.OfType() .FirstOrDefault(r => r.Method == RequestMethods.TasksGet); Assert.NotNull(taskRequest); await transport.DisposeAsync(); await runTask; } [Fact] public async Task GetTaskAsync_ThrowsArgumentException_WhenTaskIdIsEmpty() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.GetTaskAsync("", CancellationToken.None)); await transport.DisposeAsync(); await runTask; } #endregion #region GetTaskResultAsync Tests [Fact] public async Task GetTaskResultAsync_ThrowsException_WhenClientDoesNotSupportTasks() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.GetTaskResultAsync("task-id", cancellationToken: CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task GetTaskResultAsync_ReturnsDeserializedResult() { // Arrange await using var transport = new TestServerTransport(); transport.MockTaskResult = new CreateMessageResult { Content = [new TextContentBlock { Text = "Hello from task result!" }], Model = "gpt-4" }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act var result = await server.GetTaskResultAsync( "task-id", cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); Assert.Equal("gpt-4", result.Model); Assert.Single(result.Content); var textContent = Assert.IsType(result.Content[0]); Assert.Equal("Hello from task result!", textContent.Text); await transport.DisposeAsync(); await runTask; } #endregion #region ListTasksAsync Tests [Fact] public async Task ListTasksAsync_ThrowsException_WhenClientDoesNotSupportTasks() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.ListTasksAsync(CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task ListTasksAsync_ThrowsException_WhenClientDoesNotSupportTaskListing() { // Arrange - Client supports tasks but NOT task listing await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability { // Note: No List capability } }, TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.ListTasksAsync(CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task ListTasksAsync_ReturnsTaskList() { // Arrange await using var transport = new TestServerTransport(); transport.MockTaskList = [ new McpTask { TaskId = "task-a", Status = McpTaskStatus.Completed, CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-10), LastUpdatedAt = DateTimeOffset.UtcNow, }, new McpTask { TaskId = "task-b", Status = McpTaskStatus.Working, CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }, new McpTask { TaskId = "task-c", Status = McpTaskStatus.Failed, StatusMessage = "Task failed", CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-2), LastUpdatedAt = DateTimeOffset.UtcNow, } ]; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability { List = new ListMcpTasksCapability() } }, TestContext.Current.CancellationToken); // Act var tasks = await server.ListTasksAsync(TestContext.Current.CancellationToken); // Assert Assert.NotNull(tasks); Assert.Equal(3, tasks.Count); Assert.Equal("task-a", tasks[0].TaskId); Assert.Equal("task-b", tasks[1].TaskId); Assert.Equal("task-c", tasks[2].TaskId); await transport.DisposeAsync(); await runTask; } #endregion #region CancelTaskAsync Tests [Fact] public async Task CancelTaskAsync_ThrowsException_WhenClientDoesNotSupportTasks() { // Arrange await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities(), TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.CancelTaskAsync("task-id", CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task CancelTaskAsync_ThrowsException_WhenClientDoesNotSupportTaskCancellation() { // Arrange - Client supports tasks but NOT task cancellation await using var transport = new TestServerTransport(); await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability { // Note: No Cancel capability } }, TestContext.Current.CancellationToken); // Act & Assert await Assert.ThrowsAsync(async () => await server.CancelTaskAsync("task-id", CancellationToken.None)); await transport.DisposeAsync(); await runTask; } [Fact] public async Task CancelTaskAsync_SendsRequest_AndReturnsCancelledTask() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "task-to-cancel", Status = McpTaskStatus.Working, CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-3), LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability { Cancel = new CancelMcpTasksCapability() } }, TestContext.Current.CancellationToken); // Act var task = await server.CancelTaskAsync("task-to-cancel", TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("task-to-cancel", task.TaskId); Assert.Equal(McpTaskStatus.Cancelled, task.Status); // Verify the request was sent var cancelRequest = transport.SentMessages.OfType() .FirstOrDefault(r => r.Method == RequestMethods.TasksCancel); Assert.NotNull(cancelRequest); await transport.DisposeAsync(); await runTask; } #endregion #region PollTaskUntilCompleteAsync Tests [Fact] public async Task PollTaskUntilCompleteAsync_ReturnsImmediately_WhenTaskIsAlreadyComplete() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "completed-task", Status = McpTaskStatus.Completed, CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act var task = await server.PollTaskUntilCompleteAsync("completed-task", TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("completed-task", task.TaskId); Assert.Equal(McpTaskStatus.Completed, task.Status); await transport.DisposeAsync(); await runTask; } [Fact] public async Task PollTaskUntilCompleteAsync_ReturnsTask_WhenTaskFails() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "failed-task", Status = McpTaskStatus.Failed, StatusMessage = "Task execution failed", CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act var task = await server.PollTaskUntilCompleteAsync("failed-task", TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("failed-task", task.TaskId); Assert.Equal(McpTaskStatus.Failed, task.Status); Assert.Equal("Task execution failed", task.StatusMessage); await transport.DisposeAsync(); await runTask; } #endregion #region WaitForTaskResultAsync Tests [Fact] public async Task WaitForTaskResultAsync_ReturnsTaskAndResult_WhenTaskCompletes() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "task-with-result", Status = McpTaskStatus.Completed, CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }; transport.MockTaskResult = new CreateMessageResult { Content = [new TextContentBlock { Text = "Final result from task" }], Model = "test-model" }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act var (task, result) = await server.WaitForTaskResultAsync( "task-with-result", cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(task); Assert.Equal("task-with-result", task.TaskId); Assert.Equal(McpTaskStatus.Completed, task.Status); Assert.NotNull(result); Assert.Equal("test-model", result.Model); var textContent = Assert.IsType(Assert.Single(result.Content)); Assert.Equal("Final result from task", textContent.Text); await transport.DisposeAsync(); await runTask; } [Fact] public async Task WaitForTaskResultAsync_ThrowsException_WhenTaskFails() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "failed-task", Status = McpTaskStatus.Failed, StatusMessage = "Something went wrong", CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act & Assert var ex = await Assert.ThrowsAsync(async () => await server.WaitForTaskResultAsync( "failed-task", cancellationToken: TestContext.Current.CancellationToken)); Assert.Contains("failed", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("Something went wrong", ex.Message); await transport.DisposeAsync(); await runTask; } [Fact] public async Task WaitForTaskResultAsync_ThrowsException_WhenTaskIsCancelled() { // Arrange await using var transport = new TestServerTransport(); transport.MockTask = new McpTask { TaskId = "cancelled-task", Status = McpTaskStatus.Cancelled, CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5), LastUpdatedAt = DateTimeOffset.UtcNow, }; await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); await InitializeServerAsync(transport, new ClientCapabilities { Tasks = new McpTasksCapability() }, TestContext.Current.CancellationToken); // Act & Assert var ex = await Assert.ThrowsAsync(async () => await server.WaitForTaskResultAsync( "cancelled-task", cancellationToken: TestContext.Current.CancellationToken)); Assert.Contains("cancelled", ex.Message, StringComparison.OrdinalIgnoreCase); await transport.DisposeAsync(); await runTask; } #endregion #region Helper Methods private static async Task InitializeServerAsync(TestServerTransport transport, ClientCapabilities capabilities, CancellationToken cancellationToken = default) { var initializeRequest = new JsonRpcRequest { Id = new RequestId("init-1"), Method = RequestMethods.Initialize, Params = JsonSerializer.SerializeToNode(new InitializeRequestParams { ProtocolVersion = "2024-11-05", Capabilities = capabilities, ClientInfo = new Implementation { Name = "test-client", Version = "1.0.0" } }, McpJsonUtilities.DefaultOptions) }; var tcs = new TaskCompletionSource(); transport.OnMessageSent = (message) => { if (message is JsonRpcResponse response && response.Id == initializeRequest.Id) { tcs.TrySetResult(true); } }; await transport.SendClientMessageAsync(initializeRequest, cancellationToken); // Wait for the initialize response to be sent await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); } #endregion }