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
45 changed files with 8190 additions and 11405 deletions
|
|
@ -46,14 +46,6 @@ public class TestServerTransport : ITransport
|
|||
await SamplingAsync(request, cancellationToken);
|
||||
else if (request.Method == RequestMethods.ElicitationCreate)
|
||||
await ElicitAsync(request, cancellationToken);
|
||||
else if (request.Method == RequestMethods.TasksGet)
|
||||
await TasksGetAsync(request, cancellationToken);
|
||||
else if (request.Method == RequestMethods.TasksResult)
|
||||
await TasksResultAsync(request, cancellationToken);
|
||||
else if (request.Method == RequestMethods.TasksList)
|
||||
await TasksListAsync(request, cancellationToken);
|
||||
else if (request.Method == RequestMethods.TasksCancel)
|
||||
await TasksCancelAsync(request, cancellationToken);
|
||||
else
|
||||
await WriteMessageAsync(request, cancellationToken);
|
||||
}
|
||||
|
|
@ -79,161 +71,21 @@ public class TestServerTransport : ITransport
|
|||
|
||||
private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if the request is task-augmented (has Task metadata)
|
||||
var requestParams = JsonSerializer.Deserialize<CreateMessageRequestParams>(request.Params, McpJsonUtilities.DefaultOptions);
|
||||
if (requestParams?.Task is not null && MockTask is not null)
|
||||
// Return a normal sampling response
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
// Return a task-augmented response
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new CreateTaskResult { Task = MockTask }, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return a normal sampling response
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model" }, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model" }, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ElicitAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if the request is task-augmented (has Task metadata)
|
||||
var requestParams = JsonSerializer.Deserialize<ElicitRequestParams>(request.Params, McpJsonUtilities.DefaultOptions);
|
||||
if (requestParams?.Task is not null && MockTask is not null)
|
||||
{
|
||||
// Return a task-augmented response
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new CreateTaskResult { Task = MockTask }, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Return a normal elicitation response
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new ElicitResult { Action = "decline" }, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the task to return from tasks/get requests.
|
||||
/// </summary>
|
||||
public McpTask? MockTask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the result to return from tasks/result requests.
|
||||
/// </summary>
|
||||
public object? MockTaskResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of tasks to return from tasks/list requests.
|
||||
/// </summary>
|
||||
public McpTask[]? MockTaskList { get; set; }
|
||||
|
||||
private async Task TasksGetAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = MockTask ?? new McpTask
|
||||
{
|
||||
TaskId = "test-task-id",
|
||||
Status = McpTaskStatus.Completed,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastUpdatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
// Return a normal elicitation response
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new GetTaskResult
|
||||
{
|
||||
TaskId = task.TaskId,
|
||||
Status = task.Status,
|
||||
StatusMessage = task.StatusMessage,
|
||||
CreatedAt = task.CreatedAt,
|
||||
LastUpdatedAt = task.LastUpdatedAt,
|
||||
TimeToLive = task.TimeToLive,
|
||||
PollInterval = task.PollInterval
|
||||
}, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task TasksResultAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = MockTaskResult ?? new CreateMessageResult
|
||||
{
|
||||
Content = [new TextContentBlock { Text = "Task result" }],
|
||||
Model = "test-model"
|
||||
};
|
||||
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(result, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task TasksListAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = MockTaskList ?? [
|
||||
new McpTask
|
||||
{
|
||||
TaskId = "task-1",
|
||||
Status = McpTaskStatus.Completed,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastUpdatedAt = DateTimeOffset.UtcNow,
|
||||
},
|
||||
new McpTask
|
||||
{
|
||||
TaskId = "task-2",
|
||||
Status = McpTaskStatus.Working,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-3),
|
||||
LastUpdatedAt = DateTimeOffset.UtcNow,
|
||||
}
|
||||
];
|
||||
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new ListTasksResult
|
||||
{
|
||||
Tasks = tasks,
|
||||
}, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task TasksCancelAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = MockTask ?? new McpTask
|
||||
{
|
||||
TaskId = "test-task-id",
|
||||
Status = McpTaskStatus.Cancelled,
|
||||
StatusMessage = "Task cancelled by request",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
LastUpdatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
await WriteMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
Result = JsonSerializer.SerializeToNode(new CancelMcpTaskResult
|
||||
{
|
||||
TaskId = task.TaskId,
|
||||
Status = McpTaskStatus.Cancelled,
|
||||
StatusMessage = task.StatusMessage ?? "Task cancelled",
|
||||
CreatedAt = task.CreatedAt,
|
||||
LastUpdatedAt = DateTimeOffset.UtcNow,
|
||||
TimeToLive = task.TimeToLive,
|
||||
PollInterval = task.PollInterval
|
||||
}, McpJsonUtilities.DefaultOptions),
|
||||
Result = JsonSerializer.SerializeToNode(new ElicitResult { Action = "decline" }, McpJsonUtilities.DefaultOptions),
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<!-- Suppress the experimental tasks warning in test projects -->
|
||||
<!-- Suppress the experimental extensions warning in test projects -->
|
||||
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
|
||||
<!-- Suppress the obsolete SSE warning in test projects that need to test legacy SSE -->
|
||||
<NoWarn>$(NoWarn);MCP9004</NoWarn>
|
||||
|
|
|
|||
|
|
@ -1,342 +0,0 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.AspNetCore.Tests.Utils;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for MCP Tasks feature over HTTP transports.
|
||||
/// Tests task creation, polling, cancellation, and result retrieval.
|
||||
/// </summary>
|
||||
public class HttpTaskIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
|
||||
{
|
||||
private readonly HttpClientTransportOptions DefaultTransportOptions = new()
|
||||
{
|
||||
Endpoint = new("http://localhost:5000/"),
|
||||
Name = "In-memory Streamable HTTP Client",
|
||||
};
|
||||
|
||||
private Task<McpClient> ConnectMcpClientAsync(
|
||||
HttpClient? httpClient = null,
|
||||
HttpClientTransportOptions? transportOptions = null,
|
||||
McpClientOptions? clientOptions = null)
|
||||
=> McpClient.CreateAsync(
|
||||
new HttpClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory),
|
||||
clientOptions,
|
||||
LoggerFactory,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
private static IDictionary<string, JsonElement> CreateArguments(string key, object? value)
|
||||
{
|
||||
return new Dictionary<string, JsonElement>
|
||||
{
|
||||
[key] = JsonSerializer.SerializeToElement(value, McpJsonUtilities.DefaultOptions)
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallToolAsTask_ReturnsTask_WhenServerSupportsTasksAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// Act - Call tool with task augmentation
|
||||
var result = await client.CallToolAsync(
|
||||
new CallToolRequestParams
|
||||
{
|
||||
Name = "long_running_operation",
|
||||
Arguments = CreateArguments("durationMs", 100),
|
||||
Task = new McpTaskMetadata()
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert - Response should indicate task was created
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.IsError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskAsync_ReturnsTaskStatus_WhenTaskExistsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// First create a task by calling a tool with task augmentation
|
||||
_ = await client.CallToolAsync(
|
||||
new CallToolRequestParams
|
||||
{
|
||||
Name = "long_running_operation",
|
||||
Arguments = CreateArguments("durationMs", 500),
|
||||
Task = new McpTaskMetadata()
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Get all tasks
|
||||
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
Assert.NotEmpty(tasks);
|
||||
|
||||
// Act - Get the task status
|
||||
var task = await client.GetTaskAsync(tasks[0].TaskId, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(task);
|
||||
Assert.Equal(tasks[0].TaskId, task.TaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListTasksAsync_ReturnsTasks_WhenTasksExistAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// Create multiple tasks
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await client.CallToolAsync(
|
||||
new CallToolRequestParams
|
||||
{
|
||||
Name = "long_running_operation",
|
||||
Arguments = CreateArguments("durationMs", 1000),
|
||||
Task = new McpTaskMetadata()
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
// Act
|
||||
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(tasks);
|
||||
Assert.Equal(3, tasks.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelTaskAsync_CancelsTask_WhenTaskIsRunningAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// Create a long-running task
|
||||
await client.CallToolAsync(
|
||||
new CallToolRequestParams
|
||||
{
|
||||
Name = "long_running_operation",
|
||||
Arguments = CreateArguments("durationMs", 10000),
|
||||
Task = new McpTaskMetadata()
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
Assert.NotEmpty(tasks);
|
||||
|
||||
// Act - Cancel the task
|
||||
var cancelledTask = await client.CancelTaskAsync(tasks[0].TaskId, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(cancelledTask);
|
||||
Assert.Equal(McpTaskStatus.Cancelled, cancelledTask.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskResultAsync_ReturnsResult_WhenTaskCompletesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// Create a quick task
|
||||
await client.CallToolAsync(
|
||||
new CallToolRequestParams
|
||||
{
|
||||
Name = "long_running_operation",
|
||||
Arguments = CreateArguments("durationMs", 50),
|
||||
Task = new McpTaskMetadata()
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
Assert.NotEmpty(tasks);
|
||||
|
||||
// Wait a bit for the task to complete
|
||||
await Task.Delay(200, TestContext.Current.CancellationToken);
|
||||
|
||||
// Act - Get the task result
|
||||
var result = await client.GetTaskResultAsync(tasks[0].TaskId, cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(default, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TasksIsolated_BetweenSessions_WhenMultipleClientsConnectAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Connect two separate clients
|
||||
await using var client1 = await ConnectMcpClientAsync();
|
||||
await using var client2 = await ConnectMcpClientAsync();
|
||||
|
||||
// Client 1 creates a task
|
||||
await client1.CallToolAsync(
|
||||
new CallToolRequestParams
|
||||
{
|
||||
Name = "long_running_operation",
|
||||
Arguments = CreateArguments("durationMs", 1000),
|
||||
Task = new McpTaskMetadata()
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Act - Both clients list tasks
|
||||
var client1Tasks = await client1.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
var client2Tasks = await client2.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert - Tasks should be isolated by session
|
||||
Assert.Single(client1Tasks);
|
||||
Assert.Empty(client2Tasks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServerCapabilities_IncludesTasks_WhenTaskStoreConfiguredAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Act
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(client.ServerCapabilities?.Tasks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListTools_ShowsTaskSupport_WhenToolIsAsyncAsync()
|
||||
{
|
||||
// Arrange
|
||||
var taskStore = new InMemoryMcpTaskStore();
|
||||
Builder.Services.AddMcpServer(options =>
|
||||
{
|
||||
options.TaskStore = taskStore;
|
||||
})
|
||||
.WithHttpTransport()
|
||||
.WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
app.MapMcp();
|
||||
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var client = await ConnectMcpClientAsync();
|
||||
|
||||
// Act
|
||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
var asyncTool = tools.FirstOrDefault(t => t.Name == "long_running_operation");
|
||||
Assert.NotNull(asyncTool);
|
||||
Assert.NotNull(asyncTool.ProtocolTool.Execution);
|
||||
Assert.Equal(ToolTaskSupport.Optional, asyncTool.ProtocolTool.Execution.TaskSupport);
|
||||
}
|
||||
|
||||
[McpServerToolType]
|
||||
public sealed class LongRunningTools
|
||||
{
|
||||
[McpServerTool, Description("Simulates a long-running operation")]
|
||||
public static async Task<string> LongRunningOperation(
|
||||
[Description("Duration of the operation in milliseconds")] int durationMs,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(durationMs, cancellationToken);
|
||||
return $"Operation completed after {durationMs}ms";
|
||||
}
|
||||
|
||||
[McpServerTool, Description("A synchronous tool that does not support tasks")]
|
||||
public static string SyncTool([Description("Input message")] string message)
|
||||
{
|
||||
return $"Sync result: {message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,27 +162,6 @@ internal static class Program
|
|||
"""),
|
||||
},
|
||||
new Tool
|
||||
{
|
||||
Name = "longRunning",
|
||||
Description = "Simulates a long-running operation that supports task-based execution.",
|
||||
InputSchema = JsonElement.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"durationMs": {
|
||||
"type": "number",
|
||||
"description": "Duration of the operation in milliseconds"
|
||||
}
|
||||
},
|
||||
"required": ["durationMs"]
|
||||
}
|
||||
"""),
|
||||
Execution = new ToolExecution
|
||||
{
|
||||
TaskSupport = ToolTaskSupport.Optional
|
||||
}
|
||||
},
|
||||
new Tool
|
||||
{
|
||||
Name = "crash",
|
||||
Description = "Terminates the server process with a specified exit code.",
|
||||
|
|
@ -245,19 +224,6 @@ internal static class Program
|
|||
Content = [new TextContentBlock { Text = cliArg ?? "null" }]
|
||||
};
|
||||
}
|
||||
else if (request.Params.Name == "longRunning")
|
||||
{
|
||||
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue))
|
||||
{
|
||||
throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
int durationMs = Convert.ToInt32(durationMsValue.GetRawText());
|
||||
await Task.Delay(durationMs, cancellationToken);
|
||||
return new CallToolResult
|
||||
{
|
||||
Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }]
|
||||
};
|
||||
}
|
||||
else if (request.Params.Name == "crash")
|
||||
{
|
||||
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("exitCode", out var exitCodeValue))
|
||||
|
|
|
|||
|
|
@ -146,27 +146,6 @@ public class Program
|
|||
}
|
||||
"""),
|
||||
},
|
||||
new Tool
|
||||
{
|
||||
Name = "longRunning",
|
||||
Description = "Simulates a long-running operation that supports task-based execution.",
|
||||
InputSchema = JsonElement.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"durationMs": {
|
||||
"type": "number",
|
||||
"description": "Duration of the operation in milliseconds"
|
||||
}
|
||||
},
|
||||
"required": ["durationMs"]
|
||||
}
|
||||
"""),
|
||||
Execution = new ToolExecution
|
||||
{
|
||||
TaskSupport = ToolTaskSupport.Optional
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
|
@ -212,19 +191,6 @@ public class Program
|
|||
Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}" }]
|
||||
};
|
||||
}
|
||||
else if (request.Params.Name == "longRunning")
|
||||
{
|
||||
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("durationMs", out var durationMsValue))
|
||||
{
|
||||
throw new McpProtocolException("Missing required argument 'durationMs'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
int durationMs = Convert.ToInt32(durationMsValue.ToString());
|
||||
await Task.Delay(durationMs, cancellationToken);
|
||||
return new CallToolResult
|
||||
{
|
||||
Content = [new TextContentBlock { Text = $"Long-running operation completed after {durationMs}ms" }]
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new McpProtocolException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue