csharp-sdk/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs
Eirik Tsarpalis 10e4f5b20f
Implement MCP task support (SEP-1686) (#1170)
Co-authored-by: Stephen Halter <halter73@gmail.com>
2026-01-27 19:21:02 -08:00

261 lines
8.9 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Client;
public class McpClientTaskMethodsTests : ClientServerTestBase
{
public McpClientTaskMethodsTests(ITestOutputHelper outputHelper)
: base(outputHelper)
{
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
// Add task store for server-side task support
var taskStore = new InMemoryMcpTaskStore();
services.AddSingleton<IMcpTaskStore>(taskStore);
// Configure server to use the task store directly
services.Configure<McpServerOptions>(options =>
{
options.TaskStore = taskStore;
});
// Add a simple tool for testing
mcpServerBuilder.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<string, JsonElement> CreateArguments(string key, object? value)
{
// For simple strings, just create a JsonElement from a string value
return new Dictionary<string, JsonElement>
{
[key] = JsonDocument.Parse($"\"{value}\"").RootElement.Clone()
};
}
[Fact]
public async Task GetTaskAsync_ReturnsTaskStatus()
{
await using McpClient client = await CreateMcpClientForServer();
// Create a task by calling a tool with task metadata
var callResult = await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "test"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
// The response should contain task metadata
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
// Now get the task status
var task = await client.GetTaskAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(taskId, task.TaskId);
}
[Fact]
public async Task GetTaskAsync_ThrowsForInvalidTaskId()
{
await using McpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetTaskAsync("", cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
public async Task GetTaskResultAsync_ReturnsDeserializedResult()
{
await using McpClient client = await CreateMcpClientForServer();
// Create a task
var callResult = await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "hello"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
// Wait for task to complete and get the result
JsonElement result = await client.GetTaskResultAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
// Verify the result has the expected CallToolResult shape
CallToolResult? toolResult = result.Deserialize<CallToolResult>(McpJsonUtilities.DefaultOptions);
Assert.NotNull(toolResult);
Assert.NotEmpty(toolResult.Content);
TextContentBlock? textContent = toolResult.Content[0] as TextContentBlock;
Assert.NotNull(textContent);
Assert.Equal("Processed: hello", textContent.Text);
}
[Fact]
public async Task GetTaskResultAsync_ThrowsForInvalidTaskId()
{
await using McpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetTaskResultAsync("", cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
public async Task ListTasksAsync_ReturnsTasks()
{
await using McpClient client = await CreateMcpClientForServer();
// Create a task
var callResult = await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "test"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
// List all tasks
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tasks);
Assert.Contains(tasks, t => t.TaskId == taskId);
}
[Fact]
public async Task ListTasksAsync_HandlesEmptyResult()
{
await using McpClient client = await CreateMcpClientForServer();
// List tasks (may or may not be empty depending on state)
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tasks);
}
[Fact]
public async Task ListTasksAsync_LowLevel_ReturnsRawResult()
{
await using McpClient client = await CreateMcpClientForServer();
// Create a task first
await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "task1"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
// Use low-level API
var result = await client.ListTasksAsync(new ListTasksRequestParams(), TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.NotNull(result.Tasks);
}
[Fact]
public async Task ListTasksAsync_LowLevel_ThrowsForNullParams()
{
await using McpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
await client.ListTasksAsync((ListTasksRequestParams)null!, TestContext.Current.CancellationToken));
}
[Fact]
public async Task CancelTaskAsync_CancelsRunningTask()
{
await using McpClient client = await CreateMcpClientForServer();
// Create a task
var callResult = await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "test"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
// Cancel the task
var canceledTask = await client.CancelTaskAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(taskId, canceledTask.TaskId);
}
[Fact]
public async Task CancelTaskAsync_ThrowsForInvalidTaskId()
{
await using McpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.CancelTaskAsync("", cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
public async Task ListTasksAsync_HandlesPagination()
{
await using McpClient client = await CreateMcpClientForServer();
// Create multiple tasks
var taskIds = new List<string>();
for (int i = 0; i < 3; i++)
{
var result = await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", $"task-{i}"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result.Task);
taskIds.Add(result.Task.TaskId);
}
// List all tasks (should handle pagination automatically if needed)
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tasks);
Assert.True(tasks.Count >= taskIds.Count, "Should retrieve at least the tasks we created");
// Verify all our tasks are in the result
foreach (var taskId in taskIds)
{
Assert.Contains(tasks, t => t.TaskId == taskId);
}
}
}