Implement SEP-2663 Tasks Extension (#1579)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
ed07fdb6f4
commit
dbb7a20176
40 changed files with 8190 additions and 11405 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]!);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue