using ModelContextProtocol.Extensions.Tasks; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.Runtime.InteropServices; using System.Text.Json; #pragma warning disable MCPEXP001 namespace ModelContextProtocol.Tests.Client; /// /// Integration tests for the client-side task API methods: GetTaskAsync, CancelTaskAsync, /// UpdateTaskAsync, CallToolAsTaskAsync, and the automatic polling in CallToolWithPollingAsync. /// public class McpClientTaskMethodsTests : ClientServerTestBase { public McpClientTaskMethodsTests(ITestOutputHelper outputHelper) : base(outputHelper) { #if !NET Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "https://github.com/modelcontextprotocol/csharp-sdk/issues/587"); #endif } protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) { mcpServerBuilder .WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 50, }) .WithTools([McpServerTool.Create( async (string input, CancellationToken ct) => { await Task.Delay(50, ct); return $"Processed: {input}"; }, new McpServerToolCreateOptions { Name = "test-tool", Description = "A test tool" })]); } private static IDictionary CreateArguments(string key, string value) { return new Dictionary { [key] = JsonDocument.Parse($"\"{value}\"").RootElement.Clone() }; } [Fact] public async Task GetTaskAsync_ReturnsTaskStatus() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var augmented = await client.CallToolAsTaskAsync( new CallToolRequestParams { Name = "test-tool", Arguments = CreateArguments("input", "test"), }, ct); Assert.True(augmented.IsTask); var taskId = augmented.TaskCreated!.TaskId; // Get the task status var task = await client.GetTaskAsync(taskId, ct); Assert.NotNull(task); } [Fact] public async Task GetTaskAsync_UnknownTaskId_Throws() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var ex = await Assert.ThrowsAsync(async () => await client.GetTaskAsync("nonexistent-id", ct)); Assert.Contains("Unknown task", ex.Message); } [Fact] public async Task GetTaskAsync_NullTaskId_Throws() { await using var client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.GetTaskAsync((string)null!, TestContext.Current.CancellationToken)); } [Fact] public async Task CallToolAsTaskAsync_WithTaskStore_ReturnsCreatedTask() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var augmented = await client.CallToolAsTaskAsync( new CallToolRequestParams { Name = "test-tool", Arguments = CreateArguments("input", "hello"), }, ct); Assert.True(augmented.IsTask); Assert.NotNull(augmented.TaskCreated); Assert.Equal(McpTaskStatus.Working, augmented.TaskCreated.Status); Assert.NotNull(augmented.TaskCreated.TaskId); Assert.True(augmented.TaskCreated.PollIntervalMs > 0); } [Fact] public async Task CallToolAsync_PollsUntilCompletion_ReturnsResult() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var result = await client.CallToolWithPollingAsync( new CallToolRequestParams { Name = "test-tool", Arguments = CreateArguments("input", "hello"), }, cancellationToken: ct); Assert.NotNull(result); Assert.NotEmpty(result.Content); var textContent = Assert.IsType(result.Content[0]); Assert.Equal("Processed: hello", textContent.Text); } [Fact] public async Task CancelTaskAsync_ForWorkingTask_Succeeds() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var augmented = await client.CallToolAsTaskAsync( new CallToolRequestParams { Name = "test-tool", Arguments = CreateArguments("input", "test"), }, ct); Assert.True(augmented.IsTask); var taskId = augmented.TaskCreated!.TaskId; // Cancel immediately (may succeed or fail depending on timing) try { await client.CancelTaskAsync(taskId, ct); // Cancellation is eventually consistent. The task may complete before the cancellation // request wins the race, but the terminal-task cancellation acknowledgement is idempotent. var taskResult = await client.GetTaskAsync(taskId, ct); Assert.True(taskResult is CancelledTaskResult or CompletedTaskResult); } catch (McpProtocolException) { // Task may have already completed before we could cancel — that's fine } } [Fact] public async Task CancelTaskAsync_NullTaskId_Throws() { await using var client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.CancelTaskAsync((string)null!, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] public async Task CancelTaskAsync_UnknownTaskId_AcknowledgesIdempotently() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; // SEP-2663 requires servers to always acknowledge tasks/cancel, even when the task is // unknown (e.g., has been garbage collected). The default handler must not throw. var result = await client.CancelTaskAsync("nonexistent-id", ct); Assert.NotNull(result); } [Fact] public async Task GetTaskAsync_AfterCompletion_ReturnsCompletedResult() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var augmented = await client.CallToolAsTaskAsync( new CallToolRequestParams { Name = "test-tool", Arguments = CreateArguments("input", "hello"), }, ct); var taskId = augmented.TaskCreated!.TaskId; // Poll until completed GetTaskResult? taskResult = null; for (int i = 0; i < 40; i++) { await Task.Delay(50, ct); taskResult = await client.GetTaskAsync(taskId, ct); if (taskResult is CompletedTaskResult) { break; } } var completed = Assert.IsType(taskResult); // Deserialize the stored result var toolResult = JsonSerializer.Deserialize(completed.Result, McpJsonUtilities.DefaultOptions); Assert.NotNull(toolResult); Assert.NotEmpty(toolResult.Content); var textContent = Assert.IsType(toolResult.Content[0]); Assert.Equal("Processed: hello", textContent.Text); } [Fact] public async Task MultipleTasks_CreatedConcurrently_HaveUniqueIds() { await using var client = await CreateMcpClientForServer(); var ct = TestContext.Current.CancellationToken; var taskIds = new HashSet(); for (int i = 0; i < 5; i++) { var augmented = await client.CallToolAsTaskAsync( new CallToolRequestParams { Name = "test-tool", Arguments = CreateArguments("input", $"task-{i}"), }, ct); Assert.True(augmented.IsTask); taskIds.Add(augmented.TaskCreated!.TaskId); } // All task IDs should be unique Assert.Equal(5, taskIds.Count); } }