Implement SEP-2663 Tasks Extension (#1579)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Pranav Senthilnathan 2026-06-11 13:10:45 -07:00 • committed by GitHub
commit dbb7a20176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 8190 additions and 11405 deletions

View file

@ -1,32 +1,38 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
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;
/// <summary>
/// Integration tests for the client-side task API methods: GetTaskAsync, CancelTaskAsync,
/// UpdateTaskAsync, CallToolRawAsync, and the automatic polling in CallToolAsync.
/// </summary>
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)
{
// 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 =>
mcpServerBuilder.Services.Configure<McpServerOptions>(options =>
{
options.TaskStore = taskStore;
options.TaskStore = new InMemoryMcpTaskStore
{
DefaultPollIntervalMs = 50,
};
});
// Add a simple tool for testing
mcpServerBuilder.WithTools([McpServerTool.Create(
async (string input, CancellationToken ct) =>
{
@ -40,9 +46,8 @@ public class McpClientTaskMethodsTests : ClientServerTestBase
})]);
}
private static IDictionary<string, JsonElement> CreateArguments(string key, object? value)
private static IDictionary<string, JsonElement> CreateArguments(string key, string value)
{
// For simple strings, just create a JsonElement from a string value
return new Dictionary<string, JsonElement>
{
[key] = JsonDocument.Parse($"\"{value}\"").RootElement.Clone()
@ -52,210 +57,196 @@ public class McpClientTaskMethodsTests : ClientServerTestBase
[Fact]
public async Task GetTaskAsync_ReturnsTaskStatus()
{
await using McpClient client = await CreateMcpClientForServer();
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
// Create a task by calling a tool with task metadata
var callResult = await client.CallToolAsync(
var augmented = await client.CallToolRawAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "test"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
}, ct);
// The response should contain task metadata
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
Assert.True(augmented.IsTask);
var taskId = augmented.TaskCreated!.TaskId;
// Now get the task status
var task = await client.GetTaskAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(taskId, task.TaskId);
// Get the task status
var task = await client.GetTaskAsync(taskId, ct);
Assert.NotNull(task);
}
[Fact]
public async Task GetTaskAsync_ThrowsForInvalidTaskId()
public async Task GetTaskAsync_UnknownTaskId_Throws()
{
await using McpClient client = await CreateMcpClientForServer();
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
await Assert.ThrowsAsync<ArgumentException>(async () =>
await client.GetTaskAsync("", cancellationToken: TestContext.Current.CancellationToken));
var ex = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.GetTaskAsync("nonexistent-id", ct));
Assert.Contains("Unknown task", ex.Message);
}
[Fact]
public async Task GetTaskResultAsync_ReturnsDeserializedResult()
public async Task GetTaskAsync_NullTaskId_Throws()
{
await using McpClient client = await CreateMcpClientForServer();
await using var client = await CreateMcpClientForServer();
// Create a task
var callResult = await client.CallToolAsync(
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
await client.GetTaskAsync((string)null!, TestContext.Current.CancellationToken));
}
[Fact]
public async Task CallToolRawAsync_WithTaskStore_ReturnsCreatedTask()
{
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
var augmented = await client.CallToolRawAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "hello"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
}, ct);
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
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);
}
// Wait for task to complete and get the result
JsonElement result = await client.GetTaskResultAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
[Fact]
public async Task CallToolAsync_PollsUntilCompletion_ReturnsResult()
{
await using var client = await CreateMcpClientForServer();
var ct = 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);
var result = await client.CallToolAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "hello"),
}, ct);
Assert.NotNull(result);
Assert.NotEmpty(result.Content);
var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Equal("Processed: hello", textContent.Text);
}
[Fact]
public async Task GetTaskResultAsync_ThrowsForInvalidTaskId()
public async Task CancelTaskAsync_ForWorkingTask_Succeeds()
{
await using McpClient client = await CreateMcpClientForServer();
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
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(
var augmented = await client.CallToolRawAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "test"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
}, ct);
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
Assert.True(augmented.IsTask);
var taskId = augmented.TaskCreated!.TaskId;
// List all tasks
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
// Cancel immediately (may succeed or fail depending on timing)
try
{
await client.CancelTaskAsync(taskId, ct);
Assert.NotNull(tasks);
Assert.Contains(tasks, t => t.TaskId == taskId);
// If cancel succeeded, verify the task is cancelled
var taskResult = await client.GetTaskAsync(taskId, ct);
Assert.IsType<CancelledTaskResult>(taskResult);
}
catch (McpProtocolException)
{
// Task may have already completed before we could cancel — that's fine
}
}
[Fact]
public async Task ListTasksAsync_HandlesEmptyResult()
public async Task CancelTaskAsync_NullTaskId_Throws()
{
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 using var client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
await client.ListTasksAsync((ListTasksRequestParams)null!, TestContext.Current.CancellationToken));
await client.CancelTaskAsync((string)null!, TestContext.Current.CancellationToken));
}
[Fact]
public async Task CancelTaskAsync_CancelsRunningTask()
public async Task CancelTaskAsync_UnknownTaskId_AcknowledgesIdempotently()
{
await using McpClient client = await CreateMcpClientForServer();
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
// Create a task
var callResult = await client.CallToolAsync(
// 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.CallToolRawAsync(
new CallToolRequestParams
{
Name = "test-tool",
Arguments = CreateArguments("input", "test"),
Task = new McpTaskMetadata()
},
cancellationToken: TestContext.Current.CancellationToken);
Arguments = CreateArguments("input", "hello"),
}, ct);
Assert.NotNull(callResult.Task);
string taskId = callResult.Task.TaskId;
var taskId = augmented.TaskCreated!.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++)
// Poll until completed
GetTaskResult? taskResult = null;
for (int i = 0; i < 40; i++)
{
var result = await client.CallToolAsync(
await Task.Delay(50, ct);
taskResult = await client.GetTaskAsync(taskId, ct);
if (taskResult is CompletedTaskResult)
{
break;
}
}
var completed = Assert.IsType<CompletedTaskResult>(taskResult);
// Deserialize the stored result
var toolResult = JsonSerializer.Deserialize<CallToolResult>(completed.Result, McpJsonUtilities.DefaultOptions);
Assert.NotNull(toolResult);
Assert.NotEmpty(toolResult.Content);
var textContent = Assert.IsType<TextContentBlock>(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<string>();
for (int i = 0; i < 5; i++)
{
var augmented = await client.CallToolRawAsync(
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);
}, ct);
Assert.True(augmented.IsTask);
taskIds.Add(augmented.TaskCreated!.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);
}
// All task IDs should be unique
Assert.Equal(5, taskIds.Count);
}
}

View file

@ -1,867 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Client;
/// <summary>
/// Integration tests for task-based sampling and elicitation on the client side.
/// Tests the client's ability to receive task-augmented requests from the server,
/// execute them as tasks, and report results.
/// </summary>
public class McpClientTaskSamplingElicitationTests : ClientServerTestBase
{
public McpClientTaskSamplingElicitationTests(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
services.Configure<McpServerOptions>(options =>
{
options.TaskStore = taskStore;
});
// Add a tool that uses sampling to generate responses
mcpServerBuilder.WithTools([McpServerTool.Create(
async (string prompt, McpServer server, CancellationToken ct) =>
{
// This tool requests sampling from the client
var result = await server.SampleAsync(new CreateMessageRequestParams
{
Messages = [new SamplingMessage { Role = Role.User, Content = [new TextContentBlock { Text = prompt }] }],
MaxTokens = 100
}, ct);
return result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text ?? "No response";
},
new McpServerToolCreateOptions
{
Name = "sample-tool",
Description = "A tool that uses sampling"
}),
McpServerTool.Create(
async (string message, McpServer server, CancellationToken ct) =>
{
// This tool requests elicitation from the client
var result = await server.ElicitAsync(new ElicitRequestParams
{
Message = message,
RequestedSchema = new()
}, ct);
return result.Action == "confirm" ? "Confirmed" : "Declined";
},
new McpServerToolCreateOptions
{
Name = "elicit-tool",
Description = "A tool that uses elicitation"
})]);
}
private static IDictionary<string, JsonElement> CreateArguments(string key, object? value)
{
return new Dictionary<string, JsonElement>
{
[key] = JsonDocument.Parse($"\"{value}\"").RootElement.Clone()
};
}
#region Client Task-Based Sampling Tests
[Fact]
public async Task Client_WithTaskStoreAndSamplingHandler_AdvertisesTaskAugmentedSamplingCapability()
{
// Arrange - Create client with task store and sampling handler
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = (request, progress, ct) =>
{
return new ValueTask<CreateMessageResult>(new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Sampled response" }],
Model = "test-model"
});
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// The server should see the client's task capabilities
// We verify by checking server can use task-augmented requests
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Sampling);
Assert.NotNull(Server.ClientCapabilities.Tasks);
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests?.Sampling?.CreateMessage);
}
[Fact]
public async Task Client_WithoutTaskStore_DoesNotAdvertiseTaskAugmentedSamplingCapability()
{
// Arrange - Create client with sampling handler but NO task store
var clientOptions = new McpClientOptions
{
// No TaskStore configured
Handlers = new McpClientHandlers
{
SamplingHandler = (request, progress, ct) =>
{
return new ValueTask<CreateMessageResult>(new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Sampled response" }],
Model = "test-model"
});
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// The server should see sampling capability but NOT task-augmented sampling
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Sampling);
// Task capabilities should be null (no task store)
Assert.Null(Server.ClientCapabilities.Tasks);
}
[Fact]
public async Task Server_SampleAsTaskAsync_FailsWhenClientDoesNotSupportTaskAugmentedSampling()
{
// Arrange - Client with sampling handler but NO task store
var clientOptions = new McpClientOptions
{
Handlers = new McpClientHandlers
{
SamplingHandler = (request, progress, ct) =>
{
return new ValueTask<CreateMessageResult>(new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Response" }],
Model = "model"
});
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act & Assert - Server should throw when trying to use task-augmented sampling
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await Server.SampleAsTaskAsync(
new CreateMessageRequestParams
{
Messages = [new SamplingMessage { Role = Role.User, Content = [new TextContentBlock { Text = "Test" }] }],
MaxTokens = 100
},
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
});
Assert.Contains("task-augmented sampling", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Client_WithTaskStore_CanExecuteSamplingAsTask()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var samplingCompleted = new TaskCompletionSource<bool>();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = async (request, progress, ct) =>
{
// Simulate some work
await Task.Delay(50, ct);
samplingCompleted.TrySetResult(true);
return new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Task-based sampling response" }],
Model = "test-model"
};
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act - Server requests task-augmented sampling
var mcpTask = await Server.SampleAsTaskAsync(
new CreateMessageRequestParams
{
Messages = [new SamplingMessage { Role = Role.User, Content = [new TextContentBlock { Text = "Hello" }] }],
MaxTokens = 100
},
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Assert - Task was created
Assert.NotNull(mcpTask);
Assert.NotEmpty(mcpTask.TaskId);
Assert.Equal(McpTaskStatus.Working, mcpTask.Status);
// Wait for sampling to complete
await samplingCompleted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// Poll until task is complete
McpTask taskStatus;
do
{
await Task.Delay(100, TestContext.Current.CancellationToken);
taskStatus = await Server.GetTaskAsync(mcpTask.TaskId, TestContext.Current.CancellationToken);
}
while (taskStatus.Status == McpTaskStatus.Working);
Assert.Equal(McpTaskStatus.Completed, taskStatus.Status);
// Get the result
var result = await Server.GetTaskResultAsync<CreateMessageResult>(
mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
var textContent = Assert.IsType<TextContentBlock>(Assert.Single(result.Content));
Assert.Equal("Task-based sampling response", textContent.Text);
}
#endregion
#region Client Task-Based Elicitation Tests
[Fact]
public async Task Client_WithTaskStoreAndElicitationHandler_AdvertisesTaskAugmentedElicitationCapability()
{
// Arrange - Create client with task store and elicitation handler
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
ElicitationHandler = (request, ct) =>
{
return new ValueTask<ElicitResult>(new ElicitResult { Action = "confirm" });
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Verify client advertised task-augmented elicitation
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Elicitation);
Assert.NotNull(Server.ClientCapabilities.Tasks);
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests?.Elicitation?.Create);
}
[Fact]
public async Task Client_WithoutTaskStore_DoesNotAdvertiseTaskAugmentedElicitationCapability()
{
// Arrange - Create client with elicitation handler but NO task store
var clientOptions = new McpClientOptions
{
// No TaskStore configured
Handlers = new McpClientHandlers
{
ElicitationHandler = (request, ct) =>
{
return new ValueTask<ElicitResult>(new ElicitResult { Action = "confirm" });
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Verify elicitation is supported but NOT task-augmented
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Elicitation);
Assert.Null(Server.ClientCapabilities.Tasks);
}
[Fact]
public async Task Server_ElicitAsTaskAsync_FailsWhenClientDoesNotSupportTaskAugmentedElicitation()
{
// Arrange - Client with elicitation handler but NO task store
var clientOptions = new McpClientOptions
{
Handlers = new McpClientHandlers
{
ElicitationHandler = (request, ct) =>
{
return new ValueTask<ElicitResult>(new ElicitResult { Action = "confirm" });
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act & Assert - Server should throw when trying to use task-augmented elicitation
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await Server.ElicitAsTaskAsync(
new ElicitRequestParams
{
Message = "Please confirm",
RequestedSchema = new()
},
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
});
Assert.Contains("task-augmented elicitation", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Client_WithTaskStore_CanExecuteElicitationAsTask()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var elicitationCompleted = new TaskCompletionSource<bool>();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
ElicitationHandler = async (request, ct) =>
{
// Simulate user interaction time
await Task.Delay(50, ct);
elicitationCompleted.TrySetResult(true);
return new ElicitResult
{
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
["answer"] = JsonDocument.Parse("\"yes\"").RootElement.Clone()
}
};
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act - Server requests task-augmented elicitation
var mcpTask = await Server.ElicitAsTaskAsync(
new ElicitRequestParams
{
Message = "Do you want to proceed?",
RequestedSchema = new()
},
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Assert - Task was created
Assert.NotNull(mcpTask);
Assert.NotEmpty(mcpTask.TaskId);
Assert.Equal(McpTaskStatus.Working, mcpTask.Status);
// Wait for elicitation to complete
await elicitationCompleted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// Poll until task is complete
McpTask taskStatus;
do
{
await Task.Delay(100, TestContext.Current.CancellationToken);
taskStatus = await Server.GetTaskAsync(mcpTask.TaskId, TestContext.Current.CancellationToken);
}
while (taskStatus.Status == McpTaskStatus.Working);
Assert.Equal(McpTaskStatus.Completed, taskStatus.Status);
// Get the result
var result = await Server.GetTaskResultAsync<ElicitResult>(
mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("accept", result.Action);
}
#endregion
#region Client Task Reporting Tests
[Fact]
public async Task Client_CanListOwnTasks()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = async (request, progress, ct) =>
{
await Task.Delay(50, ct);
return new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Response" }],
Model = "model"
};
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Create multiple tasks
var task1 = await Server.SampleAsTaskAsync(
new CreateMessageRequestParams { Messages = [], MaxTokens = 100 },
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
var task2 = await Server.SampleAsTaskAsync(
new CreateMessageRequestParams { Messages = [], MaxTokens = 100 },
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Act - Server lists tasks from client
var tasks = await Server.ListTasksAsync(TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(tasks);
Assert.True(tasks.Count >= 2, "Should have at least 2 tasks");
Assert.Contains(tasks, t => t.TaskId == task1.TaskId);
Assert.Contains(tasks, t => t.TaskId == task2.TaskId);
}
[Fact]
public async Task Client_CanCancelTasks()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var samplingStarted = new TaskCompletionSource<bool>();
var allowCompletion = new TaskCompletionSource<bool>();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = async (request, progress, ct) =>
{
samplingStarted.TrySetResult(true);
// Wait for either completion signal or cancellation
try
{
await allowCompletion.Task.WaitAsync(ct);
}
catch (OperationCanceledException)
{
throw;
}
return new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Should not reach here" }],
Model = "model"
};
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Create a task that will be in progress
var mcpTask = await Server.SampleAsTaskAsync(
new CreateMessageRequestParams { Messages = [], MaxTokens = 100 },
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Wait for sampling to start
await samplingStarted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// Act - Cancel the task
var cancelledTask = await Server.CancelTaskAsync(mcpTask.TaskId, TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(cancelledTask);
Assert.Equal(McpTaskStatus.Cancelled, cancelledTask.Status);
// Allow completion to avoid hanging (the handler might still be running)
allowCompletion.TrySetResult(true);
}
[Fact]
public async Task Client_TaskStatusNotifications_SentWhenEnabled()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var workingNotificationReceived = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var completedNotificationReceived = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var notificationsReceived = new List<McpTask>();
var notificationsLock = new object();
string? expectedTaskId = null;
var expectedTaskIdLock = new object();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
SendTaskStatusNotifications = true,
Handlers = new McpClientHandlers
{
SamplingHandler = async (request, progress, ct) =>
{
await Task.Delay(100, ct);
return new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Done" }],
Model = "model"
};
}
}
};
// Register notification handler on the server BEFORE creating the client
var notificationHandler = Server.RegisterNotificationHandler(
NotificationMethods.TaskStatusNotification,
(notification, ct) =>
{
if (notification.Params is not { } paramsNode)
{
return default;
}
var taskNotification = JsonSerializer.Deserialize<McpTaskStatusNotificationParams>(
paramsNode, McpJsonUtilities.DefaultOptions);
if (taskNotification is null)
{
return default;
}
// Only track notifications for our task
string? taskId;
lock (expectedTaskIdLock)
{
taskId = expectedTaskId;
}
if (taskId is not null && taskNotification.TaskId != taskId)
{
return default;
}
lock (notificationsLock)
{
notificationsReceived.Add(new McpTask
{
TaskId = taskNotification.TaskId,
Status = taskNotification.Status,
CreatedAt = taskNotification.CreatedAt,
LastUpdatedAt = taskNotification.LastUpdatedAt
});
}
// Signal when we receive the Working and Completed notifications
if (taskNotification.Status == McpTaskStatus.Working)
{
workingNotificationReceived.TrySetResult(true);
}
else if (taskNotification.Status == McpTaskStatus.Completed)
{
completedNotificationReceived.TrySetResult(true);
}
return default;
});
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act - Create a task
var mcpTask = await Server.SampleAsTaskAsync(
new CreateMessageRequestParams { Messages = [], MaxTokens = 100 },
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Store the expected task ID for filtering
lock (expectedTaskIdLock)
{
expectedTaskId = mcpTask.TaskId;
}
// Wait for both Working and Completed notifications to arrive
// The notifications are sent asynchronously so we need to wait for both
await Task.WhenAll(
workingNotificationReceived.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken),
completedNotificationReceived.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken));
// Assert - Should have received notifications for status transitions
await notificationHandler.DisposeAsync();
List<McpTask> notifications;
lock (notificationsLock)
{
notifications = [.. notificationsReceived];
}
Assert.NotEmpty(notifications);
Assert.Contains(notifications, t => t.Status == McpTaskStatus.Working);
Assert.Contains(notifications, t => t.Status == McpTaskStatus.Completed);
// Verify all notifications are for the correct task
Assert.All(notifications, t => Assert.Equal(mcpTask.TaskId, t.TaskId));
}
#endregion
#region Error Handling Tests
[Fact]
public async Task Client_SamplingHandlerException_ResultsInFailedTask()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var samplingAttempted = new TaskCompletionSource<bool>();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = (request, progress, ct) =>
{
samplingAttempted.TrySetResult(true);
throw new InvalidOperationException("Sampling failed!");
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act
var mcpTask = await Server.SampleAsTaskAsync(
new CreateMessageRequestParams { Messages = [], MaxTokens = 100 },
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Wait for sampling attempt
await samplingAttempted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// Poll until task status changes
McpTask taskStatus;
do
{
await Task.Delay(100, TestContext.Current.CancellationToken);
taskStatus = await Server.GetTaskAsync(mcpTask.TaskId, TestContext.Current.CancellationToken);
}
while (taskStatus.Status == McpTaskStatus.Working);
// Assert - Task should be in failed state
Assert.Equal(McpTaskStatus.Failed, taskStatus.Status);
Assert.NotNull(taskStatus.StatusMessage);
Assert.Contains("Sampling failed!", taskStatus.StatusMessage);
}
[Fact]
public async Task Client_ElicitationHandlerException_ResultsInFailedTask()
{
// Arrange
var taskStore = new InMemoryMcpTaskStore();
var elicitationAttempted = new TaskCompletionSource<bool>();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
ElicitationHandler = (request, ct) =>
{
elicitationAttempted.TrySetResult(true);
throw new InvalidOperationException("Elicitation failed!");
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Act
var mcpTask = await Server.ElicitAsTaskAsync(
new ElicitRequestParams
{
Message = "Test",
RequestedSchema = new()
},
new McpTaskMetadata(),
TestContext.Current.CancellationToken);
// Wait for elicitation attempt
await elicitationAttempted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// Poll until task status changes
McpTask taskStatus;
do
{
await Task.Delay(100, TestContext.Current.CancellationToken);
taskStatus = await Server.GetTaskAsync(mcpTask.TaskId, TestContext.Current.CancellationToken);
}
while (taskStatus.Status == McpTaskStatus.Working);
// Assert
Assert.Equal(McpTaskStatus.Failed, taskStatus.Status);
Assert.NotNull(taskStatus.StatusMessage);
Assert.Contains("Elicitation failed!", taskStatus.StatusMessage);
}
#endregion
#region Capability Validation Tests
[Fact]
public async Task Client_WithOnlySamplingHandler_OnlyAdvertisesSamplingTasks()
{
// Arrange - Client with only sampling handler and task store
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = (request, progress, ct) =>
{
return new ValueTask<CreateMessageResult>(new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Response" }],
Model = "model"
});
}
// No ElicitationHandler
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Assert
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Tasks);
// Should have sampling task capability
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests?.Sampling?.CreateMessage);
// Should NOT have elicitation task capability
Assert.Null(Server.ClientCapabilities.Tasks.Requests?.Elicitation);
}
[Fact]
public async Task Client_WithOnlyElicitationHandler_OnlyAdvertisesElicitationTasks()
{
// Arrange - Client with only elicitation handler and task store
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
ElicitationHandler = (request, ct) =>
{
return new ValueTask<ElicitResult>(new ElicitResult { Action = "confirm" });
}
// No SamplingHandler
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Assert
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Tasks);
// Should have elicitation task capability
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests?.Elicitation?.Create);
// Should NOT have sampling task capability
Assert.Null(Server.ClientCapabilities.Tasks.Requests?.Sampling);
}
[Fact]
public async Task Client_WithBothHandlers_AdvertisesBothTaskCapabilities()
{
// Arrange - Client with both handlers and task store
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers
{
SamplingHandler = (request, progress, ct) =>
{
return new ValueTask<CreateMessageResult>(new CreateMessageResult
{
Content = [new TextContentBlock { Text = "Response" }],
Model = "model"
});
},
ElicitationHandler = (request, ct) =>
{
return new ValueTask<ElicitResult>(new ElicitResult { Action = "confirm" });
}
}
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Assert
Assert.NotNull(Server.ClientCapabilities);
Assert.NotNull(Server.ClientCapabilities.Tasks);
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests);
// Should have both capabilities
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests.Sampling?.CreateMessage);
Assert.NotNull(Server.ClientCapabilities.Tasks.Requests.Elicitation?.Create);
// Should also have list and cancel capabilities
Assert.NotNull(Server.ClientCapabilities.Tasks.List);
Assert.NotNull(Server.ClientCapabilities.Tasks.Cancel);
}
[Fact]
public async Task Client_WithNoHandlers_DoesNotAdvertiseTaskCapabilities()
{
// Arrange - Client with task store but no handlers
var taskStore = new InMemoryMcpTaskStore();
var clientOptions = new McpClientOptions
{
TaskStore = taskStore,
Handlers = new McpClientHandlers()
// No handlers configured
};
await using McpClient client = await CreateMcpClientForServer(clientOptions);
// Assert - No capabilities should be advertised without handlers
Assert.NotNull(Server.ClientCapabilities);
// Note: Tasks capability is advertised based on task store being present,
// but request types depend on specific handlers
if (Server.ClientCapabilities.Tasks is not null)
{
// If Tasks is present, requests should be null or have no request types
var requests = Server.ClientCapabilities.Tasks.Requests;
if (requests is not null)
{
Assert.Null(requests.Sampling);
Assert.Null(requests.Elicitation);
}
}
}
#endregion
}

View file

@ -72,12 +72,23 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
{
List<string> messageTypes = [];
// The client sends notifications/initialized fire-and-forget, so unlike the initialize and
// tools/list request/response exchanges it has no synchronization point the test can await.
// Signal once the filter finishes processing it so the strict counts below observe a
// complete, stable log instead of racing the still-in-flight notification.
var initializedNotificationProcessed = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
McpServerBuilder
.WithMessageFilters(filters => filters.AddIncomingFilter((next) => async (context, cancellationToken) =>
{
var messageTypeName = context.JsonRpcMessage.GetType().Name;
messageTypes.Add(messageTypeName);
await next(context, cancellationToken);
if (context.JsonRpcMessage is JsonRpcNotification { Method: NotificationMethods.InitializedNotification })
{
initializedNotificationProcessed.TrySetResult(true);
}
}))
.WithTools<TestTool>();
@ -87,6 +98,10 @@ public class McpServerBuilderExtensionsMessageFilterTests(ITestOutputHelper test
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
// Wait for the fire-and-forget initialized notification to flow through the filter pipeline
// before snapshotting the counts; otherwise the strict counts below can race the notification.
await initializedNotificationProcessed.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
// The message filter should intercept JsonRpcRequest messages.
// Use strict counts so a regression that invokes the filter pipeline more than once per
// incoming message (analogous to the SendRequestAsync double-wrap regression on the outgoing

View file

@ -284,57 +284,4 @@ public class McpServerOptionsSetupTests
Assert.NotNull(options.Capabilities?.Completions);
}
#endregion
#region TaskStore Tests
[Fact]
public void TaskStore_IsPopulatedFromDI_WhenNotExplicitlySet()
{
var services = new ServiceCollection();
services.AddMcpServer();
services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.IsType<InMemoryMcpTaskStore>(options.TaskStore);
}
[Fact]
public void TaskStore_ExplicitOption_TakesPrecedenceOverDI()
{
var explicitStore = new InMemoryMcpTaskStore();
var services = new ServiceCollection();
services.AddMcpServer(options => options.TaskStore = explicitStore);
services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Same(explicitStore, options.TaskStore);
}
[Fact]
public void TaskStore_RemainsNull_WhenNothingIsRegistered()
{
var services = new ServiceCollection();
services.AddMcpServer();
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.TaskStore);
}
[Fact]
public void TaskStore_CanBeOverriddenToNull_AfterDIRegistration()
{
var services = new ServiceCollection();
services.AddMcpServer();
services.AddSingleton<IMcpTaskStore, InMemoryMcpTaskStore>();
services.Configure<McpServerOptions>(options => options.TaskStore = null);
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.TaskStore);
}
#endregion
}

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ModelContextProtocol.Protocol;
@ -10,13 +11,13 @@ namespace ModelContextProtocol.Tests;
/// </summary>
/// <remarks>
/// <para>
/// Experimental properties (e.g. <see cref="Tool.Execution"/>, <see cref="ServerCapabilities.Tasks"/>)
/// Experimental properties (e.g. <see cref="ServerCapabilities.Extensions"/>, <see cref="ClientCapabilities.Extensions"/>)
/// use an internal <c>*Core</c> property for serialization. A consumer's source-generated
/// <see cref="JsonSerializerContext"/> cannot see internal members, so experimental data is
/// silently dropped unless the consumer chains the SDK's resolver into their options.
/// </para>
/// <para>
/// These tests depend on <see cref="Tool.Execution"/> and <see cref="ServerCapabilities.Tasks"/>
/// These tests depend on <see cref="ServerCapabilities.Extensions"/> and <see cref="ClientCapabilities.Extensions"/>
/// being experimental. When those APIs stabilize, update these tests to reference whatever
/// experimental properties exist at that time, or remove them entirely if no experimental
/// APIs remain.
@ -32,36 +33,36 @@ public class ExperimentalPropertySerializationTests
TypeInfoResolverChain = { ConsumerJsonContext.Default }
};
var tool = new Tool
var capabilities = new ServerCapabilities
{
Name = "test-tool",
Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
Tools = new ToolsCapability(),
Extensions = new Dictionary<string, object> { ["io.test"] = new JsonObject { ["enabled"] = true } }
};
string json = JsonSerializer.Serialize(tool, options);
Assert.DoesNotContain("\"execution\"", json);
Assert.Contains("\"name\"", json);
string json = JsonSerializer.Serialize(capabilities, options);
Assert.DoesNotContain("\"extensions\"", json);
Assert.Contains("\"tools\"", json);
}
[Fact]
public void ExperimentalProperties_IgnoredOnDeserialize_WithConsumerContextOnly()
{
string json = JsonSerializer.Serialize(
new Tool
new ServerCapabilities
{
Name = "test-tool",
Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
Tools = new ToolsCapability(),
Extensions = new Dictionary<string, object> { ["io.test"] = new JsonObject { ["enabled"] = true } }
},
McpJsonUtilities.DefaultOptions);
Assert.Contains("\"execution\"", json);
Assert.Contains("\"extensions\"", json);
var options = new JsonSerializerOptions
{
TypeInfoResolverChain = { ConsumerJsonContext.Default }
};
var deserialized = JsonSerializer.Deserialize<Tool>(json, options)!;
Assert.Equal("test-tool", deserialized.Name);
Assert.Null(deserialized.Execution);
var deserialized = JsonSerializer.Deserialize<ServerCapabilities>(json, options)!;
Assert.NotNull(deserialized.Tools);
Assert.Null(deserialized.Extensions);
}
[Fact]
@ -76,35 +77,36 @@ public class ExperimentalPropertySerializationTests
}
};
var tool = new Tool
var capabilities = new ServerCapabilities
{
Name = "test-tool",
Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
Tools = new ToolsCapability(),
Extensions = new Dictionary<string, object> { ["io.test"] = new JsonObject { ["enabled"] = true } }
};
string json = JsonSerializer.Serialize(tool, options);
Assert.Contains("\"execution\"", json);
Assert.Contains("\"name\"", json);
string json = JsonSerializer.Serialize(capabilities, options);
Assert.Contains("\"extensions\"", json);
Assert.Contains("\"tools\"", json);
var deserialized = JsonSerializer.Deserialize<Tool>(json, options)!;
Assert.Equal("test-tool", deserialized.Name);
Assert.NotNull(deserialized.Execution);
Assert.Equal(ToolTaskSupport.Optional, deserialized.Execution.TaskSupport);
var deserialized = JsonSerializer.Deserialize<ServerCapabilities>(json, options)!;
Assert.NotNull(deserialized.Tools);
Assert.NotNull(deserialized.Extensions);
Assert.True(deserialized.Extensions.ContainsKey("io.test"));
}
[Fact]
public void ExperimentalProperties_RoundTrip_WithDefaultOptions()
{
var capabilities = new ServerCapabilities
var capabilities = new ClientCapabilities
{
Tasks = new McpTasksCapability()
Extensions = new Dictionary<string, object> { ["io.test"] = new JsonObject { ["enabled"] = true } }
};
string json = JsonSerializer.Serialize(capabilities, McpJsonUtilities.DefaultOptions);
Assert.Contains("\"tasks\"", json);
Assert.Contains("\"extensions\"", json);
var deserialized = JsonSerializer.Deserialize<ServerCapabilities>(json, McpJsonUtilities.DefaultOptions)!;
Assert.NotNull(deserialized.Tasks);
var deserialized = JsonSerializer.Deserialize<ClientCapabilities>(json, McpJsonUtilities.DefaultOptions)!;
Assert.NotNull(deserialized.Extensions);
Assert.True(deserialized.Extensions.ContainsKey("io.test"));
}
}

View file

@ -35,10 +35,6 @@
<ItemGroup>
<Compile Include="..\Common\**\*.cs" />
<!-- Link InMemoryMcpTaskStore.cs for testing with TimeProvider support -->
<Compile Include="..\..\src\ModelContextProtocol.Core\Server\InMemoryMcpTaskStore.cs" Link="Server\InMemoryMcpTaskStore.cs" />
<!-- Include dependencies for the linked InMemoryMcpTaskStore.cs -->
<Compile Include="..\..\src\Common\Experimentals.cs" Link="Experimentals.cs" />
<Compile Include="..\..\src\Common\McpHttpHeaders.cs" Link="McpHttpHeaders.cs" />
</ItemGroup>

View file

@ -17,7 +17,6 @@ public static class CallToolRequestParamsTests
["city"] = JsonDocument.Parse("\"Seattle\"").RootElement.Clone(),
["units"] = JsonDocument.Parse("\"metric\"").RootElement.Clone()
},
Task = new McpTaskMetadata { TimeToLive = TimeSpan.FromHours(1) },
Meta = new JsonObject { ["progressToken"] = "token-123" }
};
@ -30,8 +29,6 @@ public static class CallToolRequestParamsTests
Assert.Equal(2, deserialized.Arguments.Count);
Assert.Equal("Seattle", deserialized.Arguments["city"].GetString());
Assert.Equal("metric", deserialized.Arguments["units"].GetString());
Assert.NotNull(deserialized.Task);
Assert.Equal(original.Task.TimeToLive, deserialized.Task.TimeToLive);
Assert.NotNull(deserialized.Meta);
Assert.Equal("token-123", (string)deserialized.Meta["progressToken"]!);
}
@ -50,7 +47,6 @@ public static class CallToolRequestParamsTests
Assert.NotNull(deserialized);
Assert.Equal(original.Name, deserialized.Name);
Assert.Null(deserialized.Arguments);
Assert.Null(deserialized.Task);
Assert.Null(deserialized.Meta);
}
}

View file

@ -14,13 +14,6 @@ public static class CallToolResultTests
Content = [new TextContentBlock { Text = "Result text" }],
StructuredContent = JsonElement.Parse("""{"temperature":72}"""),
IsError = false,
Task = new McpTask
{
TaskId = "task-1",
Status = McpTaskStatus.Completed,
CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
LastUpdatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
},
Meta = new JsonObject { ["key"] = "value" }
};
@ -34,8 +27,6 @@ public static class CallToolResultTests
Assert.NotNull(deserialized.StructuredContent);
Assert.Equal(72, deserialized.StructuredContent.Value.GetProperty("temperature").GetInt32());
Assert.False(deserialized.IsError);
Assert.NotNull(deserialized.Task);
Assert.Equal("task-1", deserialized.Task.TaskId);
Assert.NotNull(deserialized.Meta);
Assert.Equal("value", (string)deserialized.Meta["key"]!);
}
@ -52,7 +43,6 @@ public static class CallToolResultTests
Assert.Empty(deserialized.Content);
Assert.Null(deserialized.StructuredContent);
Assert.Null(deserialized.IsError);
Assert.Null(deserialized.Task);
Assert.Null(deserialized.Meta);
}
}

View file

@ -1,25 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class CancelMcpTaskRequestParamsTests
{
[Fact]
public static void CancelMcpTaskRequestParams_SerializationRoundTrip()
{
// Arrange
var original = new CancelMcpTaskRequestParams
{
TaskId = "cancel-task-456"
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CancelMcpTaskRequestParams>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TaskId, deserialized.TaskId);
}
}

View file

@ -1,33 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class CancelMcpTaskResultTests
{
[Fact]
public static void CancelMcpTaskResult_SerializationRoundTrip()
{
// Arrange
var original = new CancelMcpTaskResult
{
TaskId = "cancelled-789",
Status = McpTaskStatus.Cancelled,
StatusMessage = "Cancelled by user",
CreatedAt = DateTimeOffset.UtcNow,
LastUpdatedAt = DateTimeOffset.UtcNow,
TimeToLive = null,
PollInterval = null
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CancelMcpTaskResult>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TaskId, deserialized.TaskId);
Assert.Equal(original.Status, deserialized.Status);
Assert.Equal(original.StatusMessage, deserialized.StatusMessage);
}
}

View file

@ -21,7 +21,6 @@ public static class ClientCapabilitiesTests
Form = new FormElicitationCapability(),
Url = new UrlElicitationCapability()
},
Tasks = new McpTasksCapability(),
Extensions = new Dictionary<string, object>
{
["io.modelcontextprotocol/test"] = new object()
@ -40,7 +39,6 @@ public static class ClientCapabilitiesTests
Assert.NotNull(deserialized.Elicitation);
Assert.NotNull(deserialized.Elicitation.Form);
Assert.NotNull(deserialized.Elicitation.Url);
Assert.NotNull(deserialized.Tasks);
Assert.NotNull(deserialized.Extensions);
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/test"));
}
@ -58,7 +56,6 @@ public static class ClientCapabilitiesTests
Assert.Null(deserialized.Roots);
Assert.Null(deserialized.Sampling);
Assert.Null(deserialized.Elicitation);
Assert.Null(deserialized.Tasks);
Assert.Null(deserialized.Extensions);
}

View file

@ -1,41 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Protocol;
public static class CreateTaskResultTests
{
[Fact]
public static void CreateTaskResult_SerializationRoundTrip_PreservesAllProperties()
{
var original = new CreateTaskResult
{
Task = new McpTask
{
TaskId = "task-123",
Status = McpTaskStatus.Working,
StatusMessage = "Processing",
CreatedAt = new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero),
LastUpdatedAt = new DateTimeOffset(2025, 6, 1, 12, 5, 0, TimeSpan.Zero),
TimeToLive = TimeSpan.FromHours(1),
PollInterval = TimeSpan.FromSeconds(5)
},
Meta = new JsonObject { ["key"] = "value" }
};
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<CreateTaskResult>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
Assert.Equal("task-123", deserialized.Task.TaskId);
Assert.Equal(McpTaskStatus.Working, deserialized.Task.Status);
Assert.Equal("Processing", deserialized.Task.StatusMessage);
Assert.Equal(original.Task.CreatedAt, deserialized.Task.CreatedAt);
Assert.Equal(original.Task.LastUpdatedAt, deserialized.Task.LastUpdatedAt);
Assert.Equal(original.Task.TimeToLive, deserialized.Task.TimeToLive);
Assert.Equal(original.Task.PollInterval, deserialized.Task.PollInterval);
Assert.NotNull(deserialized.Meta);
Assert.Equal("value", (string)deserialized.Meta["key"]!);
}
}

View file

@ -23,7 +23,6 @@ public static class ElicitRequestParamsTests
["age"] = new ElicitRequestParams.NumberSchema { Description = "Your age" }
}
},
Task = new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(10) },
Meta = new JsonObject { ["progressToken"] = "tok-1" }
};
@ -37,8 +36,6 @@ public static class ElicitRequestParamsTests
Assert.Equal("Please provide your details", deserialized.Message);
Assert.NotNull(deserialized.RequestedSchema);
Assert.Equal(2, deserialized.RequestedSchema.Properties.Count);
Assert.NotNull(deserialized.Task);
Assert.Equal(TimeSpan.FromMinutes(10), deserialized.Task.TimeToLive);
Assert.NotNull(deserialized.Meta);
Assert.Equal("tok-1", (string)deserialized.Meta["progressToken"]!);
}
@ -63,7 +60,6 @@ public static class ElicitRequestParamsTests
Assert.Equal("https://example.com/auth", deserialized.Url);
Assert.Equal("Please authenticate", deserialized.Message);
Assert.Null(deserialized.RequestedSchema);
Assert.Null(deserialized.Task);
}
[Fact]
@ -83,7 +79,6 @@ public static class ElicitRequestParamsTests
Assert.Null(deserialized.ElicitationId);
Assert.Null(deserialized.Url);
Assert.Null(deserialized.RequestedSchema);
Assert.Null(deserialized.Task);
Assert.Null(deserialized.Meta);
}
}

View file

@ -1,25 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class GetTaskPayloadRequestParamsTests
{
[Fact]
public static void GetTaskPayloadRequestParams_SerializationRoundTrip()
{
// Arrange
var original = new GetTaskPayloadRequestParams
{
TaskId = "payload-task-999"
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<GetTaskPayloadRequestParams>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TaskId, deserialized.TaskId);
}
}

View file

@ -1,25 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class GetTaskRequestParamsTests
{
[Fact]
public static void GetTaskRequestParams_SerializationRoundTrip()
{
// Arrange
var original = new GetTaskRequestParams
{
TaskId = "get-task-123"
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<GetTaskRequestParams>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TaskId, deserialized.TaskId);
}
}

View file

@ -1,37 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class GetTaskResultTests
{
[Fact]
public static void GetTaskResult_SerializationRoundTrip()
{
// Arrange
var original = new GetTaskResult
{
TaskId = "result-123",
Status = McpTaskStatus.Completed,
StatusMessage = "Done",
CreatedAt = DateTimeOffset.UtcNow,
LastUpdatedAt = DateTimeOffset.UtcNow,
TimeToLive = TimeSpan.FromHours(1),
PollInterval = TimeSpan.FromSeconds(1)
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<GetTaskResult>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TaskId, deserialized.TaskId);
Assert.Equal(original.Status, deserialized.Status);
Assert.Equal(original.StatusMessage, deserialized.StatusMessage);
Assert.Equal(original.CreatedAt, deserialized.CreatedAt);
Assert.Equal(original.LastUpdatedAt, deserialized.LastUpdatedAt);
Assert.Equal(original.TimeToLive, deserialized.TimeToLive);
Assert.Equal(original.PollInterval, deserialized.PollInterval);
}
}

View file

@ -1,25 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class ListTasksRequestParamsTests
{
[Fact]
public static void ListTasksRequestParams_SerializationRoundTrip()
{
// Arrange
var original = new ListTasksRequestParams
{
Cursor = "cursor-abc123"
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ListTasksRequestParams>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.Cursor, deserialized.Cursor);
}
}

View file

@ -1,46 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class ListTasksResultTests
{
[Fact]
public static void ListTasksResult_SerializationRoundTrip()
{
// Arrange
var original = new ListTasksResult
{
Tasks =
[
new McpTask
{
TaskId = "task-1",
Status = McpTaskStatus.Working,
CreatedAt = DateTimeOffset.UtcNow,
LastUpdatedAt = DateTimeOffset.UtcNow
},
new McpTask
{
TaskId = "task-2",
Status = McpTaskStatus.Completed,
CreatedAt = DateTimeOffset.UtcNow,
LastUpdatedAt = DateTimeOffset.UtcNow
}
],
NextCursor = "next-page-token"
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<ListTasksResult>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.NotNull(deserialized.Tasks);
Assert.Equal(2, deserialized.Tasks.Count);
Assert.Equal(original.Tasks[0].TaskId, deserialized.Tasks[0].TaskId);
Assert.Equal(original.Tasks[1].TaskId, deserialized.Tasks[1].TaskId);
Assert.Equal(original.NextCursor, deserialized.NextCursor);
}
}

View file

@ -1,53 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class McpTaskMetadataTests
{
[Fact]
public static void McpTaskMetadata_SerializationRoundTrip_WithTimeToLive()
{
// Arrange
var original = new McpTaskMetadata
{
TimeToLive = TimeSpan.FromHours(2)
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<McpTaskMetadata>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TimeToLive, deserialized.TimeToLive);
}
[Fact]
public static void McpTaskMetadata_SerializationRoundTrip_WithNullTimeToLive()
{
// Arrange
var original = new McpTaskMetadata();
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<McpTaskMetadata>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Null(deserialized.TimeToLive);
}
[Fact]
public static void McpTaskMetadata_HasCorrectJsonPropertyNames()
{
var metadata = new McpTaskMetadata
{
TimeToLive = TimeSpan.FromMinutes(15)
};
string json = JsonSerializer.Serialize(metadata, McpJsonUtilities.DefaultOptions);
Assert.Contains("\"ttl\":", json);
}
}

View file

@ -1,37 +0,0 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class McpTaskStatusNotificationParamsTests
{
[Fact]
public static void McpTaskStatusNotificationParams_SerializationRoundTrip()
{
// Arrange
var original = new McpTaskStatusNotificationParams
{
TaskId = "notification-task",
Status = McpTaskStatus.Completed,
StatusMessage = "Task completed successfully",
CreatedAt = new DateTimeOffset(2025, 12, 9, 10, 0, 0, TimeSpan.Zero),
LastUpdatedAt = new DateTimeOffset(2025, 12, 9, 10, 30, 0, TimeSpan.Zero),
TimeToLive = TimeSpan.FromHours(1),
PollInterval = TimeSpan.FromSeconds(2)
};
// Act
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<McpTaskStatusNotificationParams>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equal(original.TaskId, deserialized.TaskId);
Assert.Equal(original.Status, deserialized.Status);
Assert.Equal(original.StatusMessage, deserialized.StatusMessage);
Assert.Equal(original.CreatedAt, deserialized.CreatedAt);
Assert.Equal(original.LastUpdatedAt, deserialized.LastUpdatedAt);
Assert.Equal(original.TimeToLive, deserialized.TimeToLive);
Assert.Equal(original.PollInterval, deserialized.PollInterval);
}
}

Some files were not shown because too many files have changed in this diff Show more