diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index 1090c5377..fda2f311e 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -44,7 +44,6 @@
-
diff --git a/docs/concepts/tasks/tasks.md b/docs/concepts/tasks/tasks.md
index c0b571f77..65928d878 100644
--- a/docs/concepts/tasks/tasks.md
+++ b/docs/concepts/tasks/tasks.md
@@ -1,604 +1,9 @@
---
title: Tasks
-author: eiriktsarpalis
description: MCP Tasks for Long-Running Operations
uid: tasks
---
# MCP Tasks
-
-> [!WARNING]
-> Tasks are an **experimental feature** in the MCP specification (version 2025-11-25). The API may change in future releases. See the [Experimental APIs](../../experimental.md) documentation for details on working with experimental APIs.
-
-The Model Context Protocol (MCP) supports [task-based execution] for long-running operations. Tasks enable a "call-now, fetch-later" pattern where clients can initiate operations that may take significant time to complete, then poll for status and retrieve results when ready.
-
-[task-based execution]: https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks
-
-## Overview
-
-Tasks are useful when operations may take a long time to complete, such as:
-
-- Large dataset processing or analysis
-- Complex report generation
-- Code migration or refactoring operations
-- Machine learning inference or training
-- Batch data transformations
-
-Without tasks, clients must keep connections open for the entire duration of long-running operations. Tasks allow clients to:
-
-1. Initiate an operation and receive a task ID immediately
-2. Disconnect and reconnect later
-3. Poll for status updates
-4. Retrieve results when complete
-5. Cancel operations if needed
-
-## Task Lifecycle
-
-Tasks follow a defined lifecycle through these status values:
-
-| Status | Description |
-|--------|-------------|
-| `working` | Task is actively being processed |
-| `input_required` | Task is waiting for additional input (e.g., elicitation) |
-| `completed` | Task finished successfully; results are available |
-| `failed` | Task encountered an error |
-| `cancelled` | Task was cancelled by the client |
-
-Tasks begin in the `working` status and transition to one of the terminal states (`completed`, `failed`, or `cancelled`). Once in a terminal state, the status cannot change.
-
-## Server Implementation
-
-### Configuring Task Support
-
-To enable task support on a server, configure a task store when setting up the MCP server:
-
-```csharp
-var builder = WebApplication.CreateBuilder(args);
-
-// Create a task store for managing task state
-var taskStore = new InMemoryMcpTaskStore();
-
-builder.Services.AddMcpServer(options =>
-{
- // Enable tasks by providing a task store
- options.TaskStore = taskStore;
-})
-.WithHttpTransport(o => o.Stateless = true)
-.WithTools();
-```
-
-The is a reference implementation suitable for development and single-server deployments. For production multi-server scenarios, implement with a persistent backing store (database, Redis, etc.).
-
-### Task Store Configuration
-
-The `InMemoryMcpTaskStore` constructor accepts several optional parameters:
-
-```csharp
-var taskStore = new InMemoryMcpTaskStore(
- defaultTtl: TimeSpan.FromHours(1), // Default task retention time
- maxTtl: TimeSpan.FromHours(24), // Maximum allowed TTL
- pollInterval: TimeSpan.FromSeconds(1), // Suggested client poll interval
- cleanupInterval: TimeSpan.FromMinutes(5), // Background cleanup frequency
- pageSize: 100, // Tasks per page for listing
- maxTasks: 1000, // Maximum total tasks allowed
- maxTasksPerSession: 100 // Maximum tasks per session
-);
-```
-
-### Tool Task Support
-
-Tools automatically advertise task support when they return `Task`, `ValueTask`, `Task`, or `ValueTask`:
-
-```csharp
-[McpServerToolType]
-public class MyTools
-{
- // This tool automatically supports task-augmented calls
- // because it returns Task (async method)
- [McpServerTool, Description("Processes a large dataset")]
- public static async Task ProcessDataset(
- int recordCount,
- CancellationToken cancellationToken)
- {
- // Long-running operation
- await Task.Delay(5000, cancellationToken);
- return $"Processed {recordCount} records";
- }
-
- // Synchronous tools don't support task augmentation by default
- [McpServerTool, Description("Quick operation")]
- public static string QuickOperation(string input) => $"Result: {input}";
-}
-```
-
-You can explicitly control task support using :
-
-```csharp
-// In Program.cs or configuration
-builder.Services.AddMcpServer()
- .WithTools([
- McpServerTool.Create(
- (int count, CancellationToken ct) => ProcessAsync(count, ct),
- new McpServerToolCreateOptions
- {
- Name = "requiredTaskTool",
- Execution = new ToolExecution
- {
- // Require clients to use task augmentation
- TaskSupport = ToolTaskSupport.Required
- }
- })
- ]);
-```
-
-Task support levels:
-- `Forbidden` (default for sync methods): Tool cannot be called with task augmentation
-- `Optional` (default for async methods): Tool can be called with or without task augmentation
-- `Required`: Tool must be called with task augmentation
-
-### Explicit Task Creation with `IMcpTaskStore`
-
-For more control over task lifecycle, tools can directly interact with and return an `McpTask`. This approach allows you to:
-
-- Create a task and return immediately while work continues in the background
-- Control exactly when and how task status and results are updated
-- Integrate with external systems for task execution
-
-Here's a simple example using `Task.Run` to schedule background work:
-
-```csharp
-[McpServerToolType]
-public class MyTools(IMcpTaskStore taskStore)
-{
- [McpServerTool]
- [Description("Starts a background job and returns a task for polling.")]
- public async Task StartBackgroundJob(
- [Description("Number of items to process")] int itemCount,
- RequestContext context,
- CancellationToken cancellationToken)
- {
- // Create a task in the store - this records the task metadata
- var task = await taskStore.CreateTaskAsync(
- new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(30) },
- context.JsonRpcRequest.Id!,
- context.JsonRpcRequest,
- context.Server.SessionId,
- cancellationToken);
-
- // Schedule work to run in the background (fire-and-forget)
- _ = Task.Run(async () =>
- {
- try
- {
- // Simulate long-running work
- await Task.Delay(TimeSpan.FromSeconds(10));
- var result = $"Processed {itemCount} items successfully";
-
- // Store the completed result
- await taskStore.StoreTaskResultAsync(
- task.TaskId,
- McpTaskStatus.Completed,
- JsonSerializer.SerializeToElement(new CallToolResult
- {
- Content = [new TextContentBlock { Text = result }]
- }),
- context.Server.SessionId);
- }
- catch (Exception ex)
- {
- // Mark task as failed on error
- await taskStore.StoreTaskResultAsync(
- task.TaskId,
- McpTaskStatus.Failed,
- JsonSerializer.SerializeToElement(new CallToolResult
- {
- Content = [new TextContentBlock { Text = ex.Message }],
- IsError = true
- }),
- context.Server.SessionId);
- }
- }, CancellationToken.None);
-
- // Return immediately - client will poll for completion
- return task;
- }
-}
-```
-
-When a tool returns `McpTask`, the SDK bypasses automatic task wrapping and returns the task directly to the client.
-
-
-> [!IMPORTANT]
-> **No Fault Tolerance Guarantees**: Both `InMemoryMcpTaskStore` and the automatic task support for `Task`-returning tool methods do **not** provide fault tolerance. Task state and execution are bounded by the memory of the server process. If the server crashes or restarts:
-> - All in-memory task metadata is lost
-> - Any in-flight task execution is terminated
-> - Clients will receive errors when polling for previously created tasks
->
-> For fault-tolerant task execution, see the [Fault-Tolerant Task Implementations](#fault-tolerant-task-implementations) section.
-
-### Task Status Notifications
-
-When `SendTaskStatusNotifications` is enabled, the server automatically sends status updates to connected clients:
-
-```csharp
-builder.Services.AddMcpServer(options =>
-{
- options.TaskStore = taskStore;
- options.SendTaskStatusNotifications = true; // Enable notifications
-});
-```
-
-Clients receive `notifications/tasks/status` messages when task status changes.
-
-## Client Implementation
-
-### Calling Tools as Tasks
-
-To execute a tool as a task, include the `Task` property in the request:
-
-```csharp
-using ModelContextProtocol.Client;
-using ModelContextProtocol.Protocol;
-
-var client = await McpClient.CreateAsync(transport);
-
-// Call tool with task augmentation
-var result = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "processDataset",
- Arguments = new Dictionary
- {
- ["recordCount"] = JsonSerializer.SerializeToElement(1000)
- },
- Task = new McpTaskMetadata
- {
- TimeToLive = TimeSpan.FromHours(2) // Request 2-hour retention
- }
- },
- cancellationToken);
-
-// Check if a task was created
-if (result.Task != null)
-{
- Console.WriteLine($"Task created: {result.Task.TaskId}");
- Console.WriteLine($"Status: {result.Task.Status}");
-}
-```
-
-### Polling for Task Status
-
-Use to check task status:
-
-```csharp
-var task = await client.GetTaskAsync(taskId, cancellationToken: cancellationToken);
-Console.WriteLine($"Status: {task.Status}");
-Console.WriteLine($"Last Updated: {task.LastUpdatedAt}");
-
-if (task.StatusMessage != null)
-{
- Console.WriteLine($"Message: {task.StatusMessage}");
-}
-```
-
-### Waiting for Completion
-
-The SDK provides helper methods for polling until a task completes:
-
-```csharp
-// Poll until task reaches terminal state
-var completedTask = await client.PollTaskUntilCompleteAsync(
- taskId,
- cancellationToken: cancellationToken);
-
-if (completedTask.Status == McpTaskStatus.Completed)
-{
- // Get the result as raw JSON
- var resultJson = await client.GetTaskResultAsync(
- taskId,
- cancellationToken: cancellationToken);
-
- // Deserialize to the expected type
- var result = resultJson.Deserialize(McpJsonUtilities.DefaultOptions);
-
- foreach (var content in result?.Content ?? [])
- {
- if (content is TextContentBlock text)
- {
- Console.WriteLine(text.Text);
- }
- }
-}
-else if (completedTask.Status == McpTaskStatus.Failed)
-{
- Console.WriteLine($"Task failed: {completedTask.StatusMessage}");
-}
-```
-
-### Listing Tasks
-
-List all tasks for the current session:
-
-```csharp
-var tasks = await client.ListTasksAsync(cancellationToken: cancellationToken);
-
-foreach (var task in tasks)
-{
- Console.WriteLine($"{task.TaskId}: {task.Status}");
-}
-```
-
-### Cancelling Tasks
-
-Cancel a running task:
-
-```csharp
-var cancelledTask = await client.CancelTaskAsync(
- taskId,
- cancellationToken: cancellationToken);
-
-Console.WriteLine($"Task status: {cancelledTask.Status}"); // Cancelled
-```
-
-### Handling Status Notifications
-
-Register a handler to receive real-time status updates:
-
-```csharp
-var options = new McpClientOptions
-{
- Handlers = new McpClientHandlers
- {
- TaskStatusHandler = (task, cancellationToken) =>
- {
- Console.WriteLine($"Task {task.TaskId} status changed to {task.Status}");
- return ValueTask.CompletedTask;
- }
- }
-};
-
-var client = await McpClient.CreateAsync(transport, options);
-```
-
-
-> [!NOTE]
-> Clients should not rely on receiving status notifications. Notifications are optional and may not be sent in all scenarios. Always use polling as the primary mechanism for tracking task status.
-
-## Implementing a Custom Task Store
-
-For production deployments, implement with a persistent backing store:
-
-```csharp
-public class DatabaseTaskStore : IMcpTaskStore
-{
- private readonly IDbConnection _db;
-
- public DatabaseTaskStore(IDbConnection db) => _db = db;
-
- public async Task CreateTaskAsync(
- McpTaskMetadata taskMetadata,
- RequestId requestId,
- JsonRpcRequest request,
- string? sessionId,
- CancellationToken cancellationToken)
- {
- var task = new McpTask
- {
- TaskId = Guid.NewGuid().ToString(),
- Status = McpTaskStatus.Working,
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow,
- TimeToLive = taskMetadata.TimeToLive ?? TimeSpan.FromHours(1)
- };
-
- // Store in database
- await _db.ExecuteAsync(
- "INSERT INTO Tasks (TaskId, SessionId, Status, ...) VALUES (@TaskId, @SessionId, @Status, ...)",
- new { task.TaskId, sessionId, task.Status, ... });
-
- return task;
- }
-
- public async Task GetTaskAsync(
- string taskId,
- string? sessionId,
- CancellationToken cancellationToken)
- {
- // Retrieve from database with session isolation
- return await _db.QuerySingleOrDefaultAsync(
- "SELECT * FROM Tasks WHERE TaskId = @TaskId AND SessionId = @SessionId",
- new { taskId, sessionId });
- }
-
- // Implement other interface methods...
-}
-```
-
-### Task Store Best Practices
-
-1. **Session Isolation**: Always filter tasks by session ID to prevent cross-session access
-2. **TTL Enforcement**: Implement background cleanup of expired tasks
-3. **Thread Safety**: Ensure all operations are thread-safe for concurrent access
-4. **Atomic Updates**: Use database transactions for status transitions
-5. **Optimistic Concurrency**: Prevent lost updates with version checking or row locks
-
-## Error Handling
-
-Task operations may throw with these error codes:
-
-| Error Code | Scenario |
-|------------|----------|
-| `InvalidParams` | Invalid or nonexistent task ID or invalid cursor |
-| `InvalidParams` | Tool with `taskSupport: forbidden` called with task metadata, or tool with `taskSupport: required` called without task metadata |
-| `InternalError` | Task execution failure or result unavailable |
-
-Example error handling:
-
-```csharp
-try
-{
- var task = await client.GetTaskAsync(taskId, cancellationToken: ct);
-}
-catch (McpProtocolException ex) when (ex.ErrorCode == McpErrorCode.InvalidParams)
-{
- Console.WriteLine($"Task not found: {taskId}");
-}
-```
-
-## Complete Example
-
-
-
-See the [LongRunningTasks sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/LongRunningTasks) for a complete working example demonstrating:
-
-
-- Server setup with a file-based `IMcpTaskStore` for durability
-- Explicit task creation via `IMcpTaskStore` in tools returning `McpTask`
-- Task polling and result retrieval across server restarts
-- Cancellation support
-
-## Fault-Tolerant Task Implementations
-
-The default `InMemoryMcpTaskStore` and automatic task support for async tools are convenient for development, but they provide no durability or fault tolerance. When the server process terminates—whether due to a crash, deployment, or scaling event—all task state and in-flight computations are lost.
-
-### Why Fault Tolerance Requires External Systems
-
-True fault tolerance for long-running tasks requires two key capabilities that cannot be provided by an in-process solution:
-
-1. **Durable Task State**: Task metadata (ID, status, results) must survive process termination. This requires an external persistent store such as a database, Redis, or distributed cache.
-
-2. **Resumable Compute**: The actual work being performed must be executed by an external system that can continue running independently of the MCP server process—such as a job queue (Azure Service Bus, RabbitMQ), workflow engine (Temporal, Azure Durable Functions), or batch processing system (Azure Batch, Kubernetes Jobs).
-
-### Explicit Task Creation with `IMcpTaskStore`
-
-To implement fault-tolerant tasks, tools can directly interact with `IMcpTaskStore` and return an `McpTask` instead of relying on automatic task wrapping. This approach gives you full control over task lifecycle and enables integration with external compute fabrics:
-
-```csharp
-[McpServerToolType]
-public class FaultTolerantTools(IMcpTaskStore taskStore, IJobQueue jobQueue)
-{
- [McpServerTool]
- [Description("Submits a long-running job with fault-tolerant execution.")]
- public async Task SubmitJob(
- [Description("The job parameters")] string jobInput,
- RequestContext context,
- CancellationToken cancellationToken)
- {
- // 1. Create a task in the durable store
- var task = await taskStore.CreateTaskAsync(
- new McpTaskMetadata { TimeToLive = TimeSpan.FromHours(24) },
- context.JsonRpcRequest.Id!,
- context.JsonRpcRequest,
- context.Server.SessionId,
- cancellationToken);
-
- // 2. Submit work to an external compute fabric
- // The job queue handles execution independently of this process
- await jobQueue.EnqueueAsync(new JobMessage
- {
- TaskId = task.TaskId,
- SessionId = context.Server.SessionId,
- Input = jobInput
- }, cancellationToken);
-
- // 3. Return the task immediately - client will poll for completion
- return task;
- }
-}
-```
-
-The external job processor updates the task store when work completes:
-
-```csharp
-// In a separate worker process or Azure Function
-public class JobProcessor(IMcpTaskStore taskStore)
-{
- public async Task ProcessJobAsync(JobMessage job, CancellationToken cancellationToken)
- {
- try
- {
- // Perform the actual long-running work
- var result = await DoExpensiveWorkAsync(job.Input, cancellationToken);
-
- // Store the result in the durable task store
- await taskStore.StoreTaskResultAsync(
- job.TaskId,
- McpTaskStatus.Completed,
- JsonSerializer.SerializeToElement(new CallToolResult
- {
- Content = [new TextContentBlock { Text = result }]
- }),
- job.SessionId,
- cancellationToken);
- }
- catch (Exception ex)
- {
- // Mark task as failed
- await taskStore.StoreTaskResultAsync(
- job.TaskId,
- McpTaskStatus.Failed,
- JsonSerializer.SerializeToElement(new CallToolResult
- {
- Content = [new TextContentBlock { Text = ex.Message }],
- IsError = true
- }),
- job.SessionId,
- cancellationToken);
- }
- }
-}
-```
-
-### Simplified Example: File-Based Task Store
-
-
-
-The [LongRunningTasks sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/LongRunningTasks) demonstrates a simplified fault-tolerant approach using the file system. The `FileBasedMcpTaskStore` persists task state to disk, allowing tasks to survive server restarts:
-
-
-```csharp
-// Use a file-based task store for durability
-var taskStorePath = Path.Combine(Path.GetTempPath(), "mcp-tasks");
-var taskStore = new FileBasedMcpTaskStore(taskStorePath);
-
-builder.Services.AddMcpServer(options =>
-{
- options.TaskStore = taskStore;
-})
-.WithHttpTransport(o => o.Stateless = true)
-.WithTools();
-```
-
-The sample's tool returns an `McpTask` directly by calling `CreateTaskAsync`:
-
-```csharp
-[McpServerToolType]
-public class TaskTools(IMcpTaskStore taskStore)
-{
- [McpServerTool]
- [Description("Submits a job and returns a task that can be polled for completion.")]
- public async Task SubmitJob(
- [Description("A label for the job")] string jobName,
- RequestContext context,
- CancellationToken cancellationToken)
- {
- return await taskStore.CreateTaskAsync(
- new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(10) },
- context.JsonRpcRequest.Id!,
- context.JsonRpcRequest,
- context.Server.SessionId,
- cancellationToken);
- }
-}
-```
-
-While this file-based approach demonstrates the pattern, production systems should use proper distributed storage and compute infrastructure for true fault tolerance and scalability.
-
-## See Also
-
--
--
--
--
-- [MCP Tasks Specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)
+
\ No newline at end of file
diff --git a/docs/experimental.md b/docs/experimental.md
index 1ad75a9b4..59a1d7579 100644
--- a/docs/experimental.md
+++ b/docs/experimental.md
@@ -26,8 +26,8 @@ Add the diagnostic ID to `` in your project file:
Use `#pragma warning disable` around specific call sites:
```csharp
-#pragma warning disable MCPEXP001 // The Tasks feature is experimental per the MCP specification and is subject to change.
-tool.Execution = new ToolExecution { ... };
+#pragma warning disable MCPEXP001 // The Extensions feature is part of a future MCP specification version that has not yet been ratified and is subject to change.
+capabilities.Extensions = new Dictionary { ... };
#pragma warning restore MCPEXP001
```
@@ -67,4 +67,3 @@ By placing the SDK's resolver first, MCP types are serialized using the SDK's co
- [Versioning](versioning.md)
- [List of diagnostics](list-of-diagnostics.md#experimental-apis)
-- [Tasks](concepts/tasks/tasks.md) (an experimental feature)
diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md
index 515472817..fb44442ef 100644
--- a/docs/list-of-diagnostics.md
+++ b/docs/list-of-diagnostics.md
@@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
| Diagnostic ID | Description |
| :------------ | :---------- |
-| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
+| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Extensions. Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
| `MCPEXP002` | Experimental SDK APIs unrelated to the MCP specification itself, including subclassing `McpClient`/`McpServer` (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)) and `RunSessionHandler`, which may be removed or change signatures in a future release (consider using `ConfigureSessionOptions` instead). |
## Obsolete APIs
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 81955a710..105a039d0 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -12,7 +12,7 @@ The C# SDK tracks implementation of MCP spec components using the [modelcontextp
### Next Spec Revision
-The next MCP specification revision is being developed in the [protocol repository](https://github.com/modelcontextprotocol/modelcontextprotocol). The C# SDK already has experimental support for [Tasks](concepts/tasks/tasks.md) (experimental in the specification), which will be updated as the specification is revised.
+The next MCP specification revision is being developed in the [protocol repository](https://github.com/modelcontextprotocol/modelcontextprotocol).
### Feedback and End-to-End Scenarios
diff --git a/samples/LongRunningTasks/FileBasedMcpTaskStore.cs b/samples/LongRunningTasks/FileBasedMcpTaskStore.cs
deleted file mode 100644
index 55a6e77d5..000000000
--- a/samples/LongRunningTasks/FileBasedMcpTaskStore.cs
+++ /dev/null
@@ -1,393 +0,0 @@
-using ModelContextProtocol;
-using ModelContextProtocol.Protocol;
-using System.Text.Json;
-using System.Text.Json.Nodes;
-using System.Text.Json.Serialization;
-
-namespace LongRunningTasks;
-
-///
-/// A minimal file-based implementation of that demonstrates
-/// durable, fault-tolerant task storage using simple time-based completion.
-///
-///
-///
-/// This implementation stores task data to disk: task ID, creation timestamp, execution duration,
-/// session ID, TTL, and optional result. Task completion is determined by:
-///
-/// - Explicit completion or failure via
-/// - Explicit cancellation via
-/// - Time-based auto-completion when execution time has elapsed
-///
-///
-///
-/// The file-based approach enables durability across process restarts - if the server
-/// crashes and restarts, tasks can still be queried and will complete based on elapsed time.
-///
-///
-public sealed partial class FileBasedMcpTaskStore : IMcpTaskStore
-{
- private readonly string _storePath;
- private readonly TimeSpan _executionTime;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The directory path where task files will be stored.
- ///
- /// The fixed execution time for all tasks. Tasks are reported as completed once this
- /// duration has elapsed since creation. Defaults to 5 seconds.
- ///
- public FileBasedMcpTaskStore(string storePath, TimeSpan? executionTime = null)
- {
- _storePath = storePath ?? throw new ArgumentNullException(nameof(storePath));
- _executionTime = executionTime ?? TimeSpan.FromSeconds(5);
- Directory.CreateDirectory(_storePath);
- }
-
- ///
- public async Task CreateTaskAsync(
- McpTaskMetadata taskParams,
- RequestId requestId,
- JsonRpcRequest request,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- var taskId = Guid.NewGuid().ToString("N");
- var now = DateTimeOffset.UtcNow;
-
- var entry = new TaskFileEntry
- {
- TaskId = taskId,
- SessionId = sessionId,
- Status = McpTaskStatus.Working,
- CreatedAt = now,
- ExecutionTime = _executionTime,
- TimeToLive = taskParams.TimeToLive,
- Result = JsonSerializer.SerializeToElement(request.Params, JsonContext.Default.JsonNode)
- };
-
- await WriteTaskEntryAsync(GetTaskFilePath(taskId), entry);
-
- return ToMcpTask(entry);
- }
-
- ///
- public async Task GetTaskAsync(
- string taskId,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- var entry = await ReadTaskEntryAsync(taskId);
- if (entry is null)
- {
- return null;
- }
-
- // Session isolation
- if (sessionId is not null && entry.SessionId != sessionId)
- {
- return null;
- }
-
- // Skip if TTL has expired
- if (IsExpired(entry))
- {
- return null;
- }
-
- return ToMcpTask(entry);
- }
-
- ///
- public async Task StoreTaskResultAsync(
- string taskId,
- McpTaskStatus status,
- JsonElement result,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- if (status is not (McpTaskStatus.Completed or McpTaskStatus.Failed))
- {
- throw new ArgumentException(
- $"Status must be {nameof(McpTaskStatus.Completed)} or {nameof(McpTaskStatus.Failed)}.",
- nameof(status));
- }
-
- var updatedEntry = await UpdateTaskEntryAsync(taskId, sessionId, entry =>
- {
- var effectiveStatus = GetEffectiveStatus(entry);
- if (IsTerminalStatus(effectiveStatus))
- {
- throw new InvalidOperationException(
- $"Cannot store result for task in terminal state: {effectiveStatus}");
- }
-
- return entry with
- {
- Status = status,
- Result = result
- };
- });
-
- return ToMcpTask(updatedEntry);
- }
-
- ///
- public async Task GetTaskResultAsync(
- string taskId,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- var entry = await ReadTaskEntryAsync(taskId)
- ?? throw new InvalidOperationException($"Task not found: {taskId}");
-
- if (sessionId is not null && entry.SessionId != sessionId)
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- var effectiveStatus = GetEffectiveStatus(entry);
- if (!IsTerminalStatus(effectiveStatus))
- {
- throw new InvalidOperationException($"Task not yet completed: {taskId}");
- }
-
- // Return stored result
- return entry.Result ?? default;
- }
-
- ///
- public async Task UpdateTaskStatusAsync(
- string taskId,
- McpTaskStatus status,
- string? statusMessage,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- var updatedEntry = await UpdateTaskEntryAsync(taskId, sessionId, entry =>
- entry with
- {
- Status = status,
- StatusMessage = statusMessage
- });
-
- return ToMcpTask(updatedEntry);
- }
-
- ///
- public async Task ListTasksAsync(
- string? cursor = null,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- var tasks = new List();
-
- foreach (var file in Directory.EnumerateFiles(_storePath, "*.json"))
- {
- try
- {
- var entry = await ReadTaskEntryFromFileAsync(file);
- if (entry is not null)
- {
- // Session isolation
- if (sessionId is not null && entry.SessionId != sessionId)
- {
- continue;
- }
-
- // Skip expired tasks
- if (IsExpired(entry))
- {
- continue;
- }
-
- tasks.Add(ToMcpTask(entry));
- }
- }
- catch
- {
- // Skip corrupted or inaccessible files
- }
- }
-
- tasks.Sort((a, b) => a.CreatedAt.CompareTo(b.CreatedAt));
-
- return new ListTasksResult { Tasks = [.. tasks] };
- }
-
- ///
- public async Task CancelTaskAsync(
- string taskId,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- var updatedEntry = await UpdateTaskEntryAsync(taskId, sessionId, entry =>
- {
- var effectiveStatus = GetEffectiveStatus(entry);
- if (IsTerminalStatus(effectiveStatus))
- {
- // Already terminal, return unchanged
- return entry;
- }
-
- return entry with { Status = McpTaskStatus.Cancelled };
- });
-
- return ToMcpTask(updatedEntry);
- }
-
- private string GetTaskFilePath(string taskId) => Path.Combine(_storePath, $"{taskId}.json");
-
- ///
- /// Reads, transforms, and writes a task entry while holding an exclusive file lock.
- ///
- /// The task ID to update.
- /// Optional session ID for access control.
- /// A function that transforms the entry. May throw to abort the update.
- /// The updated task entry.
- private async Task UpdateTaskEntryAsync(
- string taskId,
- string? sessionId,
- Func updateFunc)
- {
- var filePath = GetTaskFilePath(taskId);
-
- // Acquire exclusive lock on the file for the entire read-modify-write cycle
- using var stream = await AcquireFileStreamAsync(filePath, FileMode.Open, FileAccess.ReadWrite);
-
- var entry = await JsonSerializer.DeserializeAsync(stream, JsonContext.Default.TaskFileEntry)
- ?? throw new InvalidOperationException($"Task not found: {taskId}");
-
- // Enforce session isolation
- if (sessionId is not null && entry.SessionId != sessionId)
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // Apply the transformation (may throw to abort)
- var updatedEntry = updateFunc(entry);
-
- // Write back to the same stream
- stream.SetLength(0);
- stream.Position = 0;
- await JsonSerializer.SerializeAsync(stream, updatedEntry, JsonContext.Default.TaskFileEntry);
-
- return updatedEntry;
- }
-
- private async Task ReadTaskEntryAsync(string taskId)
- {
- var filePath = GetTaskFilePath(taskId);
- return File.Exists(filePath) ? await ReadTaskEntryFromFileAsync(filePath) : null;
- }
-
- private static async Task ReadTaskEntryFromFileAsync(string filePath)
- {
- try
- {
- using var stream = await AcquireFileStreamAsync(filePath, FileMode.Open, FileAccess.Read);
- return await JsonSerializer.DeserializeAsync(stream, JsonContext.Default.TaskFileEntry);
- }
- catch
- {
- return null;
- }
- }
-
- private static async Task WriteTaskEntryAsync(string filePath, TaskFileEntry entry)
- {
- using var stream = await AcquireFileStreamAsync(filePath, FileMode.Create, FileAccess.Write);
- await JsonSerializer.SerializeAsync(stream, entry, JsonContext.Default.TaskFileEntry);
- }
-
- private static async Task AcquireFileStreamAsync(string filePath, FileMode fileMode, FileAccess fileAccess)
- {
- const int MaxRetries = 10;
- const int RetryDelayMs = 50;
-
- for (int attempt = 0; ; attempt++)
- {
- try
- {
- return new FileStream(filePath, fileMode, fileAccess, FileShare.None);
- }
- catch (IOException) when (attempt < MaxRetries)
- {
- await Task.Delay(RetryDelayMs); // File is locked by another process, wait and retry
- }
- }
- }
-
- private McpTask ToMcpTask(TaskFileEntry entry)
- {
- var now = DateTimeOffset.UtcNow;
- return new McpTask
- {
- TaskId = entry.TaskId,
- Status = GetEffectiveStatus(entry),
- StatusMessage = entry.StatusMessage,
- CreatedAt = entry.CreatedAt,
- LastUpdatedAt = now,
- TimeToLive = entry.TimeToLive
- };
- }
-
- private static McpTaskStatus GetEffectiveStatus(TaskFileEntry entry)
- {
- // If already in a terminal state, return it
- if (IsTerminalStatus(entry.Status))
- {
- return entry.Status;
- }
-
- // Check if execution time has elapsed - auto-complete
- if (DateTimeOffset.UtcNow - entry.CreatedAt >= entry.ExecutionTime)
- {
- return McpTaskStatus.Completed;
- }
-
- return entry.Status;
- }
-
- private static bool IsTerminalStatus(McpTaskStatus status) =>
- status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled;
-
- private static bool IsExpired(TaskFileEntry entry) =>
- entry.TimeToLive.HasValue && DateTimeOffset.UtcNow - entry.CreatedAt > entry.TimeToLive.Value;
-
- ///
- /// Represents the data stored for each task.
- ///
- private sealed record TaskFileEntry
- {
- /// The unique task identifier.
- public required string TaskId { get; init; }
-
- /// The session that created this task.
- public string? SessionId { get; init; }
-
- /// The current task status.
- public required McpTaskStatus Status { get; init; }
-
- /// Optional status message describing the current state.
- public string? StatusMessage { get; init; }
-
- /// When the task was created.
- public required DateTimeOffset CreatedAt { get; init; }
-
- /// How long until the task is considered complete (if not explicitly completed).
- public required TimeSpan ExecutionTime { get; init; }
-
- /// Time to live - task is filtered out after this duration from creation.
- public TimeSpan? TimeToLive { get; init; }
-
- /// The task result - initialized with request params, updated via StoreTaskResultAsync.
- public JsonElement? Result { get; init; }
- }
-
- [JsonSourceGenerationOptions(WriteIndented = true)]
- [JsonSerializable(typeof(TaskFileEntry))]
- [JsonSerializable(typeof(JsonNode))]
- private sealed partial class JsonContext : JsonSerializerContext;
-}
diff --git a/samples/LongRunningTasks/LongRunningTasks.csproj b/samples/LongRunningTasks/LongRunningTasks.csproj
deleted file mode 100644
index ffe1fc716..000000000
--- a/samples/LongRunningTasks/LongRunningTasks.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net9.0
- enable
- enable
- $(NoWarn);MCPEXP001
-
-
-
-
-
-
-
diff --git a/samples/LongRunningTasks/Program.cs b/samples/LongRunningTasks/Program.cs
deleted file mode 100644
index ee9174554..000000000
--- a/samples/LongRunningTasks/Program.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// This sample demonstrates using a custom IMcpTaskStore implementation for
-// durable task storage. The FileBasedMcpTaskStore persists tasks to disk,
-// allowing them to survive server restarts.
-//
-// To test:
-// 1. Start the server and call the SubmitJob tool
-// 2. Poll the returned task using tasks/get
-// 3. Optionally restart the server - the task will still be queryable
-
-using LongRunningTasks;
-using LongRunningTasks.Tools;
-
-var builder = WebApplication.CreateBuilder(args);
-
-// Use a file-based task store for persistence across server restarts.
-// Tasks survive server restarts and can be resumed or queried after a crash.
-var taskStorePath = Path.Combine(Path.GetTempPath(), "mcp-tasks");
-var taskStore = new FileBasedMcpTaskStore(taskStorePath);
-
-builder.Services.AddMcpServer(options =>
-{
- options.TaskStore = taskStore;
- options.ServerInfo = new()
- {
- Name = "LongRunningTasksServer",
- Version = "1.0.0"
- };
-})
-.WithHttpTransport(o => o.Stateless = true)
-.WithTools();
-
-var app = builder.Build();
-app.MapMcp();
-app.Run();
diff --git a/samples/LongRunningTasks/Properties/launchSettings.json b/samples/LongRunningTasks/Properties/launchSettings.json
deleted file mode 100644
index 9a7c84f4b..000000000
--- a/samples/LongRunningTasks/Properties/launchSettings.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "profiles": {
- "LongRunningTasks": {
- "commandName": "Project",
- "launchBrowser": true,
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- },
- "applicationUrl": "https://localhost:60964;http://localhost:60965"
- }
- }
-}
\ No newline at end of file
diff --git a/samples/LongRunningTasks/README.md b/samples/LongRunningTasks/README.md
deleted file mode 100644
index 71130e44a..000000000
--- a/samples/LongRunningTasks/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Long-Running Tasks Sample
-
-This sample demonstrates **explicit task handling** in MCP servers using the `IMcpTaskStore` interface directly. Unlike implicit task handling (where the server framework manages tasks automatically), this approach gives you full control over task lifecycle.
\ No newline at end of file
diff --git a/samples/LongRunningTasks/Tools/TaskTools.cs b/samples/LongRunningTasks/Tools/TaskTools.cs
deleted file mode 100644
index 30eb43335..000000000
--- a/samples/LongRunningTasks/Tools/TaskTools.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using ModelContextProtocol;
-using ModelContextProtocol.Protocol;
-using ModelContextProtocol.Server;
-using System.ComponentModel;
-
-namespace LongRunningTasks.Tools;
-
-///
-/// Demonstrates creating and returning tasks via .
-///
-[McpServerToolType]
-public class TaskTools(IMcpTaskStore taskStore)
-{
- ///
- /// Submits a job to the task store and returns a task handle for polling.
- ///
- [McpServerTool]
- [Description("Submits a job and returns a task that can be polled for completion.")]
- public Task SubmitJob(
- [Description("A label for the job")] string jobName,
- RequestContext context,
- CancellationToken cancellationToken)
- {
- return taskStore.CreateTaskAsync(
- new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(10) },
- context.JsonRpcRequest.Id!,
- context.JsonRpcRequest,
- context.Server.SessionId,
- cancellationToken);
- }
-}
diff --git a/samples/LongRunningTasks/appsettings.json b/samples/LongRunningTasks/appsettings.json
deleted file mode 100644
index 757d8426e..000000000
--- a/samples/LongRunningTasks/appsettings.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "localhost;127.0.0.1;[::1]"
-}
diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs
index 7e7e969bb..1fe7979b0 100644
--- a/src/Common/Experimentals.cs
+++ b/src/Common/Experimentals.cs
@@ -10,7 +10,7 @@ namespace ModelContextProtocol;
///
/// -
/// MCPEXP001 covers APIs related to experimental features in the MCP specification itself,
-/// such as Tasks and Extensions. These APIs may change as the specification evolves.
+/// such as Extensions. These APIs may change as the specification evolves.
///
/// -
/// MCPEXP002 covers experimental SDK APIs that are unrelated to the MCP specification,
@@ -35,30 +35,9 @@ namespace ModelContextProtocol;
///
internal static class Experimentals
{
- ///
- /// Diagnostic ID for the experimental MCP Tasks feature.
- ///
- public const string Tasks_DiagnosticId = "MCPEXP001";
-
- ///
- /// Message for the experimental MCP Tasks feature.
- ///
- public const string Tasks_Message = "The Tasks feature is experimental per the MCP specification and is subject to change.";
-
- ///
- /// URL for the experimental MCP Tasks feature.
- ///
- public const string Tasks_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
-
///
/// Diagnostic ID for the experimental MCP Extensions feature.
///
- ///
- /// This uses the same diagnostic ID as because both
- /// Tasks and Extensions are covered by the same MCPEXP001 diagnostic for experimental
- /// MCP features. Having separate constants improves code clarity while maintaining a
- /// single diagnostic suppression point.
- ///
public const string Extensions_DiagnosticId = "MCPEXP001";
///
diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
index f04c32ffd..e12f2a39a 100644
--- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
@@ -969,330 +969,6 @@ public ValueTask CallToolAsync(
cancellationToken: cancellationToken);
}
- ///
- /// Invokes a tool on the server as a task for long-running operations.
- ///
- /// The name of the tool to call on the server.
- /// An optional dictionary of arguments to pass to the tool.
- /// Metadata for task augmentation, including optional TTL. If , an empty metadata is used.
- /// An optional progress reporter for server notifications.
- /// Optional request options including metadata, serialization settings, and progress tracking.
- /// The to monitor for cancellation requests. The default is .
- ///
- /// An representing the created task. Use to poll for status updates
- /// and to retrieve the final result.
- ///
- /// is .
- /// The request failed or the server returned an error response.
- ///
- ///
- /// Task-augmented tool calls allow long-running operations to be executed asynchronously. Instead of blocking
- /// until the tool completes, the server immediately returns a task identifier that can be used to poll for
- /// status updates and retrieve the final result.
- ///
- ///
- /// The server must advertise task support via capabilities.tasks.requests.tools.call and the tool
- /// must have execution.taskSupport set to "optional" or "required".
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public ValueTask CallToolAsTaskAsync(
- string toolName,
- IReadOnlyDictionary? arguments = null,
- McpTaskMetadata? taskMetadata = null,
- IProgress? progress = null,
- RequestOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNull(toolName);
-
- var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
- serializerOptions.MakeReadOnly();
-
- if (progress is null)
- {
- return SendTaskAugmentedCallToolRequestAsync(toolName, arguments, taskMetadata, options?.GetMetaForRequest(), serializerOptions, cancellationToken);
- }
-
- return SendTaskAugmentedCallToolRequestWithProgressAsync(toolName, arguments, taskMetadata, progress, options?.GetMetaForRequest(), serializerOptions, cancellationToken);
-
- async ValueTask SendTaskAugmentedCallToolRequestAsync(
- string toolName,
- IReadOnlyDictionary? arguments,
- McpTaskMetadata? taskMetadata,
- JsonObject? meta,
- JsonSerializerOptions serializerOptions,
- CancellationToken cancellationToken)
- {
- var result = await SendRequestAsync(
- RequestMethods.ToolsCall,
- new CallToolRequestParams
- {
- Name = toolName,
- Arguments = ToArgumentsDictionary(arguments, serializerOptions),
- Meta = meta,
- Task = taskMetadata ?? new McpTaskMetadata(),
- },
- McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
- McpJsonUtilities.JsonContext.Default.CreateTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- return result.Task;
- }
-
- async ValueTask SendTaskAugmentedCallToolRequestWithProgressAsync(
- string toolName,
- IReadOnlyDictionary? arguments,
- McpTaskMetadata? taskMetadata,
- IProgress progress,
- JsonObject? meta,
- JsonSerializerOptions serializerOptions,
- CancellationToken cancellationToken)
- {
- ProgressToken progressToken = new(Guid.NewGuid().ToString("N"));
-
- await using var _ = RegisterNotificationHandler(NotificationMethods.ProgressNotification,
- (notification, cancellationToken) =>
- {
- if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn &&
- pn.ProgressToken == progressToken)
- {
- progress.Report(pn.Progress);
- }
-
- return default;
- }).ConfigureAwait(false);
-
- JsonObject metaWithProgress = meta is not null ? (JsonObject)meta.DeepClone() : [];
- metaWithProgress["progressToken"] = progressToken.ToString();
-
- var result = await SendRequestAsync(
- RequestMethods.ToolsCall,
- new CallToolRequestParams
- {
- Name = toolName,
- Arguments = ToArgumentsDictionary(arguments, serializerOptions),
- Meta = metaWithProgress,
- Task = taskMetadata ?? new McpTaskMetadata(),
- },
- McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
- McpJsonUtilities.JsonContext.Default.CreateTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- return result.Task;
- }
- }
-
- ///
- /// Retrieves the current state of a specific task from the server.
- ///
- /// The unique identifier of the task to retrieve.
- /// Optional request options including metadata, serialization settings, and progress tracking.
- /// The to monitor for cancellation requests. The default is .
- /// The current state of the task.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The request failed or the server returned an error response.
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask GetTaskAsync(
- string taskId,
- RequestOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
-
- var result = await SendRequestAsync(
- RequestMethods.TasksGet,
- new GetTaskRequestParams { TaskId = taskId, Meta = options?.GetMetaForRequest() },
- McpJsonUtilities.JsonContext.Default.GetTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.GetTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- // Convert GetTaskResult to McpTask
- return new McpTask
- {
- TaskId = result.TaskId,
- Status = result.Status,
- StatusMessage = result.StatusMessage,
- CreatedAt = result.CreatedAt,
- LastUpdatedAt = result.LastUpdatedAt,
- TimeToLive = result.TimeToLive,
- PollInterval = result.PollInterval
- };
- }
-
- ///
- /// Retrieves the result of a completed task, blocking until the task reaches a terminal state.
- ///
- /// The unique identifier of the task whose result to retrieve.
- /// Optional request options including metadata, serialization settings, and progress tracking.
- /// The to monitor for cancellation requests. The default is .
- /// The raw JSON result of the task.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The request failed or the server returned an error response.
- ///
- /// This method sends a tasks/result request to the server, which will block until the task completes if it hasn't already.
- /// The server handles all polling logic internally.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public ValueTask GetTaskResultAsync(
- string taskId,
- RequestOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
-
- return SendRequestAsync(
- RequestMethods.TasksResult,
- new GetTaskPayloadRequestParams { TaskId = taskId, Meta = options?.GetMetaForRequest() },
- McpJsonUtilities.JsonContext.Default.GetTaskPayloadRequestParams,
- McpJsonUtilities.JsonContext.Default.JsonElement,
- cancellationToken: cancellationToken);
- }
-
- ///
- /// Retrieves a list of all tasks from the server.
- ///
- /// Optional request options including metadata, serialization settings, and progress tracking.
- /// The to monitor for cancellation requests. The default is .
- /// A list of all tasks.
- /// The request failed or the server returned an error response.
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask> ListTasksAsync(
- RequestOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- ListTasksRequestParams requestParams = new() { Meta = options?.GetMetaForRequest() };
- List tasks = new();
- do
- {
- var taskResults = await ListTasksAsync(requestParams, cancellationToken).ConfigureAwait(false);
- tasks.AddRange(taskResults.Tasks);
- requestParams.Cursor = taskResults.NextCursor;
- }
- while (requestParams.Cursor is not null);
-
- return tasks;
- }
-
- ///
- /// Retrieves a list of tasks from the server.
- ///
- /// The request parameters to send in the request.
- /// The to monitor for cancellation requests. The default is .
- /// The result of the request as provided by the server.
- /// is .
- /// The request failed or the server returned an error response.
- ///
- /// The overload retrieves all tasks by automatically handling pagination.
- /// This overload works with the lower-level and , returning the raw result from the server.
- /// Any pagination needs to be managed by the caller.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public ValueTask ListTasksAsync(
- ListTasksRequestParams requestParams,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNull(requestParams);
-
- return SendRequestAsync(
- RequestMethods.TasksList,
- requestParams,
- McpJsonUtilities.JsonContext.Default.ListTasksRequestParams,
- McpJsonUtilities.JsonContext.Default.ListTasksResult,
- cancellationToken: cancellationToken);
- }
-
- ///
- /// Cancels a running task on the server.
- ///
- /// The unique identifier of the task to cancel.
- /// Optional request options including metadata, serialization settings, and progress tracking.
- /// The to monitor for cancellation requests. The default is .
- /// The updated state of the task after cancellation.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The request failed or the server returned an error response.
- ///
- /// Cancelling a task requests that the server stop execution. The server may not immediately cancel the task,
- /// and may choose to allow the task to complete if it's close to finishing.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask CancelTaskAsync(
- string taskId,
- RequestOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
-
- var result = await SendRequestAsync(
- RequestMethods.TasksCancel,
- new CancelMcpTaskRequestParams { TaskId = taskId, Meta = options?.GetMetaForRequest() },
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- // Convert CancelMcpTaskResult to McpTask
- return new McpTask
- {
- TaskId = result.TaskId,
- Status = result.Status,
- StatusMessage = result.StatusMessage,
- CreatedAt = result.CreatedAt,
- LastUpdatedAt = result.LastUpdatedAt,
- TimeToLive = result.TimeToLive,
- PollInterval = result.PollInterval
- };
- }
-
- ///
- /// Polls a task until it reaches a terminal status (completed, failed, or cancelled).
- ///
- /// The unique identifier of the task to poll.
- /// Optional request options including metadata, serialization settings, and progress tracking.
- /// The to monitor for cancellation requests. The default is .
- /// The task in its terminal state.
- /// is .
- /// is empty or composed entirely of whitespace.
- ///
- ///
- /// This method repeatedly calls until the task reaches a terminal status.
- /// It respects the returned by the server to determine how long
- /// to wait between polling attempts.
- ///
- ///
- /// For retrieving the actual result of a completed task, use .
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask PollTaskUntilCompleteAsync(
- string taskId,
- RequestOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
-
- McpTask task;
- do
- {
- task = await GetTaskAsync(taskId, options, cancellationToken).ConfigureAwait(false);
-
- // If task is in a terminal state, we're done
- if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled)
- {
- break;
- }
-
- // Wait for the poll interval before checking again (default to 1 second)
- var pollInterval = task.PollInterval ?? TimeSpan.FromSeconds(1);
- await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
- }
- while (true);
-
- return task;
- }
-
///
/// Sets the logging level for the server to control which log messages are sent to the client.
///
diff --git a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs
index 2109555bc..0866e4aef 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs
@@ -86,25 +86,4 @@ public sealed class McpClientHandlers
///
///
public Func, CancellationToken, ValueTask>? SamplingHandler { get; set; }
-
- ///
- /// Gets or sets the handler for processing notifications.
- ///
- ///
- ///
- /// This handler is called when the server sends a task status notification to inform the client
- /// about changes to a task's state. These notifications are optional and clients MUST NOT rely
- /// on receiving them.
- ///
- ///
- /// The handler receives the updated object containing the current task state,
- /// including its status, status message, and timestamps.
- ///
- ///
- /// This handler is typically used to update UI or trigger actions based on task progress
- /// without requiring explicit polling.
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public Func? TaskStatusHandler { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs
index 0d5803559..5ad7281ef 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs
@@ -22,7 +22,6 @@ internal sealed partial class McpClientImpl : McpClient
private readonly McpClientOptions _options;
private readonly McpSessionHandler _sessionHandler;
private readonly SemaphoreSlim _disposeLock = new(1, 1);
- private readonly McpTaskCancellationTokenProvider? _taskCancellationTokenProvider;
private readonly ConcurrentDictionary _toolCache = new(StringComparer.Ordinal);
private ServerCapabilities? _serverCapabilities;
@@ -49,12 +48,6 @@ internal McpClientImpl(ITransport transport, string endpointName, McpClientOptio
_options = options;
_logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance;
- // Only allocate the cancellation token provider if a task store is configured
- if (options.TaskStore is not null)
- {
- _taskCancellationTokenProvider = new();
- }
-
var notificationHandlers = new NotificationHandlers();
var requestHandlers = new RequestHandlers();
@@ -83,89 +76,22 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
var samplingHandler = handlers.SamplingHandler;
var rootsHandler = handlers.RootsHandler;
var elicitationHandler = handlers.ElicitationHandler;
- var taskStatusHandler = handlers.TaskStatusHandler;
- var taskStore = options.TaskStore;
if (notificationHandlersFromOptions is not null)
{
notificationHandlers.RegisterRange(notificationHandlersFromOptions);
}
- if (taskStatusHandler is not null)
- {
- notificationHandlers.Register(
- NotificationMethods.TaskStatusNotification,
- (notification, cancellationToken) =>
- {
- if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.McpTaskStatusNotificationParams) is { } notificationParams)
- {
- var task = new McpTask
- {
- TaskId = notificationParams.TaskId,
- Status = notificationParams.Status,
- StatusMessage = notificationParams.StatusMessage,
- CreatedAt = notificationParams.CreatedAt,
- LastUpdatedAt = notificationParams.LastUpdatedAt,
- TimeToLive = notificationParams.TimeToLive,
- PollInterval = notificationParams.PollInterval
- };
- return taskStatusHandler(task, cancellationToken);
- }
-
- return default;
- });
- }
-
if (samplingHandler is not null)
{
- // If task store is configured, wrap the handler to support task-augmented requests
- if (taskStore is not null)
- {
- requestHandlers.Set(
- RequestMethods.SamplingCreateMessage,
- async (request, jsonRpcRequest, cancellationToken) =>
- {
- // Check if this is a task-augmented request
- if (request?.Task is { } taskMetadata)
- {
- // Create task in store and return immediately
- return await ExecuteAsTaskAsync(
- taskStore,
- taskMetadata,
- jsonRpcRequest,
- async ct =>
- {
- var result = await samplingHandler(
- request,
- request.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
- ct).ConfigureAwait(false);
- return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CreateMessageResult);
- },
- options.SendTaskStatusNotifications,
- cancellationToken).ConfigureAwait(false);
- }
-
- // Normal synchronous execution - serialize result to JsonElement
- var samplingResult = await samplingHandler(
- request,
- request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
- cancellationToken).ConfigureAwait(false);
- return JsonSerializer.SerializeToElement(samplingResult, McpJsonUtilities.JsonContext.Default.CreateMessageResult);
- },
- McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
- McpJsonUtilities.JsonContext.Default.JsonElement); // Return JsonElement to support both CreateMessageResult and CreateTaskResult
- }
- else
- {
- requestHandlers.Set(
- RequestMethods.SamplingCreateMessage,
- (request, _, cancellationToken) => samplingHandler(
- request,
- request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
- cancellationToken),
- McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
- McpJsonUtilities.JsonContext.Default.CreateMessageResult);
- }
+ requestHandlers.Set(
+ RequestMethods.SamplingCreateMessage,
+ (request, _, cancellationToken) => samplingHandler(
+ request,
+ request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
+ cancellationToken),
+ McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
+ McpJsonUtilities.JsonContext.Default.CreateMessageResult);
_options.Capabilities ??= new();
_options.Capabilities.Sampling ??= new();
@@ -185,51 +111,15 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
if (elicitationHandler is not null)
{
- // If task store is configured, wrap the handler to support task-augmented requests
- if (taskStore is not null)
- {
- requestHandlers.Set(
- RequestMethods.ElicitationCreate,
- async (request, jsonRpcRequest, cancellationToken) =>
- {
- // Check if this is a task-augmented request
- if (request?.Task is { } taskMetadata)
- {
- // Create task in store and return immediately
- return await ExecuteAsTaskAsync(
- taskStore,
- taskMetadata,
- jsonRpcRequest,
- async ct =>
- {
- var result = await elicitationHandler(request, ct).ConfigureAwait(false);
- result = ElicitResult.WithDefaults(request, result);
- return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.ElicitResult);
- },
- options.SendTaskStatusNotifications,
- cancellationToken).ConfigureAwait(false);
- }
-
- // Normal synchronous execution - serialize result to JsonElement
- var elicitResult = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
- elicitResult = ElicitResult.WithDefaults(request, elicitResult);
- return JsonSerializer.SerializeToElement(elicitResult, McpJsonUtilities.JsonContext.Default.ElicitResult);
- },
- McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
- McpJsonUtilities.JsonContext.Default.JsonElement); // Return JsonElement to support both ElicitResult and CreateTaskResult
- }
- else
- {
- requestHandlers.Set(
- RequestMethods.ElicitationCreate,
- async (request, _, cancellationToken) =>
- {
- var result = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
- return ElicitResult.WithDefaults(request, result);
- },
- McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
- McpJsonUtilities.JsonContext.Default.ElicitResult);
- }
+ requestHandlers.Set(
+ RequestMethods.ElicitationCreate,
+ async (request, _, cancellationToken) =>
+ {
+ var result = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
+ return ElicitResult.WithDefaults(request, result);
+ },
+ McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
+ McpJsonUtilities.JsonContext.Default.ElicitResult);
_options.Capabilities ??= new();
_options.Capabilities.Elicitation ??= new();
@@ -240,276 +130,6 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not
_options.Capabilities.Elicitation.Form = new();
}
}
-
- // Register task handlers if a task store is configured
- if (taskStore is not null)
- {
- RegisterTaskHandlers(requestHandlers, taskStore);
- }
- }
-
- ///
- /// Executes an operation as a task, creating the task immediately and running the operation asynchronously.
- ///
- private async ValueTask ExecuteAsTaskAsync(
- IMcpTaskStore taskStore,
- McpTaskMetadata taskMetadata,
- JsonRpcRequest jsonRpcRequest,
- Func> operation,
- bool sendNotifications,
- CancellationToken cancellationToken)
- {
- // Create the task in the store
- var mcpTask = await taskStore.CreateTaskAsync(
- taskMetadata,
- jsonRpcRequest.Id,
- jsonRpcRequest,
- SessionId,
- cancellationToken).ConfigureAwait(false);
-
- // Register the task for TTL-based cancellation
- var taskCancellationToken = _taskCancellationTokenProvider!.RequestToken(mcpTask.TaskId, mcpTask.TimeToLive);
-
- // Execute the operation asynchronously in the background
- _ = Task.Run(async () =>
- {
- try
- {
- // Send notification if enabled
- if (sendNotifications)
- {
- var workingTask = await taskStore.GetTaskAsync(mcpTask.TaskId, SessionId, CancellationToken.None).ConfigureAwait(false);
- if (workingTask is not null)
- {
- _ = NotifyTaskStatusAsync(workingTask, CancellationToken.None);
- }
- }
-
- // Execute the operation with task-specific cancellation token
- var result = await operation(taskCancellationToken).ConfigureAwait(false);
-
- // Store the result
- var completedTask = await taskStore.StoreTaskResultAsync(
- mcpTask.TaskId,
- McpTaskStatus.Completed,
- result,
- SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send final notification if enabled
- if (sendNotifications)
- {
- _ = NotifyTaskStatusAsync(completedTask, CancellationToken.None);
- }
- }
- catch (OperationCanceledException) when (taskCancellationToken.IsCancellationRequested)
- {
- // Task was cancelled via TTL expiration or explicit cancellation.
- // For TTL expiration, the task is deleted so no status update needed.
- // For explicit cancellation, the cancel handler already updates the status.
- }
- catch (Exception ex)
- {
- // Store error result using a simple string message
- try
- {
- var errorElement = JsonSerializer.SerializeToElement(ex.Message, McpJsonUtilities.JsonContext.Default.String);
- await taskStore.StoreTaskResultAsync(
- mcpTask.TaskId,
- McpTaskStatus.Failed,
- errorElement,
- SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Update task with error message
- var failedTask = await taskStore.UpdateTaskStatusAsync(
- mcpTask.TaskId,
- McpTaskStatus.Failed,
- ex.Message,
- SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send failure notification if enabled
- if (sendNotifications)
- {
- _ = NotifyTaskStatusAsync(failedTask, CancellationToken.None);
- }
- }
- catch
- {
- // If we can't store the error result, there's not much we can do
- }
- }
- finally
- {
- // Clean up task cancellation tracking
- _taskCancellationTokenProvider!.Complete(mcpTask.TaskId);
- }
- }, CancellationToken.None);
-
- // Return the task result immediately
- var createTaskResult = new CreateTaskResult { Task = mcpTask };
- return JsonSerializer.SerializeToElement(createTaskResult, McpJsonUtilities.JsonContext.Default.CreateTaskResult);
- }
-
- ///
- /// Sends a task status notification to the connected server.
- ///
- private Task NotifyTaskStatusAsync(McpTask task, CancellationToken cancellationToken)
- {
- var notificationParams = new McpTaskStatusNotificationParams
- {
- TaskId = task.TaskId,
- Status = task.Status,
- StatusMessage = task.StatusMessage,
- CreatedAt = task.CreatedAt,
- LastUpdatedAt = task.LastUpdatedAt,
- TimeToLive = task.TimeToLive,
- PollInterval = task.PollInterval
- };
-
- return this.SendNotificationAsync(
- NotificationMethods.TaskStatusNotification,
- notificationParams,
- McpJsonUtilities.JsonContext.Default.McpTaskStatusNotificationParams,
- cancellationToken);
- }
-
- ///
- /// Registers handlers for task-related requests from the server.
- ///
- private void RegisterTaskHandlers(RequestHandlers requestHandlers, IMcpTaskStore taskStore)
- {
- // tasks/get handler - Retrieve task status
- requestHandlers.Set(
- RequestMethods.TasksGet,
- async (request, _, cancellationToken) =>
- {
- if (request?.TaskId is not { } taskId)
- {
- throw new McpProtocolException("Missing required parameter 'taskId'", McpErrorCode.InvalidParams);
- }
-
- var task = await taskStore.GetTaskAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- if (task is null)
- {
- throw new McpProtocolException($"Task not found: '{taskId}'", McpErrorCode.InvalidParams);
- }
-
- return new GetTaskResult
- {
- TaskId = task.TaskId,
- Status = task.Status,
- StatusMessage = task.StatusMessage,
- CreatedAt = task.CreatedAt,
- LastUpdatedAt = task.LastUpdatedAt,
- TimeToLive = task.TimeToLive,
- PollInterval = task.PollInterval
- };
- },
- McpJsonUtilities.JsonContext.Default.GetTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.GetTaskResult);
-
- // tasks/result handler - Retrieve task result (blocking until terminal status)
- requestHandlers.Set(
- RequestMethods.TasksResult,
- async (request, _, cancellationToken) =>
- {
- if (request?.TaskId is not { } taskId)
- {
- throw new McpProtocolException("Missing required parameter 'taskId'", McpErrorCode.InvalidParams);
- }
-
- // Poll until task reaches terminal status
- while (true)
- {
- McpTask? task = await taskStore.GetTaskAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- if (task is null)
- {
- throw new McpProtocolException($"Task not found: '{taskId}'", McpErrorCode.InvalidParams);
- }
-
- // If terminal, break and retrieve result
- if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled)
- {
- break;
- }
-
- // Poll according to task's pollInterval (default 1 second)
- var pollInterval = task.PollInterval ?? TimeSpan.FromSeconds(1);
- await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
- }
-
- // Retrieve the stored result
- return await taskStore.GetTaskResultAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- },
- McpJsonUtilities.JsonContext.Default.GetTaskPayloadRequestParams,
- McpJsonUtilities.JsonContext.Default.JsonElement);
-
- // tasks/list handler - List tasks with pagination
- requestHandlers.Set(
- RequestMethods.TasksList,
- async (request, _, cancellationToken) =>
- {
- var cursor = request?.Cursor;
- return await taskStore.ListTasksAsync(cursor, SessionId, cancellationToken).ConfigureAwait(false);
- },
- McpJsonUtilities.JsonContext.Default.ListTasksRequestParams,
- McpJsonUtilities.JsonContext.Default.ListTasksResult);
-
- // tasks/cancel handler - Cancel a task
- requestHandlers.Set(
- RequestMethods.TasksCancel,
- async (request, _, cancellationToken) =>
- {
- if (request?.TaskId is not { } taskId)
- {
- throw new McpProtocolException("Missing required parameter 'taskId'", McpErrorCode.InvalidParams);
- }
-
- // Signal cancellation if task is still running
- _taskCancellationTokenProvider!.Cancel(taskId);
-
- var task = await taskStore.CancelTaskAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- if (task is null)
- {
- throw new McpProtocolException($"Task not found: '{taskId}'", McpErrorCode.InvalidParams);
- }
-
- return new CancelMcpTaskResult
- {
- TaskId = task.TaskId,
- Status = task.Status,
- StatusMessage = task.StatusMessage,
- CreatedAt = task.CreatedAt,
- LastUpdatedAt = task.LastUpdatedAt,
- TimeToLive = task.TimeToLive,
- PollInterval = task.PollInterval
- };
- },
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskResult);
-
- // Advertise task capabilities
- _options.Capabilities ??= new();
- var tasksCapability = _options.Capabilities.Tasks ??= new McpTasksCapability();
- tasksCapability.List ??= new ListMcpTasksCapability();
- tasksCapability.Cancel ??= new CancelMcpTasksCapability();
- var requestsCapability = tasksCapability.Requests ??= new RequestMcpTasksCapability();
-
- // Only advertise sampling tasks if sampling handler is present
- if (_options.Handlers.SamplingHandler is not null)
- {
- var samplingCapability = requestsCapability.Sampling ??= new SamplingMcpTasksCapability();
- samplingCapability.CreateMessage ??= new CreateMessageMcpTasksCapability();
- }
-
- // Only advertise elicitation tasks if elicitation handler is present
- if (_options.Handlers.ElicitationHandler is not null)
- {
- var elicitationCapability = requestsCapability.Elicitation ??= new ElicitationMcpTasksCapability();
- elicitationCapability.Create ??= new CreateElicitationMcpTasksCapability();
- }
}
///
@@ -676,7 +296,6 @@ public override async ValueTask DisposeAsync()
_disposed = true;
- _taskCancellationTokenProvider?.Dispose();
await _sessionHandler.DisposeAsync().ConfigureAwait(false);
await _transport.DisposeAsync().ConfigureAwait(false);
diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs
index 6d91f5b03..8a2364ca4 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs
@@ -79,36 +79,4 @@ public McpClientHandlers Handlers
field = value;
}
}
-
- ///
- /// Gets or sets the task store for managing client-side tasks.
- ///
- ///
- ///
- /// When a task store is configured, the client will support task-augmented requests from the server.
- /// This allows the server to request sampling or elicitation as tasks, which the client executes
- /// asynchronously and allows the server to poll for status and results.
- ///
- ///
- /// If not set, task-augmented requests will not be supported, and the client will not advertise
- /// task capabilities to the server.
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public IMcpTaskStore? TaskStore { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the client should send task status notifications to the server.
- ///
- ///
- /// to send task status notifications; otherwise.
- /// The default is .
- ///
- ///
- /// When enabled and a is configured, the client will send optional
- /// notifications/tasks/status notifications to inform the server of task state changes.
- /// Servers MUST NOT rely on receiving these notifications and should continue polling via tasks/get.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public bool SendTaskStatusNotifications { get; set; } = true;
}
diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
index abb6d29df..b3d98dd0e 100644
--- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs
+++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
@@ -108,12 +108,10 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(ResourceUpdatedNotificationParams))]
[JsonSerializable(typeof(RootsListChangedNotificationParams))]
[JsonSerializable(typeof(ToolListChangedNotificationParams))]
- [JsonSerializable(typeof(McpTaskStatusNotificationParams))]
// MCP Request Params / Results
[JsonSerializable(typeof(CallToolRequestParams))]
[JsonSerializable(typeof(CallToolResult))]
- [JsonSerializable(typeof(CreateTaskResult))]
[JsonSerializable(typeof(CompleteRequestParams))]
[JsonSerializable(typeof(CompleteResult))]
[JsonSerializable(typeof(CreateMessageRequestParams))]
@@ -144,22 +142,6 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(SubscribeRequestParams))]
[JsonSerializable(typeof(UnsubscribeRequestParams))]
- // MCP Task Request Params / Results
- [JsonSerializable(typeof(McpTask))]
- [JsonSerializable(typeof(McpTaskStatus))]
- [JsonSerializable(typeof(McpTaskMetadata))]
- [JsonSerializable(typeof(GetTaskRequestParams))]
- [JsonSerializable(typeof(GetTaskResult))]
- [JsonSerializable(typeof(GetTaskPayloadRequestParams))]
- [JsonSerializable(typeof(ListTasksRequestParams))]
- [JsonSerializable(typeof(ListTasksResult))]
- [JsonSerializable(typeof(CancelMcpTaskRequestParams))]
- [JsonSerializable(typeof(CancelMcpTaskResult))]
- [JsonSerializable(typeof(McpTasksCapability))]
- [JsonSerializable(typeof(RequestMcpTasksCapability))]
- [JsonSerializable(typeof(ToolExecution))]
- [JsonSerializable(typeof(ToolTaskSupport))]
-
// MCP Content
[JsonSerializable(typeof(ContentBlock))]
[JsonSerializable(typeof(TextContentBlock))]
@@ -179,7 +161,6 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
// Other MCP Types
[JsonSerializable(typeof(IReadOnlyDictionary))]
[JsonSerializable(typeof(ProgressToken))]
- [JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(ProtectedResourceMetadata))]
[JsonSerializable(typeof(AuthorizationServerMetadata))]
diff --git a/src/ModelContextProtocol.Core/McpTaskCancellationTokenProvider.cs b/src/ModelContextProtocol.Core/McpTaskCancellationTokenProvider.cs
deleted file mode 100644
index 6ecfc4f4a..000000000
--- a/src/ModelContextProtocol.Core/McpTaskCancellationTokenProvider.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-using System.Collections.Concurrent;
-
-namespace ModelContextProtocol;
-
-///
-/// Provides cancellation tokens for running MCP tasks, enabling TTL-based
-/// automatic cancellation and explicit task cancellation.
-///
-///
-///
-/// This class provides lifecycle management for instances
-/// associated with running tasks. Each task gets its own CTS that can be:
-///
-///
-/// - Automatically cancelled when the task's TTL expires
-/// - Explicitly cancelled via the method
-/// - Cleaned up when the task completes via
-///
-///
-/// Both McpClient and McpServer use this class to manage task cancellation
-/// independently of request cancellation tokens.
-///
-///
-internal sealed class McpTaskCancellationTokenProvider : IDisposable
-{
- private readonly ConcurrentDictionary _runningTasks = new();
- private bool _disposed;
-
- ///
- /// Registers a new task and returns a cancellation token for use during execution.
- ///
- /// The unique identifier of the task.
- ///
- /// Optional TTL duration. If specified, the returned token will be automatically
- /// cancelled when the TTL expires.
- ///
- ///
- /// A that will be cancelled when the TTL expires,
- /// when is called, or when this provider is disposed.
- ///
- /// The provider has been disposed.
- /// A task with the same ID is already registered.
- public CancellationToken RequestToken(string taskId, TimeSpan? timeToLive)
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(nameof(McpTaskCancellationTokenProvider));
- }
-
- Throw.IfNullOrWhiteSpace(taskId);
- CancellationTokenSource cts = new();
-
- if (timeToLive is { } ttl)
- {
- cts.CancelAfter(ttl);
- }
-
- if (!_runningTasks.TryAdd(taskId, cts))
- {
- cts.Dispose();
- throw new InvalidOperationException($"Task '{taskId}' is already registered.");
- }
-
- return cts.Token;
- }
-
- ///
- /// Attempts to cancel a running task.
- ///
- /// The unique identifier of the task to cancel.
- ///
- /// This method signals cancellation but does not remove the task from tracking.
- /// The task executor should call when it observes
- /// the cancellation and finishes cleanup.
- ///
- public void Cancel(string taskId)
- {
- if (_runningTasks.TryGetValue(taskId, out var cts))
- {
- cts.Cancel();
- }
- }
-
- ///
- /// Marks a task as complete and releases its associated resources.
- ///
- /// The unique identifier of the task that has completed.
- ///
- /// This method should be called from a finally block in the task execution
- /// to ensure proper cleanup regardless of success, failure, or cancellation.
- ///
- public void Complete(string taskId)
- {
- if (_runningTasks.TryRemove(taskId, out var cts))
- {
- cts.Dispose();
- }
- }
-
- ///
- /// Cancels all running tasks and releases all resources.
- ///
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _disposed = true;
-
- foreach (var kvp in _runningTasks)
- {
- try
- {
- kvp.Value.Cancel();
- kvp.Value.Dispose();
- }
- catch
- {
- // Best effort cleanup
- }
- }
-
- _runningTasks.Clear();
- }
-}
diff --git a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj
index b6423b0c8..0cac92836 100644
--- a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj
+++ b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj
@@ -7,7 +7,7 @@
ModelContextProtocol.Core
Core .NET SDK for the Model Context Protocol (MCP)
README.md
-
+
$(NoWarn);MCPEXP001
diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
index 8267cd06f..d311c6b4f 100644
--- a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs
@@ -26,24 +26,4 @@ public sealed class CallToolRequestParams : RequestParams
///
[JsonPropertyName("arguments")]
public IDictionary? Arguments { get; set; }
-
- ///
- /// Gets or sets optional task metadata to augment this request with task execution.
- ///
- ///
- /// When present, indicates that the requestor wants this operation executed as a task.
- /// The receiver must support task augmentation for this specific request type.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public McpTaskMetadata? Task
- {
- get => TaskCore;
- set => TaskCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("task")]
- internal McpTaskMetadata? TaskCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
index 35dba5b6e..b2fdb3d05 100644
--- a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs
@@ -64,25 +64,4 @@ public sealed class CallToolResult : Result
///
[JsonPropertyName("isError")]
public bool? IsError { get; set; }
-
- ///
- /// Gets or sets the task data for the newly created task.
- ///
- ///
- /// This property is populated only for task-augmented tool calls. When present, the other properties
- /// (, , ) may not be populated.
- /// The actual tool result can be retrieved later via tasks/result.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public McpTask? Task
- {
- get => TaskCore;
- set => TaskCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("task")]
- internal McpTask? TaskCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/CancelMcpTaskRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CancelMcpTaskRequestParams.cs
deleted file mode 100644
index c4fb540b2..000000000
--- a/src/ModelContextProtocol.Core/Protocol/CancelMcpTaskRequestParams.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the parameters for a tasks/cancel request to explicitly cancel a task.
-///
-///
-///
-/// Receivers must reject cancellation requests for tasks already in a terminal status
-/// (, , or
-/// ) with error code -32602 (Invalid params).
-///
-///
-/// Upon receiving a valid cancellation request, receivers should attempt to stop the task
-/// execution and must transition the task to status
-/// before sending the response.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CancelMcpTaskRequestParams : RequestParams
-{
- ///
- /// Gets or sets the unique identifier of the task to cancel.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-}
-
-///
-/// Represents the result of a tasks/cancel request.
-///
-///
-/// The result contains the updated task state after cancellation. The task will be in
-/// status if the cancellation was successful.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CancelMcpTaskResult : Result
-{
- ///
- /// Gets or sets the task ID.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-
- ///
- /// Gets or sets the current status of the task (should be ).
- ///
- [JsonPropertyName("status")]
- public required McpTaskStatus Status { get; set; }
-
- ///
- /// Gets or sets an optional message describing the cancellation.
- ///
- [JsonPropertyName("statusMessage")]
- public string? StatusMessage { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task was created.
- ///
- [JsonPropertyName("createdAt")]
- public required DateTimeOffset CreatedAt { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task status was last updated.
- ///
- [JsonPropertyName("lastUpdatedAt")]
- public required DateTimeOffset LastUpdatedAt { get; set; }
-
- ///
- /// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
- ///
- [JsonPropertyName("ttl")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? TimeToLive { get; set; }
-
- ///
- /// Gets or sets the suggested time between status checks.
- ///
- [JsonPropertyName("pollInterval")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? PollInterval { get; set; }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
index 77b2bef9f..f41f50fd8 100644
--- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
@@ -68,32 +68,6 @@ public sealed class ClientCapabilities
[JsonPropertyName("elicitation")]
public ElicitationCapability? Elicitation { get; set; }
- ///
- /// Gets or sets the client's tasks capability for supporting task-augmented requests.
- ///
- ///
- ///
- /// The tasks capability enables servers to augment their requests with tasks for long-running
- /// operations. When present, servers can request that certain operations (like sampling or
- /// elicitation) execute asynchronously, with the ability to poll for status and retrieve results later.
- ///
- ///
- /// See for details on configuring which operations support tasks.
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public McpTasksCapability? Tasks
- {
- get => TasksCore;
- set => TasksCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("tasks")]
- internal McpTasksCapability? TasksCore { get; set; }
-
///
/// Gets or sets optional MCP extensions that the client supports.
///
diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs
index ef5e57d2c..bb27d70fd 100644
--- a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs
@@ -153,24 +153,4 @@ public sealed class CreateMessageRequestParams : RequestParams
///
[JsonPropertyName("toolChoice")]
public ToolChoice? ToolChoice { get; set; }
-
- ///
- /// Gets or sets optional task metadata to augment this request with task execution.
- ///
- ///
- /// When present, indicates that the requestor wants this operation executed as a task.
- /// The receiver must support task augmentation for this specific request type.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public McpTaskMetadata? Task
- {
- get => TaskCore;
- set => TaskCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("task")]
- internal McpTaskMetadata? TaskCore { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs
deleted file mode 100644
index 166d05e49..000000000
--- a/src/ModelContextProtocol.Core/Protocol/CreateTaskResult.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the response to a task-augmented request.
-///
-///
-///
-/// When a client sends a request with a task parameter, the server immediately returns
-/// a containing the created task information instead of the
-/// normal result type. The actual result can be retrieved later via tasks/result.
-///
-///
-/// This type is returned for any task-augmented request including tools/call,
-/// sampling/createMessage, and elicitation/create.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CreateTaskResult : Result
-{
- ///
- /// Gets or sets the task data for the newly created task.
- ///
- [JsonPropertyName("task")]
- public McpTask Task { get; set; } = null!;
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
index 39a5bd358..9dc1ac903 100644
--- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs
@@ -92,26 +92,6 @@ public string Mode
[JsonPropertyName("requestedSchema")]
public RequestSchema? RequestedSchema { get; set; }
- ///
- /// Gets or sets optional task metadata to augment this request with task execution.
- ///
- ///
- /// When present, indicates that the requestor wants this operation executed as a task.
- /// The receiver must support task augmentation for this specific request type.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public McpTaskMetadata? Task
- {
- get => TaskCore;
- set => TaskCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("task")]
- internal McpTaskMetadata? TaskCore { get; set; }
-
/// Represents a request schema used in a form mode elicitation request.
public sealed class RequestSchema
{
diff --git a/src/ModelContextProtocol.Core/Protocol/GetTaskPayloadRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/GetTaskPayloadRequestParams.cs
deleted file mode 100644
index d64a8b1f9..000000000
--- a/src/ModelContextProtocol.Core/Protocol/GetTaskPayloadRequestParams.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the parameters for a tasks/result request to retrieve the result of a completed task.
-///
-///
-///
-/// This request blocks until the task reaches a terminal status (,
-/// , or ).
-///
-///
-/// The result structure matches the original request type (e.g., for tools/call).
-/// This is distinct from the initial response, which contains only task data.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class GetTaskPayloadRequestParams : RequestParams
-{
- ///
- /// Gets or sets the unique identifier of the task whose result to retrieve.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/GetTaskRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/GetTaskRequestParams.cs
deleted file mode 100644
index a8aaaea93..000000000
--- a/src/ModelContextProtocol.Core/Protocol/GetTaskRequestParams.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the parameters for a tasks/get request to retrieve task status.
-///
-///
-/// Requestors poll for task completion by sending tasks/get requests. They should
-/// respect the provided in responses when determining
-/// polling frequency.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class GetTaskRequestParams : RequestParams
-{
- ///
- /// Gets or sets the unique identifier of the task to retrieve.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-}
-
-///
-/// Represents the result of a tasks/get request.
-///
-///
-/// The result contains the current state of the task, including its status, timestamps,
-/// and any status message.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class GetTaskResult : Result
-{
- ///
- /// Gets or sets the task ID.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-
- ///
- /// Gets or sets the current status of the task.
- ///
- [JsonPropertyName("status")]
- public required McpTaskStatus Status { get; set; }
-
- ///
- /// Gets or sets an optional human-readable message describing the current state.
- ///
- [JsonPropertyName("statusMessage")]
- public string? StatusMessage { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task was created.
- ///
- [JsonPropertyName("createdAt")]
- public required DateTimeOffset CreatedAt { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task status was last updated.
- ///
- [JsonPropertyName("lastUpdatedAt")]
- public required DateTimeOffset LastUpdatedAt { get; set; }
-
- ///
- /// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
- ///
- [JsonPropertyName("ttl")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? TimeToLive { get; set; }
-
- ///
- /// Gets or sets the suggested time between status checks.
- ///
- [JsonPropertyName("pollInterval")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? PollInterval { get; set; }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/ListTasksRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ListTasksRequestParams.cs
deleted file mode 100644
index 3036d977b..000000000
--- a/src/ModelContextProtocol.Core/Protocol/ListTasksRequestParams.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the parameters for a tasks/list request to retrieve a list of tasks.
-///
-///
-/// This operation supports cursor-based pagination. Receivers should use cursor-based
-/// pagination to limit the number of tasks returned in a single response.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class ListTasksRequestParams : PaginatedRequestParams
-{
- // Inherits Cursor property from PaginatedRequestParams
-}
-
-///
-/// Represents the result of a tasks/list request.
-///
-///
-/// The result contains an array of task objects and an optional cursor for pagination.
-/// If is present, more tasks are available.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class ListTasksResult : PaginatedResult
-{
- ///
- /// Gets or sets the list of tasks.
- ///
- [JsonPropertyName("tasks")]
- public required IList Tasks { get; set; }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/McpTask.cs b/src/ModelContextProtocol.Core/Protocol/McpTask.cs
deleted file mode 100644
index 2056c5890..000000000
--- a/src/ModelContextProtocol.Core/Protocol/McpTask.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents an MCP task, which is a durable state machine carrying information
-/// about the underlying execution state of a request.
-///
-///
-///
-/// Tasks are useful for representing expensive computations and batch processing requests.
-/// Each task is uniquely identifiable by a receiver-generated task ID.
-///
-///
-/// Tasks follow a defined lifecycle through the property. They begin
-/// in the status and may transition through various states
-/// before reaching a terminal status (, ,
-/// or ).
-///
-///
-/// See the tasks specification for details.
-///
-///
-[DebuggerDisplay("{DebuggerDisplay,nq}")]
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class McpTask
-{
- ///
- /// Gets or sets the unique identifier for the task.
- ///
- ///
- /// Task IDs are generated by the receiver when creating a task and must be unique
- /// among all tasks controlled by that receiver.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-
- ///
- /// Gets or sets the current state of the task execution.
- ///
- [JsonPropertyName("status")]
- public required McpTaskStatus Status { get; set; }
-
- ///
- /// Gets or sets an optional human-readable message describing the current state.
- ///
- ///
- /// This message can be present for any status, including error details for failed tasks.
- ///
- [JsonPropertyName("statusMessage")]
- public string? StatusMessage { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task was created.
- ///
- ///
- /// Receivers must include this timestamp in all task responses to indicate when
- /// the task was created.
- ///
- [JsonPropertyName("createdAt")]
- public required DateTimeOffset CreatedAt { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task status was last updated.
- ///
- ///
- /// Receivers must include this timestamp in all task responses to indicate when
- /// the task was last updated.
- ///
- [JsonPropertyName("lastUpdatedAt")]
- public required DateTimeOffset LastUpdatedAt { get; set; }
-
- ///
- /// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
- ///
- ///
- ///
- /// A null value indicates unlimited lifetime. After a task's TTL lifetime has elapsed,
- /// receivers may delete the task and its results, regardless of the task status.
- ///
- ///
- /// Receivers may override the requested TTL duration and must include the actual TTL
- /// duration (or null for unlimited) in task responses.
- ///
- ///
- [JsonPropertyName("ttl")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? TimeToLive { get; set; }
-
- ///
- /// Gets or sets the suggested time between status checks.
- ///
- ///
- /// Requestors should respect this value when provided to avoid excessive polling.
- /// This value is optional and may not be present in all task responses.
- ///
- [JsonPropertyName("pollInterval")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? PollInterval { get; set; }
-
- private string DebuggerDisplay => $"Task {TaskId}: {Status}" + (StatusMessage != null ? $" - {StatusMessage}" : "");
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/McpTaskMetadata.cs b/src/ModelContextProtocol.Core/Protocol/McpTaskMetadata.cs
deleted file mode 100644
index 72dea54f3..000000000
--- a/src/ModelContextProtocol.Core/Protocol/McpTaskMetadata.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents metadata for augmenting a request with task execution.
-///
-///
-///
-/// When included in a request's params, this metadata signals that the requestor
-/// wants the receiver to execute the request as a task rather than synchronously.
-/// The receiver will return a containing task data
-/// instead of the actual operation result.
-///
-///
-/// Requestors can specify a desired TTL (time-to-live) duration for the task,
-/// though receivers may override this value based on their resource management policies.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class McpTaskMetadata
-{
- ///
- /// Gets or sets the requested time to live (retention duration) to retain the task from creation.
- ///
- ///
- ///
- /// This is a hint to the receiver about how long the requestor expects to need access
- /// to the task data. Receivers may override this value based on their resource constraints
- /// and policies.
- ///
- ///
- /// A null value indicates no specific retention requirement. The actual TTL used by the
- /// receiver will be returned in the property.
- ///
- ///
- [JsonPropertyName("ttl")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? TimeToLive { get; set; }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/McpTaskStatus.cs b/src/ModelContextProtocol.Core/Protocol/McpTaskStatus.cs
deleted file mode 100644
index 9cf8a2f66..000000000
--- a/src/ModelContextProtocol.Core/Protocol/McpTaskStatus.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the status of an MCP task.
-///
-///
-///
-/// Tasks progress through a defined lifecycle:
-///
-/// - : The request is currently being processed.
-/// - : The receiver needs input from the requestor.
-/// The requestor should call tasks/result to receive input requests.
-/// - : The request completed successfully and results are available.
-/// - : The request did not complete successfully.
-/// - : The request was cancelled before completion.
-///
-///
-///
-/// Terminal states are , , and .
-/// Once a task reaches a terminal state, it cannot transition to any other status.
-///
-///
-[JsonConverter(typeof(JsonStringEnumConverter))]
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public enum McpTaskStatus
-{
- ///
- /// The request is currently being processed.
- ///
- ///
- /// Tasks begin in this status when created. From , tasks may transition
- /// to , , , or .
- ///
- [JsonStringEnumMemberName("working")]
- Working,
-
- ///
- /// The receiver needs input from the requestor.
- ///
- ///
- /// The requestor should call tasks/result to receive input requests, even though the task
- /// has not reached a terminal state. From , tasks may transition
- /// to , , , or .
- ///
- [JsonStringEnumMemberName("input_required")]
- InputRequired,
-
- ///
- /// The request completed successfully and results are available.
- ///
- ///
- /// This is a terminal status. Tasks in this status cannot transition to any other status.
- ///
- [JsonStringEnumMemberName("completed")]
- Completed,
-
- ///
- /// The associated request did not complete successfully.
- ///
- ///
- /// This is a terminal status. For tool calls specifically, this includes cases where
- /// the tool call result has isError set to true. Tasks in this status cannot transition
- /// to any other status.
- ///
- [JsonStringEnumMemberName("failed")]
- Failed,
-
- ///
- /// The request was cancelled before completion.
- ///
- ///
- /// This is a terminal status. Tasks in this status cannot transition to any other status.
- ///
- [JsonStringEnumMemberName("cancelled")]
- Cancelled
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/McpTaskStatusNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/McpTaskStatusNotificationParams.cs
deleted file mode 100644
index a9b536102..000000000
--- a/src/ModelContextProtocol.Core/Protocol/McpTaskStatusNotificationParams.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the parameters for a notifications/tasks/status notification.
-///
-///
-///
-/// When a task status changes, receivers may send this notification to inform the
-/// requestor of the change. This notification includes the full task state.
-///
-///
-/// Requestors must not rely on receiving this notification, as it is optional. Receivers
-/// are not required to send status notifications and may choose to only send them for
-/// certain status transitions. Requestors should continue to poll via tasks/get to ensure
-/// they receive status updates.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class McpTaskStatusNotificationParams : NotificationParams
-{
- ///
- /// Gets or sets the task ID.
- ///
- [JsonPropertyName("taskId")]
- public required string TaskId { get; set; }
-
- ///
- /// Gets or sets the current status of the task.
- ///
- [JsonPropertyName("status")]
- public required McpTaskStatus Status { get; set; }
-
- ///
- /// Gets or sets an optional human-readable message describing the current state.
- ///
- [JsonPropertyName("statusMessage")]
- public string? StatusMessage { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task was created.
- ///
- [JsonPropertyName("createdAt")]
- public required DateTimeOffset CreatedAt { get; set; }
-
- ///
- /// Gets or sets the ISO 8601 timestamp when the task status was last updated.
- ///
- [JsonPropertyName("lastUpdatedAt")]
- public required DateTimeOffset LastUpdatedAt { get; set; }
-
- ///
- /// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
- ///
- [JsonPropertyName("ttl")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? TimeToLive { get; set; }
-
- ///
- /// Gets or sets the suggested time between status checks.
- ///
- [JsonPropertyName("pollInterval")]
- [JsonConverter(typeof(TimeSpanMillisecondsConverter))]
- public TimeSpan? PollInterval { get; set; }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/McpTasksCapability.cs b/src/ModelContextProtocol.Core/Protocol/McpTasksCapability.cs
deleted file mode 100644
index 1b3ccd9dd..000000000
--- a/src/ModelContextProtocol.Core/Protocol/McpTasksCapability.cs
+++ /dev/null
@@ -1,160 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents the tasks capability configuration for servers and clients.
-///
-///
-///
-/// The tasks capability enables requestors (clients or servers) to augment their requests with
-/// tasks for long-running operations. Tasks are durable state machines that carry information
-/// about the underlying execution state of requests.
-///
-///
-/// During initialization, both parties exchange their tasks capabilities to establish which
-/// operations support task-based execution. Requestors should only augment requests with a
-/// task if the corresponding capability has been declared by the receiver.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class McpTasksCapability
-{
- ///
- /// Gets or sets whether this party supports the tasks/list operation.
- ///
- ///
- /// When present, indicates support for listing all tasks.
- ///
- [JsonPropertyName("list")]
- public ListMcpTasksCapability? List { get; set; }
-
- ///
- /// Gets or sets whether this party supports the tasks/cancel operation.
- ///
- ///
- /// When present, indicates support for cancelling tasks.
- ///
- [JsonPropertyName("cancel")]
- public CancelMcpTasksCapability? Cancel { get; set; }
-
- ///
- /// Gets or sets which request types support task augmentation.
- ///
- ///
- ///
- /// The set of capabilities in this property is exhaustive. If a request type is not present,
- /// it does not support task augmentation.
- ///
- ///
- /// For servers, this typically includes tools/call. For clients, this typically includes
- /// sampling/createMessage and elicitation/create.
- ///
- ///
- [JsonPropertyName("requests")]
- public RequestMcpTasksCapability? Requests { get; set; }
-}
-
-///
-/// Represents task support for tool-specific requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class RequestMcpTasksCapability
-{
- ///
- /// Gets or sets task support for tool-related requests.
- ///
- [JsonPropertyName("tools")]
- public ToolsMcpTasksCapability? Tools { get; set; }
-
- ///
- /// Gets or sets task support for sampling-related requests.
- ///
- [JsonPropertyName("sampling")]
- public SamplingMcpTasksCapability? Sampling { get; set; }
-
- ///
- /// Gets or sets task support for elicitation-related requests.
- ///
- [JsonPropertyName("elicitation")]
- public ElicitationMcpTasksCapability? Elicitation { get; set; }
-}
-
-///
-/// Represents task support for tool-related requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class ToolsMcpTasksCapability
-{
- ///
- /// Gets or sets whether tools/call requests support task augmentation.
- ///
- ///
- /// When present, indicates that the server supports task-augmented tools/call requests.
- ///
- [JsonPropertyName("call")]
- public CallToolMcpTasksCapability? Call { get; set; }
-}
-
-///
-/// Represents task support for sampling-related requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class SamplingMcpTasksCapability
-{
- ///
- /// Gets or sets whether sampling/createMessage requests support task augmentation.
- ///
- ///
- /// When present, indicates that the client supports task-augmented sampling/createMessage requests.
- ///
- [JsonPropertyName("createMessage")]
- public CreateMessageMcpTasksCapability? CreateMessage { get; set; }
-}
-
-///
-/// Represents task support for elicitation-related requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class ElicitationMcpTasksCapability
-{
- ///
- /// Gets or sets whether elicitation/create requests support task augmentation.
- ///
- ///
- /// When present, indicates that the client supports task-augmented elicitation/create requests.
- ///
- [JsonPropertyName("create")]
- public CreateElicitationMcpTasksCapability? Create { get; set; }
-}
-
-///
-/// Represents the capability for listing tasks.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class ListMcpTasksCapability;
-
-///
-/// Represents the capability for cancelling tasks.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CancelMcpTasksCapability;
-
-///
-/// Represents the capability for task-augmented tools/call requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CallToolMcpTasksCapability;
-
-///
-/// Represents the capability for task-augmented sampling/createMessage requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CreateMessageMcpTasksCapability;
-
-///
-/// Represents the capability for task-augmented elicitation/create requests.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class CreateElicitationMcpTasksCapability;
diff --git a/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs b/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs
index 949361650..46826f6d3 100644
--- a/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs
+++ b/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs
@@ -141,41 +141,4 @@ public static class NotificationMethods
///
///
public const string CancelledNotification = "notifications/cancelled";
-
- ///
- /// The name of the notification sent when a task status changes.
- ///
- ///
- ///
- /// When a task status changes, receivers may send this notification to inform the requestor
- /// of the change. This notification includes the full task state.
- ///
- ///
- /// Requestors must not rely on receiving this notification, as it is optional. Receivers
- /// are not required to send status notifications and may choose to only send them for
- /// certain status transitions. Requestors should continue to poll via tasks/get to ensure
- /// they receive status updates.
- ///
- ///
- public const string TaskStatusNotification = "notifications/tasks/status";
-
- ///
- /// The metadata key used to associate requests, responses, and notifications with a task.
- ///
- ///
- ///
- /// This constant defines the key "io.modelcontextprotocol/related-task" used in the
- /// _meta field to associate messages with their originating task across the entire
- /// request lifecycle.
- ///
- ///
- /// For example, an elicitation that a task-augmented tool call depends on must share the
- /// same related task ID with that tool call's task.
- ///
- ///
- /// For tasks/get, tasks/list, and tasks/cancel operations, this
- /// metadata should not be included as the taskId is already present in the message structure.
- ///
- ///
- public const string RelatedTaskMetaKey = "io.modelcontextprotocol/related-task";
}
diff --git a/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs b/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs
index e0118fa57..72cfb25a7 100644
--- a/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs
+++ b/src/ModelContextProtocol.Core/Protocol/RequestMethods.cs
@@ -121,32 +121,4 @@ public static class RequestMethods
/// and information, establishing the protocol version and available features for the session.
///
public const string Initialize = "initialize";
-
- ///
- /// The name of the request method to retrieve task status.
- ///
- ///
- /// Requestors poll for task completion by sending tasks/get requests. They should respect
- /// the pollInterval provided in responses when determining polling frequency.
- ///
- public const string TasksGet = "tasks/get";
-
- ///
- /// The name of the request method to retrieve the result of a completed task.
- ///
- ///
- /// This request blocks until the task reaches a terminal status (completed, failed, or cancelled).
- /// The result structure matches the original request type (e.g., CallToolResult for tools/call).
- ///
- public const string TasksResult = "tasks/result";
-
- ///
- /// The name of the request method to retrieve a list of tasks with pagination support.
- ///
- public const string TasksList = "tasks/list";
-
- ///
- /// The name of the request method to explicitly cancel a task.
- ///
- public const string TasksCancel = "tasks/cancel";
}
\ No newline at end of file
diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
index d4e23a66f..92ffff424 100644
--- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
+++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs
@@ -67,32 +67,6 @@ public sealed class ServerCapabilities
[JsonPropertyName("completions")]
public CompletionsCapability? Completions { get; set; }
- ///
- /// Gets or sets a server's tasks capability for supporting task-augmented requests.
- ///
- ///
- ///
- /// The tasks capability enables clients to augment their requests with tasks for long-running
- /// operations. When present, clients can request that certain operations (like tool calls)
- /// execute asynchronously, with the ability to poll for status and retrieve results later.
- ///
- ///
- /// See for details on configuring which operations support tasks.
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public McpTasksCapability? Tasks
- {
- get => TasksCore;
- set => TasksCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("tasks")]
- internal McpTasksCapability? TasksCore { get; set; }
-
///
/// Gets or sets optional MCP extensions that the server supports.
///
diff --git a/src/ModelContextProtocol.Core/Protocol/TimeSpanMillisecondsConverter.cs b/src/ModelContextProtocol.Core/Protocol/TimeSpanMillisecondsConverter.cs
deleted file mode 100644
index e789db186..000000000
--- a/src/ModelContextProtocol.Core/Protocol/TimeSpanMillisecondsConverter.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.ComponentModel;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Provides a JSON converter for that serializes as integer milliseconds.
-///
-///
-/// This converter serializes TimeSpan values as the total number of milliseconds (as an integer),
-/// and deserializes integer millisecond values back to TimeSpan. System.Text.Json automatically
-/// handles nullable TimeSpan properties using this converter.
-///
-[EditorBrowsable(EditorBrowsableState.Never)]
-public sealed class TimeSpanMillisecondsConverter : JsonConverter
-{
- ///
- public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- if (reader.TokenType is JsonTokenType.Number)
- {
- if (reader.TryGetInt64(out long milliseconds))
- {
- return TimeSpan.FromMilliseconds(milliseconds);
- }
-
- // For non-integer values, convert from fractional milliseconds
- double fractionalMilliseconds = reader.GetDouble();
- return TimeSpan.FromTicks((long)(fractionalMilliseconds * TimeSpan.TicksPerMillisecond));
- }
-
- throw new JsonException($"Unable to convert {reader.TokenType} to TimeSpan.");
- }
-
- ///
- public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
- {
- writer.WriteNumberValue((long)value.TotalMilliseconds);
- }
-}
diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs
index 8abbfd88c..9f61756f8 100644
--- a/src/ModelContextProtocol.Core/Protocol/Tool.cs
+++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs
@@ -119,26 +119,6 @@ public JsonElement? OutputSchema
[JsonPropertyName("annotations")]
public ToolAnnotations? Annotations { get; set; }
- ///
- /// Gets or sets execution-related metadata for this tool.
- ///
- ///
- /// This property provides hints about how the tool should be executed, particularly
- /// regarding task augmentation support. See for details.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- [JsonIgnore]
- public ToolExecution? Execution
- {
- get => ExecutionCore;
- set => ExecutionCore = value;
- }
-
- // See ExperimentalInternalPropertyTests.cs before modifying this property.
- [JsonInclude]
- [JsonPropertyName("execution")]
- internal ToolExecution? ExecutionCore { get; set; }
-
///
/// Gets or sets an optional list of icons for this tool.
///
diff --git a/src/ModelContextProtocol.Core/Protocol/ToolExecution.cs b/src/ModelContextProtocol.Core/Protocol/ToolExecution.cs
deleted file mode 100644
index 174298471..000000000
--- a/src/ModelContextProtocol.Core/Protocol/ToolExecution.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace ModelContextProtocol.Protocol;
-
-///
-/// Represents execution-related metadata for a tool.
-///
-///
-/// This type provides hints about how a tool should be executed, particularly
-/// regarding task augmentation support.
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class ToolExecution
-{
- ///
- /// Gets or sets the level of task augmentation support for this tool.
- ///
- ///
- ///
- /// This property declares whether a tool supports task-augmented execution:
- ///
- /// - : Clients must not attempt to invoke
- /// the tool as a task. This is the default behavior.
- /// - : Clients may invoke the tool as a task
- /// or as a normal request.
- /// - : Clients must invoke the tool as a task.
- ///
- ///
- ///
- ///
- /// This is a fine-grained layer in addition to server capabilities. Even if a server's capabilities
- /// include tasks.requests.tools.call, this property controls whether each specific tool supports tasks.
- ///
- ///
- [JsonPropertyName("taskSupport")]
- public ToolTaskSupport? TaskSupport { get; set; }
-}
-
-///
-/// Represents the level of task augmentation support for a tool.
-///
-///
-///
-/// This enum defines how a tool interacts with the task augmentation system:
-///
-/// - : Task augmentation is not allowed (default)
-/// - : Task augmentation is supported but not required
-/// - : Task augmentation is mandatory
-///
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-[JsonConverter(typeof(JsonStringEnumConverter))]
-public enum ToolTaskSupport
-{
- ///
- /// Clients must not attempt to invoke the tool as a task.
- ///
- ///
- /// This is the default behavior. Servers should return a -32601 (Method not found) error
- /// if a client attempts to invoke the tool as a task when this is set.
- ///
- [JsonStringEnumMemberName("forbidden")]
- Forbidden,
-
- ///
- /// Clients may invoke the tool as a task or as a normal request.
- ///
- ///
- /// When this is set, clients can choose whether to use task augmentation based on their needs.
- ///
- [JsonStringEnumMemberName("optional")]
- Optional,
-
- ///
- /// Clients must invoke the tool as a task.
- ///
- ///
- /// Servers must return a -32601 (Method not found) error if a client does not attempt
- /// to invoke the tool as a task when this is set.
- ///
- [JsonStringEnumMemberName("required")]
- Required
-}
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
index 961344c2c..715edb97f 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
@@ -154,23 +154,6 @@ options.OpenWorld is not null ||
tool.Meta = function.UnderlyingMethod is not null ?
CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta) :
options.Meta;
-
- // Apply user-specified Execution settings if provided
- if (options.Execution is not null)
- {
- tool.Execution = options.Execution;
- }
- }
-
- // Auto-detect async methods and mark with taskSupport = "optional" unless explicitly configured.
- // This enables implicit task support for async tools: clients can choose to invoke them
- // synchronously (wait for completion) or as a task (receive taskId, poll for result).
- if (function.UnderlyingMethod is not null &&
- IsAsyncMethod(function.UnderlyingMethod) &&
- tool.Execution?.TaskSupport is null)
- {
- tool.Execution ??= new ToolExecution();
- tool.Execution.TaskSupport = ToolTaskSupport.Optional;
}
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? []);
@@ -218,12 +201,6 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
serializerOptions: newOptions.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
inferenceOptions: newOptions.SchemaCreateOptions);
}
-
- if (toolAttr._taskSupport is { } taskSupport)
- {
- newOptions.Execution ??= new ToolExecution();
- newOptions.Execution.TaskSupport ??= taskSupport;
- }
}
if (method.GetCustomAttribute() is { } descAttr)
@@ -350,27 +327,27 @@ internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy =
// Case the name based on the provided naming policy.
return (policy ?? JsonNamingPolicy.SnakeCaseLower).ConvertName(name) ?? name;
- }
- private static bool IsAsyncMethod(MethodInfo method)
- {
- Type t = method.ReturnType;
-
- if (t == typeof(Task) || t == typeof(ValueTask))
+ static bool IsAsyncMethod(MethodInfo method)
{
- return true;
- }
+ Type t = method.ReturnType;
- if (t.IsGenericType)
- {
- t = t.GetGenericTypeDefinition();
- if (t == typeof(Task<>) || t == typeof(ValueTask<>) || t == typeof(IAsyncEnumerable<>))
+ if (t == typeof(Task) || t == typeof(ValueTask))
{
return true;
}
- }
- return false;
+ if (t.IsGenericType)
+ {
+ t = t.GetGenericTypeDefinition();
+ if (t == typeof(Task<>) || t == typeof(ValueTask<>) || t == typeof(IAsyncEnumerable<>))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
/// Creates metadata from attributes on the specified method and its declaring class, with the MethodInfo as the first item.
diff --git a/src/ModelContextProtocol.Core/Server/IMcpTaskStore.cs b/src/ModelContextProtocol.Core/Server/IMcpTaskStore.cs
deleted file mode 100644
index d322d21ef..000000000
--- a/src/ModelContextProtocol.Core/Server/IMcpTaskStore.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-
-namespace ModelContextProtocol;
-
-///
-/// Provides an interface for pluggable task storage implementations in MCP servers.
-///
-///
-///
-/// The task store is responsible for managing the lifecycle of tasks, including creation,
-/// status updates, result storage, and retrieval. Implementations must be thread-safe and
-/// may support session-based isolation for multi-session scenarios.
-///
-///
-/// TTL (Time To Live) Management: Implementations may override the requested TTL value in
-/// to enforce resource limits. The actual TTL
-/// used is returned in the property. A null TTL indicates
-/// unlimited lifetime. Tasks may be deleted after their TTL expires, regardless of status.
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public interface IMcpTaskStore
-{
- ///
- /// Creates a new task for tracking an asynchronous operation.
- ///
- /// Metadata for the task, including requested TTL.
- /// The JSON-RPC request ID that initiated this task.
- /// The original JSON-RPC request that triggered task creation.
- /// Optional session identifier for multi-session isolation.
- /// Cancellation token for the operation.
- ///
- /// A new with a unique task ID, initial status of ,
- /// and the actual TTL that will be used (which may differ from the requested TTL).
- ///
- ///
- /// Implementations must generate a unique task ID and set the
- /// and timestamps. The implementation may override the
- /// requested TTL to enforce storage limits.
- ///
- Task CreateTaskAsync(
- McpTaskMetadata taskParams,
- RequestId requestId,
- JsonRpcRequest request,
- string? sessionId = null,
- CancellationToken cancellationToken = default);
-
- ///
- /// Retrieves a task by its unique identifier.
- ///
- /// The unique identifier of the task to retrieve.
- /// Optional session identifier for access control.
- /// Cancellation token for the operation.
- ///
- /// The if found and accessible, otherwise .
- ///
- ///
- /// Returns null if the task does not exist or if session-based access control denies access.
- ///
- Task GetTaskAsync(string taskId, string? sessionId = null, CancellationToken cancellationToken = default);
-
- ///
- /// Stores the final result of a task that has reached a terminal status.
- ///
- /// The unique identifier of the task.
- /// The terminal status: or .
- /// The operation result to store as a JSON element.
- /// Optional session identifier for access control.
- /// Cancellation token for the operation.
- /// The updated with the new status and result stored.
- ///
- ///
- /// The must be either or
- /// . This method updates the task status and stores
- /// the result for later retrieval via .
- ///
- ///
- /// Implementations should throw if called on a task
- /// that is already in a terminal state, to prevent result overwrites.
- ///
- ///
- Task StoreTaskResultAsync(
- string taskId,
- McpTaskStatus status,
- JsonElement result,
- string? sessionId = null,
- CancellationToken cancellationToken = default);
-
- ///
- /// Retrieves the stored result of a completed or failed task.
- ///
- /// The unique identifier of the task.
- /// Optional session identifier for access control.
- /// Cancellation token for the operation.
- /// The stored operation result as a JSON element.
- ///
- /// This method should only be called on tasks in terminal states (
- /// or ). The result contains the JSON representation of the
- /// original operation result (e.g., for tools/call).
- ///
- Task GetTaskResultAsync(string taskId, string? sessionId = null, CancellationToken cancellationToken = default);
-
- ///
- /// Updates the status and optional status message of a task.
- ///
- /// The unique identifier of the task.
- /// The new status to set.
- /// Optional diagnostic message describing the status change.
- /// Optional session identifier for access control.
- /// Cancellation token for the operation.
- /// The updated with the new status applied.
- ///
- /// This method updates the task's , ,
- /// and properties. Common uses include transitioning to
- /// , , or updating
- /// progress messages while in status.
- ///
- Task UpdateTaskStatusAsync(
- string taskId,
- McpTaskStatus status,
- string? statusMessage,
- string? sessionId = null,
- CancellationToken cancellationToken = default);
-
- ///
- /// Lists tasks with pagination support.
- ///
- /// Optional cursor for pagination, from a previous call's nextCursor value.
- /// Optional session identifier for filtering tasks by session.
- /// Cancellation token for the operation.
- /// A containing the tasks and an optional cursor for the next page.
- ///
- /// When is provided, implementations should filter to only return
- /// tasks associated with that session. The cursor format is implementation-specific.
- ///
- Task ListTasksAsync(
- string? cursor = null,
- string? sessionId = null,
- CancellationToken cancellationToken = default);
-
- ///
- /// Attempts to cancel a task, transitioning it to status.
- ///
- /// The unique identifier of the task to cancel.
- /// Optional session identifier for access control.
- /// Cancellation token for the operation.
- ///
- /// The updated . If the task is already in a terminal state
- /// (, , or
- /// ), the task is returned unchanged.
- ///
- ///
- ///
- /// This method must be idempotent. If called on a task that is already in a terminal state,
- /// it returns the current task without error. This behavior differs from the MCP specification
- /// but ensures idempotency and avoids race conditions between cancellation and task completion.
- ///
- ///
- /// For tasks not in a terminal state, the implementation should attempt to stop the underlying
- /// operation and transition the task to status before returning.
- ///
- ///
- Task CancelTaskAsync(string taskId, string? sessionId = null, CancellationToken cancellationToken = default);
-}
diff --git a/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs b/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs
deleted file mode 100644
index b2f9b050d..000000000
--- a/src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs
+++ /dev/null
@@ -1,543 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-
-#if MCP_TEST_TIME_PROVIDER
-namespace ModelContextProtocol.Tests.Internal;
-#else
-namespace ModelContextProtocol;
-#endif
-
-///
-/// Provides an in-memory implementation of for development and testing.
-///
-///
-///
-/// This implementation uses thread-safe concurrent collections and is suitable for single-server
-/// scenarios and testing. It is not recommended for production multi-server deployments as tasks
-/// are stored only in memory and are lost on server restart.
-///
-///
-/// Features:
-///
-/// - Thread-safe operations using
-/// - Automatic TTL-based cleanup via background task
-/// - Session-based isolation when sessionId is provided
-/// - Configurable default TTL and maximum TTL limits
-///
-///
-///
-[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
-public sealed class InMemoryMcpTaskStore : IMcpTaskStore, IDisposable
-{
- private readonly ConcurrentDictionary _tasks = new();
- private readonly TimeSpan? _defaultTtl;
- private readonly TimeSpan? _maxTtl;
- private readonly TimeSpan _pollInterval;
-#if MCP_TEST_TIME_PROVIDER
- private readonly ITimer? _cleanupTimer;
-#else
- private readonly Timer? _cleanupTimer;
-#endif
- private readonly int _pageSize;
- private readonly int? _maxTasks;
- private readonly int? _maxTasksPerSession;
-#if MCP_TEST_TIME_PROVIDER
- private readonly TimeProvider _timeProvider;
-#endif
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// Default TTL to use when task creation does not specify a TTL. Null means unlimited.
- ///
- ///
- /// Maximum TTL allowed. If a task requests a longer TTL, it will be capped to this value.
- /// Null means no maximum limit.
- ///
- ///
- /// Advertised polling interval for tasks. Default is 1 second.
- /// This value is used when creating new tasks to indicate how frequently clients should poll for updates.
- ///
- ///
- /// Interval for running background cleanup of expired tasks. Default is 1 minute.
- /// Pass to disable automatic cleanup.
- ///
- ///
- /// Maximum number of tasks to return per page in . Default is 100.
- ///
- ///
- /// Maximum number of tasks allowed in the store globally. Null means unlimited.
- /// When the limit is reached, will throw .
- ///
- ///
- /// Maximum number of tasks allowed per session. Null means unlimited.
- /// When the limit is reached for a session, will throw .
- ///
- public InMemoryMcpTaskStore(
- TimeSpan? defaultTtl = null,
- TimeSpan? maxTtl = null,
- TimeSpan? pollInterval = null,
- TimeSpan? cleanupInterval = null,
- int pageSize = 100,
- int? maxTasks = null,
- int? maxTasksPerSession = null)
- {
- if (defaultTtl.HasValue && maxTtl.HasValue && defaultTtl.Value > maxTtl.Value)
- {
- throw new ArgumentException(
- $"Default TTL ({defaultTtl.Value}) cannot exceed maximum TTL ({maxTtl.Value}).",
- nameof(defaultTtl));
- }
-
- pollInterval ??= TimeSpan.FromSeconds(1);
- if (pollInterval <= TimeSpan.Zero)
- {
- throw new ArgumentOutOfRangeException(
- nameof(pollInterval),
- pollInterval,
- "Poll interval must be positive.");
- }
-
- if (pageSize <= 0)
- {
- throw new ArgumentOutOfRangeException(
- nameof(pageSize),
- pageSize,
- "Page size must be positive.");
- }
-
- if (maxTasks is <= 0)
- {
- throw new ArgumentOutOfRangeException(
- nameof(maxTasks),
- maxTasks,
- "Max tasks must be positive.");
- }
-
- if (maxTasksPerSession is <= 0)
- {
- throw new ArgumentOutOfRangeException(
- nameof(maxTasksPerSession),
- maxTasksPerSession,
- "Max tasks per session must be positive.");
- }
-
- _defaultTtl = defaultTtl;
- _maxTtl = maxTtl;
- _pollInterval = pollInterval.Value;
- _pageSize = pageSize;
- _maxTasks = maxTasks;
- _maxTasksPerSession = maxTasksPerSession;
-#if MCP_TEST_TIME_PROVIDER
- _timeProvider = TimeProvider.System;
-#endif
-
- cleanupInterval ??= TimeSpan.FromMinutes(1);
- if (cleanupInterval.Value != Timeout.InfiniteTimeSpan)
- {
-#if MCP_TEST_TIME_PROVIDER
- _cleanupTimer = _timeProvider.CreateTimer(CleanupExpiredTasks, null, cleanupInterval.Value, cleanupInterval.Value);
-#else
- _cleanupTimer = new Timer(CleanupExpiredTasks, null, cleanupInterval.Value, cleanupInterval.Value);
-#endif
- }
- }
-
-#if MCP_TEST_TIME_PROVIDER
- ///
- /// Initializes a new instance of the class with a custom time provider.
- /// This constructor is only available for testing purposes.
- ///
- internal InMemoryMcpTaskStore(
- TimeSpan? defaultTtl,
- TimeSpan? maxTtl,
- TimeSpan? pollInterval,
- TimeSpan? cleanupInterval,
- int pageSize,
- int? maxTasks,
- int? maxTasksPerSession,
- TimeProvider timeProvider)
- : this(defaultTtl, maxTtl, pollInterval, cleanupInterval, pageSize, maxTasks, maxTasksPerSession)
- {
- _timeProvider = timeProvider ?? TimeProvider.System;
- }
-#endif
-
- ///
- public Task CreateTaskAsync(
- McpTaskMetadata taskParams,
- RequestId requestId,
- JsonRpcRequest request,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- // Check global task limit
- if (_maxTasks is { } maxTasks && _tasks.Count >= maxTasks)
- {
- throw new InvalidOperationException(
- $"Maximum number of tasks ({maxTasks}) has been reached. Cannot create new task.");
- }
-
- // Check per-session task limit
- if (_maxTasksPerSession is { } maxPerSession && sessionId is not null)
- {
- var sessionTaskCount = _tasks.Values.Count(e => e.SessionId == sessionId && !IsExpired(e));
- if (sessionTaskCount >= maxPerSession)
- {
- throw new InvalidOperationException(
- $"Maximum number of tasks per session ({maxPerSession}) has been reached for session '{sessionId}'. Cannot create new task.");
- }
- }
-
- var taskId = GenerateTaskId();
- var now = GetUtcNow();
-
- // Determine TTL: use requested, fall back to default, respect max limit
- var ttl = taskParams.TimeToLive ?? _defaultTtl;
- if (ttl is { } ttlValue && _maxTtl is { } maxTtlValue && ttlValue > maxTtlValue)
- {
- ttl = maxTtlValue;
- }
-
- TaskEntry entry = new()
- {
- TaskId = taskId,
- Status = McpTaskStatus.Working,
- CreatedAt = now,
- LastUpdatedAt = now,
- TimeToLive = ttl,
- PollInterval = _pollInterval,
- RequestId = requestId,
- Request = request,
- SessionId = sessionId
- };
-
- if (!_tasks.TryAdd(taskId, entry))
- {
- // This should be extremely rare with GUID-based IDs
- throw new InvalidOperationException($"Task ID collision: {taskId}");
- }
-
- return Task.FromResult(entry.ToMcpTask());
- }
-
- ///
- public Task GetTaskAsync(string taskId, string? sessionId = null, CancellationToken cancellationToken = default)
- {
- if (!_tasks.TryGetValue(taskId, out var entry))
- {
- return Task.FromResult(null);
- }
-
- // Enforce session isolation if sessionId is provided
- if (sessionId != null && entry.SessionId != sessionId)
- {
- return Task.FromResult(null);
- }
-
- return Task.FromResult(entry.ToMcpTask());
- }
-
- ///
- public Task StoreTaskResultAsync(
- string taskId,
- McpTaskStatus status,
- JsonElement result,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- if (status is not (McpTaskStatus.Completed or McpTaskStatus.Failed))
- {
- throw new ArgumentException(
- $"Status must be {nameof(McpTaskStatus.Completed)} or {nameof(McpTaskStatus.Failed)}.",
- nameof(status));
- }
-
- // Retry loop for optimistic concurrency
- while (true)
- {
- if (!_tasks.TryGetValue(taskId, out var entry))
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // Enforce session isolation
- if (sessionId != null && entry.SessionId != sessionId)
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // Prevent overwriting terminal state
- if (IsTerminalStatus(entry.Status))
- {
- throw new InvalidOperationException(
- $"Cannot store result for task in terminal state: {entry.Status}");
- }
-
- var updatedEntry = new TaskEntry(entry)
- {
- Status = status,
- LastUpdatedAt = GetUtcNow(),
- StoredResult = result
- };
-
- if (_tasks.TryUpdate(taskId, updatedEntry, entry))
- {
- return Task.FromResult(updatedEntry.ToMcpTask());
- }
-
- // Entry was modified by another thread, retry
- }
- }
-
- ///
- public Task GetTaskResultAsync(string taskId, string? sessionId = null, CancellationToken cancellationToken = default)
- {
- if (!_tasks.TryGetValue(taskId, out var entry))
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // Enforce session isolation
- if (sessionId != entry.SessionId)
- {
- throw new InvalidOperationException($"Invalid sessionId: {sessionId} provided for {taskId}");
- }
-
- if (entry.StoredResult is not { } storedResult)
- {
- throw new InvalidOperationException($"No result stored for task: {taskId}");
- }
-
- return Task.FromResult(storedResult);
- }
-
- ///
- public Task UpdateTaskStatusAsync(
- string taskId,
- McpTaskStatus status,
- string? statusMessage,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- // Retry loop for optimistic concurrency
- while (true)
- {
- if (!_tasks.TryGetValue(taskId, out var entry))
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // Enforce session isolation
- if (sessionId != null && entry.SessionId != sessionId)
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- var updatedEntry = new TaskEntry(entry)
- {
- Status = status,
- StatusMessage = statusMessage,
- LastUpdatedAt = GetUtcNow(),
- };
-
- if (_tasks.TryUpdate(taskId, updatedEntry, entry))
- {
- return Task.FromResult(updatedEntry.ToMcpTask());
- }
-
- // Entry was modified by another thread, retry
- }
- }
-
- ///
- public Task ListTasksAsync(
- string? cursor = null,
- string? sessionId = null,
- CancellationToken cancellationToken = default)
- {
- // Stream enumeration - filter by session, exclude expired, apply keyset pagination
- var query = _tasks.Values
- .Where(e => sessionId == null || e.SessionId == sessionId)
- .Where(e => !IsExpired(e));
-
- // Apply keyset filter if cursor provided: TaskId > cursor
- // UUID v7 task IDs are monotonically increasing and inherently time-ordered
- if (cursor != null)
- {
- query = query.Where(e => string.CompareOrdinal(e.TaskId, cursor) > 0);
- }
-
- // Order by TaskId for stable, deterministic pagination
- // UUID v7 task IDs sort chronologically due to embedded timestamp
- var page = query
- .OrderBy(e => e.TaskId, StringComparer.Ordinal)
- .Take(_pageSize + 1) // Take one extra to check if there's a next page
- .Select(e => e.ToMcpTask())
- .ToList();
-
- // Set nextCursor if we have more results
- string? nextCursor;
- if (page.Count > _pageSize)
- {
- var lastItemInPage = page[_pageSize - 1]; // Last item we'll actually return
- nextCursor = lastItemInPage.TaskId;
- page.RemoveAt(_pageSize); // Remove the extra item
- }
- else
- {
- nextCursor = null;
- }
-
- return Task.FromResult(new ListTasksResult
- {
- Tasks = page.ToArray(),
- NextCursor = nextCursor
- });
- }
-
- ///
- public Task CancelTaskAsync(string taskId, string? sessionId = null, CancellationToken cancellationToken = default)
- {
- // Retry loop for optimistic concurrency
- while (true)
- {
- if (!_tasks.TryGetValue(taskId, out var entry))
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // Enforce session isolation
- if (sessionId != null && entry.SessionId != sessionId)
- {
- throw new InvalidOperationException($"Task not found: {taskId}");
- }
-
- // If already in terminal state, return unchanged
- if (IsTerminalStatus(entry.Status))
- {
- return Task.FromResult(entry.ToMcpTask());
- }
-
- var updatedEntry = new TaskEntry(entry)
- {
- Status = McpTaskStatus.Cancelled,
- LastUpdatedAt = GetUtcNow(),
- };
-
- if (_tasks.TryUpdate(taskId, updatedEntry, entry))
- {
- return Task.FromResult(updatedEntry.ToMcpTask());
- }
-
- // Entry was modified by another thread, retry
- }
- }
-
- ///
- /// Disposes the task store and stops background cleanup.
- ///
- public void Dispose()
- {
- _cleanupTimer?.Dispose();
- }
-
- private string GenerateTaskId() =>
- IdHelpers.CreateMonotonicId(GetUtcNow());
-
- private static bool IsTerminalStatus(McpTaskStatus status) =>
- status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled;
-
-#if MCP_TEST_TIME_PROVIDER
- private DateTimeOffset GetUtcNow() => _timeProvider.GetUtcNow();
-#else
- private static DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow;
-#endif
-
-#if MCP_TEST_TIME_PROVIDER
- private bool IsExpired(TaskEntry entry)
-#else
- private static bool IsExpired(TaskEntry entry)
-#endif
- {
- if (entry.TimeToLive == null)
- {
- return false; // Unlimited lifetime
- }
-
- var expirationTime = entry.CreatedAt + entry.TimeToLive.Value;
- return GetUtcNow() >= expirationTime;
- }
-
- private void CleanupExpiredTasks(object? state)
- {
- var expiredTaskIds = _tasks
- .Where(kvp => IsExpired(kvp.Value))
- .Select(kvp => kvp.Key)
- .ToList();
-
- foreach (var taskId in expiredTaskIds)
- {
- _tasks.TryRemove(taskId, out _);
- }
- }
-
- private sealed class TaskEntry
- {
- // Flattened McpTask properties
- public required string TaskId { get; init; }
- public required McpTaskStatus Status { get; init; }
- public string? StatusMessage { get; init; }
- public required DateTimeOffset CreatedAt { get; init; }
- public required DateTimeOffset LastUpdatedAt { get; init; }
- public TimeSpan? TimeToLive { get; init; }
- public TimeSpan? PollInterval { get; init; }
-
- // Request metadata
- public required RequestId RequestId { get; init; }
- public required JsonRpcRequest Request { get; init; }
- public required string? SessionId { get; init; }
- public JsonElement? StoredResult { get; init; }
-
- ///
- /// Copy constructor for creating modified copies.
- ///
- [SetsRequiredMembers]
- public TaskEntry(TaskEntry source)
- {
- TaskId = source.TaskId;
- Status = source.Status;
- StatusMessage = source.StatusMessage;
- CreatedAt = source.CreatedAt;
- LastUpdatedAt = source.LastUpdatedAt;
- TimeToLive = source.TimeToLive;
- PollInterval = source.PollInterval;
- RequestId = source.RequestId;
- Request = source.Request;
- SessionId = source.SessionId;
- StoredResult = source.StoredResult;
- }
-
- ///
- /// Default constructor for initial creation.
- ///
- public TaskEntry() { }
-
- ///
- /// Converts this entry back to an McpTask for external consumption.
- ///
- public McpTask ToMcpTask() => new()
- {
- TaskId = TaskId,
- Status = Status,
- StatusMessage = StatusMessage,
- CreatedAt = CreatedAt,
- LastUpdatedAt = LastUpdatedAt,
- TimeToLive = TimeToLive,
- PollInterval = PollInterval
- };
- }
-}
diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs
index 3caaca5a6..c8c66297d 100644
--- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs
@@ -53,63 +53,19 @@ public static McpServer Create(
/// is .
/// The client does not support sampling.
/// The request failed or the client returned an error response.
- ///
- /// When called during task-augmented tool execution, this method automatically updates the task
- /// status to while waiting for the client response,
- /// then returns to when the response is received.
- ///
- public async ValueTask SampleAsync(
+ public ValueTask SampleAsync(
CreateMessageRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
ThrowIfSamplingUnsupported();
- return await SendRequestWithTaskStatusTrackingAsync(
+ return SendRequestAsync(
RequestMethods.SamplingCreateMessage,
requestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
- "Waiting for sampling response",
- cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Requests to sample an LLM via the client as a task, allowing the server to poll for completion.
- ///
- /// The parameters for the sampling request.
- /// The task metadata specifying TTL and other task-related options.
- /// The to monitor for cancellation requests.
- /// An representing the created task on the client.
- /// or is .
- /// The client does not support sampling or task-augmented sampling.
- /// The request failed or the client returned an error response.
- ///
- /// Use to poll for task status and
- /// (with ) to retrieve the final result when the task completes.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask SampleAsTaskAsync(
- CreateMessageRequestParams requestParams,
- McpTaskMetadata taskMetadata,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNull(requestParams);
- Throw.IfNull(taskMetadata);
- ThrowIfSamplingUnsupported();
- ThrowIfTasksUnsupportedForSampling();
-
- // Set the task metadata on the request
- requestParams.Task = taskMetadata;
-
- var result = await SendRequestAsync(
- RequestMethods.SamplingCreateMessage,
- requestParams,
- McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
- McpJsonUtilities.JsonContext.Default.CreateTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- return result.Task;
+ cancellationToken: cancellationToken);
}
///
@@ -297,11 +253,6 @@ public ValueTask RequestRootsAsync(
/// is .
/// The client does not support elicitation.
/// The request failed or the client returned an error response.
- ///
- /// When called during task-augmented tool execution, this method automatically updates the task
- /// status to while waiting for user input,
- /// then returns to when the response is received.
- ///
public async ValueTask ElicitAsync(
ElicitRequestParams requestParams,
CancellationToken cancellationToken = default)
@@ -309,348 +260,14 @@ public async ValueTask ElicitAsync(
Throw.IfNull(requestParams);
ThrowIfElicitationUnsupported(requestParams);
- var result = await SendRequestWithTaskStatusTrackingAsync(
- RequestMethods.ElicitationCreate,
- requestParams,
- McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
- McpJsonUtilities.JsonContext.Default.ElicitResult,
- "Waiting for user input",
- cancellationToken).ConfigureAwait(false);
-
- return ElicitResult.WithDefaults(requestParams, result);
- }
-
- ///
- /// Requests additional information from the user via the client as a task, allowing the server to poll for completion.
- ///
- /// The parameters for the elicitation request.
- /// The task metadata specifying TTL and other task-related options.
- /// The to monitor for cancellation requests.
- /// An representing the created task on the client.
- /// or is .
- /// The client does not support elicitation or task-augmented elicitation.
- /// The request failed or the client returned an error response.
- ///
- /// Use to poll for task status and
- /// (with ) to retrieve the final result when the task completes.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask ElicitAsTaskAsync(
- ElicitRequestParams requestParams,
- McpTaskMetadata taskMetadata,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNull(requestParams);
- Throw.IfNull(taskMetadata);
- ThrowIfElicitationUnsupported(requestParams);
- ThrowIfTasksUnsupportedForElicitation();
-
- // Set the task metadata on the request
- requestParams.Task = taskMetadata;
-
var result = await SendRequestAsync(
RequestMethods.ElicitationCreate,
requestParams,
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
- McpJsonUtilities.JsonContext.Default.CreateTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- return result.Task;
- }
-
- ///
- /// Retrieves the current state of a specific task from the client.
- ///
- /// The unique identifier of the task to retrieve.
- /// The to monitor for cancellation requests. The default is .
- /// The current state of the task.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The client does not support tasks.
- /// The request failed or the client returned an error response.
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask GetTaskAsync(
- string taskId,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
- ThrowIfTasksUnsupported();
-
- var result = await SendRequestAsync(
- RequestMethods.TasksGet,
- new GetTaskRequestParams { TaskId = taskId },
- McpJsonUtilities.JsonContext.Default.GetTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.GetTaskResult,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- // Convert GetTaskResult to McpTask
- return new McpTask
- {
- TaskId = result.TaskId,
- Status = result.Status,
- StatusMessage = result.StatusMessage,
- CreatedAt = result.CreatedAt,
- LastUpdatedAt = result.LastUpdatedAt,
- TimeToLive = result.TimeToLive,
- PollInterval = result.PollInterval
- };
- }
-
- ///
- /// Retrieves the result of a completed task from the client, blocking until the task reaches a terminal state.
- ///
- /// The type to deserialize the task result into.
- /// The unique identifier of the task whose result to retrieve.
- /// Optional serializer options for deserializing the result.
- /// The to monitor for cancellation requests. The default is .
- /// The result of the task, deserialized into type .
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The client does not support tasks.
- /// The request failed or the client returned an error response.
- ///
- ///
- /// This method sends a tasks/result request to the client, which will block until the task completes if it hasn't already.
- /// The client handles all polling logic internally.
- ///
- ///
- /// For sampling tasks, use as .
- /// For elicitation tasks, use as .
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask GetTaskResultAsync(
- string taskId,
- JsonSerializerOptions? jsonSerializerOptions = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
- ThrowIfTasksUnsupported();
-
- var result = await SendRequestAsync(
- RequestMethods.TasksResult,
- new GetTaskPayloadRequestParams { TaskId = taskId },
- McpJsonUtilities.JsonContext.Default.GetTaskPayloadRequestParams,
- McpJsonUtilities.JsonContext.Default.JsonElement,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- var serializerOptions = jsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
- serializerOptions.MakeReadOnly();
-
- var typeInfo = serializerOptions.GetTypeInfo();
- return result.Deserialize(typeInfo);
- }
-
- ///
- /// Retrieves a list of all tasks from the client.
- ///
- /// The to monitor for cancellation requests. The default is .
- /// A list of all tasks.
- /// The client does not support tasks or task listing.
- /// The request failed or the client returned an error response.
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask> ListTasksAsync(
- CancellationToken cancellationToken = default)
- {
- ThrowIfTasksUnsupported();
- ThrowIfTaskListingUnsupported();
-
- List? tasks = null;
- ListTasksRequestParams requestParams = new();
- do
- {
- var taskResults = await ListTasksAsync(requestParams, cancellationToken).ConfigureAwait(false);
- if (tasks is null)
- {
- tasks = new List(taskResults.Tasks.Count);
- }
-
- foreach (var mcpTask in taskResults.Tasks)
- {
- tasks.Add(mcpTask);
- }
-
- requestParams.Cursor = taskResults.NextCursor;
- }
- while (requestParams.Cursor is not null);
-
- return tasks;
- }
-
- ///
- /// Retrieves a list of tasks from the client.
- ///
- /// The request parameters to send in the request.
- /// The to monitor for cancellation requests. The default is .
- /// The result of the request as provided by the client.
- /// is .
- /// The client does not support tasks or task listing.
- /// The request failed or the client returned an error response.
- ///
- /// The overload retrieves all tasks by automatically handling pagination.
- /// This overload works with the lower-level and , returning the raw result from the client.
- /// Any pagination needs to be managed by the caller.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public ValueTask ListTasksAsync(
- ListTasksRequestParams requestParams,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNull(requestParams);
- ThrowIfTasksUnsupported();
- ThrowIfTaskListingUnsupported();
-
- return SendRequestAsync(
- RequestMethods.TasksList,
- requestParams,
- McpJsonUtilities.JsonContext.Default.ListTasksRequestParams,
- McpJsonUtilities.JsonContext.Default.ListTasksResult,
- cancellationToken: cancellationToken);
- }
-
- ///
- /// Cancels a running task on the client.
- ///
- /// The unique identifier of the task to cancel.
- /// The to monitor for cancellation requests. The default is .
- /// The updated state of the task after cancellation.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The client does not support tasks or task cancellation.
- /// The request failed or the client returned an error response.
- ///
- /// Cancelling a task requests that the client stop execution. The client may not immediately cancel the task,
- /// and may choose to allow the task to complete if it's close to finishing.
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask CancelTaskAsync(
- string taskId,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
- ThrowIfTasksUnsupported();
- ThrowIfTaskCancellationUnsupported();
-
- var result = await SendRequestAsync(
- RequestMethods.TasksCancel,
- new CancelMcpTaskRequestParams { TaskId = taskId },
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskResult,
+ McpJsonUtilities.JsonContext.Default.ElicitResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
- // Convert CancelMcpTaskResult to McpTask
- return new McpTask
- {
- TaskId = result.TaskId,
- Status = result.Status,
- StatusMessage = result.StatusMessage,
- CreatedAt = result.CreatedAt,
- LastUpdatedAt = result.LastUpdatedAt,
- TimeToLive = result.TimeToLive,
- PollInterval = result.PollInterval
- };
- }
-
- ///
- /// Polls a task on the client until it reaches a terminal state.
- ///
- /// The unique identifier of the task to poll.
- /// The to monitor for cancellation requests. The default is .
- /// The task in its terminal state.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The client does not support tasks.
- /// The request failed or the client returned an error response.
- ///
- ///
- /// This method repeatedly calls until the task reaches a terminal status.
- /// It respects the returned by the client to determine how long
- /// to wait between polling attempts.
- ///
- ///
- /// For retrieving the actual result of a completed task, use
- /// or .
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask PollTaskUntilCompleteAsync(
- string taskId,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
-
- McpTask task;
- do
- {
- task = await GetTaskAsync(taskId, cancellationToken).ConfigureAwait(false);
-
- // If task is in a terminal state, we're done
- if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled)
- {
- break;
- }
-
- // Wait for the poll interval before checking again (default to 1 second)
- var pollInterval = task.PollInterval ?? TimeSpan.FromSeconds(1);
- await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
- }
- while (true);
-
- return task;
- }
-
- ///
- /// Waits for a task on the client to complete and retrieves its result.
- ///
- /// The type to deserialize the task result into.
- /// The unique identifier of the task whose result to retrieve.
- /// Optional serializer options for deserializing the result.
- /// The to monitor for cancellation requests. The default is .
- /// A tuple containing the final task state and its result.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// The client does not support tasks.
- /// The task failed or was cancelled.
- ///
- ///
- /// This method combines and
- /// to provide a convenient way to wait for a task to complete and retrieve its result in a single call.
- ///
- ///
- /// If the task completes with a status of or ,
- /// an is thrown.
- ///
- ///
- /// For sampling tasks, use as .
- /// For elicitation tasks, use as .
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public async ValueTask<(McpTask Task, TResult? Result)> WaitForTaskResultAsync(
- string taskId,
- JsonSerializerOptions? jsonSerializerOptions = null,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNullOrWhiteSpace(taskId);
-
- // Poll until task reaches terminal state
- var task = await PollTaskUntilCompleteAsync(taskId, cancellationToken).ConfigureAwait(false);
-
- // Check for failure or cancellation
- if (task.Status == McpTaskStatus.Failed)
- {
- throw new McpException($"Task '{taskId}' failed: {task.StatusMessage ?? "Unknown error"}");
- }
-
- if (task.Status == McpTaskStatus.Cancelled)
- {
- throw new McpException($"Task '{taskId}' was cancelled");
- }
-
- // Retrieve the result
- var result = await GetTaskResultAsync(taskId, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
-
- return (task, result);
+ return ElicitResult.WithDefaults(requestParams, result);
}
///
@@ -908,120 +525,6 @@ private void ThrowIfElicitationUnsupported(ElicitRequestParams request)
}
}
- private void ThrowIfTasksUnsupportedForSampling()
- {
- if (ClientCapabilities?.Tasks?.Requests?.Sampling?.CreateMessage is null)
- {
- if (ClientCapabilities is null)
- {
- throw new InvalidOperationException("Task-augmented sampling is not supported in stateless mode.");
- }
-
- throw new InvalidOperationException("Client does not support task-augmented sampling requests.");
- }
- }
-
- private void ThrowIfTasksUnsupportedForElicitation()
- {
- if (ClientCapabilities?.Tasks?.Requests?.Elicitation?.Create is null)
- {
- if (ClientCapabilities is null)
- {
- throw new InvalidOperationException("Task-augmented elicitation is not supported in stateless mode.");
- }
-
- throw new InvalidOperationException("Client does not support task-augmented elicitation requests.");
- }
- }
-
- private void ThrowIfTasksUnsupported()
- {
- if (ClientCapabilities?.Tasks is null)
- {
- if (ClientCapabilities is null)
- {
- throw new InvalidOperationException("Tasks are not supported in stateless mode.");
- }
-
- throw new InvalidOperationException("Client does not support tasks.");
- }
- }
-
- private void ThrowIfTaskListingUnsupported()
- {
- if (ClientCapabilities?.Tasks?.List is null)
- {
- throw new InvalidOperationException("Client does not support task listing.");
- }
- }
-
- private void ThrowIfTaskCancellationUnsupported()
- {
- if (ClientCapabilities?.Tasks?.Cancel is null)
- {
- throw new InvalidOperationException("Client does not support task cancellation.");
- }
- }
-
- ///
- /// Sends a request to the client, automatically updating task status to InputRequired during
- /// the request when called within a task execution context.
- ///
- private async ValueTask SendRequestWithTaskStatusTrackingAsync(
- string method,
- TParams requestParams,
- JsonTypeInfo paramsTypeInfo,
- JsonTypeInfo resultTypeInfo,
- string inputRequiredMessage,
- CancellationToken cancellationToken)
- where TParams : RequestParams
- where TResult : notnull
- {
- var taskContext = TaskExecutionContext.Current;
-
- // If we're not in a task execution context, just send the request normally
- if (taskContext is null)
- {
- return await SendRequestAsync(method, requestParams, paramsTypeInfo, resultTypeInfo, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
-
- // Update task status to InputRequired
- var inputRequiredTask = await taskContext.TaskStore.UpdateTaskStatusAsync(
- taskContext.TaskId,
- Protocol.McpTaskStatus.InputRequired,
- inputRequiredMessage,
- taskContext.SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send notification if enabled
- if (taskContext.SendNotifications && taskContext.NotifyTaskStatusFunc is not null)
- {
- _ = taskContext.NotifyTaskStatusFunc(inputRequiredTask, CancellationToken.None);
- }
-
- try
- {
- // Send the actual request
- return await SendRequestAsync(method, requestParams, paramsTypeInfo, resultTypeInfo, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
- finally
- {
- // Update task status back to Working
- var workingTask = await taskContext.TaskStore.UpdateTaskStatusAsync(
- taskContext.TaskId,
- Protocol.McpTaskStatus.Working,
- null, // Clear status message
- taskContext.SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send notification if enabled
- if (taskContext.SendNotifications && taskContext.NotifyTaskStatusFunc is not null)
- {
- _ = taskContext.NotifyTaskStatusFunc(workingTask, CancellationToken.None);
- }
- }
- }
-
/// Provides an implementation that's implemented via client sampling.
private sealed class SamplingChatClient(McpServer server, JsonSerializerOptions serializerOptions) : IChatClient
{
@@ -1059,50 +562,6 @@ async IAsyncEnumerable IChatClient.GetStreamingResponseAsync
void IDisposable.Dispose() { } // nop
}
- ///
- /// Sends a task status notification to the connected client.
- ///
- /// The task whose status changed.
- /// The to monitor for cancellation requests.
- /// A task representing the asynchronous notification operation.
- /// is .
- /// The notification failed or the client returned an error response.
- ///
- ///
- /// This method sends an optional status notification to inform the client of task state changes.
- /// According to the MCP specification, receivers MAY send this notification but are not required to.
- /// Clients must not rely on receiving these notifications and should continue polling via tasks/get.
- ///
- ///
- /// The notification is sent using the standard notifications/tasks/status method and includes
- /// the full task state information.
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public Task NotifyTaskStatusAsync(
- McpTask task,
- CancellationToken cancellationToken = default)
- {
- Throw.IfNull(task);
-
- var notificationParams = new McpTaskStatusNotificationParams
- {
- TaskId = task.TaskId,
- Status = task.Status,
- StatusMessage = task.StatusMessage,
- CreatedAt = task.CreatedAt,
- LastUpdatedAt = task.LastUpdatedAt,
- TimeToLive = task.TimeToLive,
- PollInterval = task.PollInterval
- };
-
- return SendNotificationAsync(
- NotificationMethods.TaskStatusNotification,
- notificationParams,
- McpJsonUtilities.JsonContext.Default.McpTaskStatusNotificationParams,
- cancellationToken);
- }
-
///
/// Provides an implementation for creating loggers
/// that send logging message notifications to the client for logged messages.
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index 04d11e016..17592eaf9 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -26,7 +26,6 @@ internal sealed partial class McpServerImpl : McpServer
private readonly RequestHandlers _requestHandlers;
private readonly McpSessionHandler _sessionHandler;
private readonly SemaphoreSlim _disposeLock = new(1, 1);
- private readonly McpTaskCancellationTokenProvider? _taskCancellationTokenProvider;
private ClientCapabilities? _clientCapabilities;
private Implementation? _clientInfo;
@@ -68,12 +67,6 @@ public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFact
_servicesScopePerRequest = options.ScopeRequests;
_logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance;
- // Only allocate the cancellation token provider if a task store is configured
- if (options.TaskStore is not null)
- {
- _taskCancellationTokenProvider = new McpTaskCancellationTokenProvider();
- }
-
_clientInfo = options.KnownClientInfo;
_clientCapabilities = options.KnownClientCapabilities;
UpdateEndpointNameWithClientInfo();
@@ -87,7 +80,6 @@ public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFact
ConfigureTools(options);
ConfigurePrompts(options);
ConfigureResources(options);
- ConfigureTasks(options);
ConfigureLogging(options);
ConfigureCompletion(options);
ConfigureExperimentalAndExtensions(options);
@@ -210,7 +202,6 @@ public override async ValueTask DisposeAsync()
_disposed = true;
- _taskCancellationTokenProvider?.Dispose();
_disposables.ForEach(d => d());
await _sessionHandler.DisposeAsync().ConfigureAwait(false);
}
@@ -700,43 +691,14 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
};
var originalCallToolHandler = callToolHandler;
- var taskStore = options.TaskStore;
- var sendNotifications = options.SendTaskStatusNotifications;
- callToolHandler = async (request, cancellationToken) =>
+ callToolHandler = (request, cancellationToken) =>
{
if (request.MatchedPrimitive is McpServerTool tool)
{
- var taskSupport = tool.ProtocolTool.Execution?.TaskSupport ?? ToolTaskSupport.Forbidden;
-
- // Check if this is a task-augmented request
- if (request.Params?.Task is { } taskMetadata)
- {
- // Validate tool-level task support
- if (taskSupport is ToolTaskSupport.Forbidden)
- {
- throw new McpProtocolException(
- $"Tool '{tool.ProtocolTool.Name}' does not support task-augmented execution.",
- McpErrorCode.InvalidParams);
- }
-
- // Task augmentation requested - return CreateTaskResult
- return await ExecuteToolAsTaskAsync(tool, request, taskMetadata, taskStore, sendNotifications, cancellationToken).ConfigureAwait(false);
- }
-
- // Validate that required task support is satisfied
- if (taskSupport is ToolTaskSupport.Required)
- {
- throw new McpProtocolException(
- $"Tool '{tool.ProtocolTool.Name}' requires task-augmented execution. " +
- "Include a 'task' parameter with the request.",
- McpErrorCode.InvalidParams);
- }
-
- // Normal synchronous execution
- return await tool.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
+ return tool.InvokeAsync(request, cancellationToken);
}
- return await originalCallToolHandler(request, cancellationToken).ConfigureAwait(false);
+ return originalCallToolHandler(request, cancellationToken);
};
listChanged = true;
@@ -756,14 +718,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
try
{
var result = await handler(request, cancellationToken).ConfigureAwait(false);
-
- // Don't log here for task-augmented calls; logging happens asynchronously
- // in ExecuteToolAsTaskAsync when the tool actually completes.
- if (result.Task is null)
- {
- ToolCallCompleted(request.Params?.Name ?? string.Empty, result.IsError is true);
- }
-
+ ToolCallCompleted(request.Params?.Name ?? string.Empty, result.IsError is true);
return result;
}
catch (Exception e)
@@ -803,138 +758,6 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
McpJsonUtilities.JsonContext.Default.CallToolResult);
}
- private void ConfigureTasks(McpServerOptions options)
- {
- var taskStore = options.TaskStore;
-
- // If no task store is configured, tasks are not supported
- if (taskStore is null)
- {
- return;
- }
-
- // Advertise task support in server capabilities
- ServerCapabilities.Tasks = new McpTasksCapability
- {
- List = new ListMcpTasksCapability(),
- Cancel = new CancelMcpTasksCapability(),
- Requests = new RequestMcpTasksCapability
- {
- Tools = new ToolsMcpTasksCapability
- {
- Call = new CallToolMcpTasksCapability()
- }
- }
- };
-
- // tasks/get handler - Retrieve task status
- McpRequestHandler getTaskHandler = async (request, cancellationToken) =>
- {
- if (request.Params?.TaskId is not { } taskId)
- {
- throw new McpProtocolException("Missing required parameter 'taskId'", McpErrorCode.InvalidParams);
- }
-
- var task = await taskStore.GetTaskAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- if (task is null)
- {
- throw new McpProtocolException($"Task not found: '{taskId}'", McpErrorCode.InvalidParams);
- }
-
- return task;
- };
-
- // tasks/result handler - Retrieve task result (blocking until terminal status)
- McpRequestHandler getTaskResultHandler = (request, cancellationToken) =>
- {
- return new ValueTask(GetTaskResultAsync(request, cancellationToken));
-
- async Task GetTaskResultAsync(RequestContext request, CancellationToken cancellationToken)
- {
- if (request.Params?.TaskId is not { } taskId)
- {
- throw new McpProtocolException("Missing required parameter 'taskId'", McpErrorCode.InvalidParams);
- }
-
- // Poll until task reaches terminal status
- while (true)
- {
- McpTask? task = await taskStore.GetTaskAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- if (task is null)
- {
- throw new McpProtocolException($"Task not found: '{taskId}'", McpErrorCode.InvalidParams);
- }
-
- // If terminal, break and retrieve result
- if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled)
- {
- break;
- }
-
- // Poll according to task's pollInterval (default 1 second)
- var pollInterval = task.PollInterval ?? TimeSpan.FromSeconds(1);
- await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
- }
-
- // Retrieve the stored result - already stored as JsonElement
- return await taskStore.GetTaskResultAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- }
- };
-
- // tasks/list handler - List tasks with pagination
- McpRequestHandler listTasksHandler = async (request, cancellationToken) =>
- {
- var cursor = request.Params?.Cursor;
- return await taskStore.ListTasksAsync(cursor, SessionId, cancellationToken).ConfigureAwait(false);
- };
-
- // tasks/cancel handler - Cancel a task
- McpRequestHandler cancelTaskHandler = async (request, cancellationToken) =>
- {
- if (request.Params?.TaskId is not { } taskId)
- {
- throw new McpProtocolException("Missing required parameter 'taskId'", McpErrorCode.InvalidParams);
- }
-
- // Signal cancellation if task is still running
- _taskCancellationTokenProvider!.Cancel(taskId);
-
- // Delegate to task store - it handles idempotent cancellation
- var task = await taskStore.CancelTaskAsync(taskId, SessionId, cancellationToken).ConfigureAwait(false);
- if (task is null)
- {
- throw new McpProtocolException($"Task not found: '{taskId}'", McpErrorCode.InvalidParams);
- }
-
- return task;
- };
-
- // Register handlers
- SetHandler(
- RequestMethods.TasksGet,
- getTaskHandler,
- McpJsonUtilities.JsonContext.Default.GetTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.McpTask);
-
- SetHandler(
- RequestMethods.TasksResult,
- getTaskResultHandler,
- McpJsonUtilities.JsonContext.Default.GetTaskPayloadRequestParams,
- McpJsonUtilities.JsonContext.Default.JsonElement);
-
- SetHandler(
- RequestMethods.TasksList,
- listTasksHandler,
- McpJsonUtilities.JsonContext.Default.ListTasksRequestParams,
- McpJsonUtilities.JsonContext.Default.ListTasksResult);
-
- SetHandler(
- RequestMethods.TasksCancel,
- cancelTaskHandler,
- McpJsonUtilities.JsonContext.Default.CancelMcpTaskRequestParams,
- McpJsonUtilities.JsonContext.Default.McpTask);
- }
-
private void ConfigureLogging(McpServerOptions options)
{
// We don't require that the handler be provided, as we always store the provided log level to the server.
@@ -1117,160 +940,4 @@ internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
[LoggerMessage(Level = LogLevel.Information, Message = "ReadResource \"{ResourceUri}\" completed.")]
private partial void ReadResourceCompleted(string resourceUri);
-
- ///
- /// Executes a tool call as a task and returns a CallToolTaskResult immediately.
- ///
- private async ValueTask ExecuteToolAsTaskAsync(
- McpServerTool tool,
- RequestContext request,
- McpTaskMetadata taskMetadata,
- IMcpTaskStore? taskStore,
- bool sendNotifications,
- CancellationToken cancellationToken)
- {
- if (taskStore is null)
- {
- throw new McpProtocolException(
- "Task-augmented requests are not supported. No task store configured.",
- McpErrorCode.InvalidRequest);
- }
-
- // Create the task in the task store
- var mcpTask = await taskStore.CreateTaskAsync(
- taskMetadata,
- request.JsonRpcRequest.Id,
- request.JsonRpcRequest,
- SessionId,
- cancellationToken).ConfigureAwait(false);
-
- // Register the task for TTL-based cancellation
- var taskCancellationToken = _taskCancellationTokenProvider!.RequestToken(mcpTask.TaskId, mcpTask.TimeToLive);
-
- // Execute the tool asynchronously in the background
- _ = Task.Run(async () =>
- {
- // When per-request service scoping is enabled, InvokeHandlerAsync creates a new
- // IServiceScope and disposes it once the handler returns. Since ExecuteToolAsTaskAsync
- // returns immediately (before the tool runs), the scope is disposed before the tool
- // gets a chance to resolve any DI services. Create a fresh scope here, tied to this
- // background task's lifetime, so the tool's DI resolution uses a live provider.
- var taskScope = _servicesScopePerRequest
- ? Services?.GetService()?.CreateAsyncScope()
- : null;
- if (taskScope is not null)
- {
- request.Services = taskScope.Value.ServiceProvider;
- }
-
- // Set up the task execution context for automatic input_required status tracking
- TaskExecutionContext.Current = new TaskExecutionContext
- {
- TaskId = mcpTask.TaskId,
- SessionId = SessionId,
- TaskStore = taskStore,
- SendNotifications = sendNotifications,
- NotifyTaskStatusFunc = NotifyTaskStatusAsync
- };
-
- try
- {
- // Update task status to working
- var workingTask = await taskStore.UpdateTaskStatusAsync(
- mcpTask.TaskId,
- McpTaskStatus.Working,
- null, // statusMessage
- SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send notification if enabled
- if (sendNotifications)
- {
- _ = NotifyTaskStatusAsync(workingTask, CancellationToken.None);
- }
-
- // Invoke the tool with task-specific cancellation token
- var result = await tool.InvokeAsync(request, taskCancellationToken).ConfigureAwait(false);
- ToolCallCompleted(request.Params?.Name ?? string.Empty, result.IsError is true);
-
- // Determine final status based on whether there was an error
- var finalStatus = result.IsError is true ? McpTaskStatus.Failed : McpTaskStatus.Completed;
-
- // Store the result (serialize to JsonElement)
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult);
- var finalTask = await taskStore.StoreTaskResultAsync(
- mcpTask.TaskId,
- finalStatus,
- resultElement,
- SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send final notification if enabled
- if (sendNotifications)
- {
- _ = NotifyTaskStatusAsync(finalTask, CancellationToken.None);
- }
- }
- catch (OperationCanceledException) when (taskCancellationToken.IsCancellationRequested)
- {
- // Task was cancelled via TTL expiration or explicit cancellation.
- // For TTL expiration, the task is deleted so no status update needed.
- // For explicit cancellation, the cancel handler already updates the status.
- }
- catch (Exception ex)
- {
- // Log the error
- ToolCallError(request.Params?.Name ?? string.Empty, ex);
-
- // Store error result
- var errorResult = new CallToolResult
- {
- IsError = true,
- Content = [new TextContentBlock { Text = $"Task execution failed: {ex.Message}" }],
- };
-
- try
- {
- var errorResultElement = JsonSerializer.SerializeToElement(errorResult, McpJsonUtilities.JsonContext.Default.CallToolResult);
- var failedTask = await taskStore.StoreTaskResultAsync(
- mcpTask.TaskId,
- McpTaskStatus.Failed,
- errorResultElement,
- SessionId,
- CancellationToken.None).ConfigureAwait(false);
-
- // Send failure notification if enabled
- if (sendNotifications)
- {
- _ = NotifyTaskStatusAsync(failedTask, CancellationToken.None);
- }
- }
- catch
- {
- // If we can't store the error result, there's not much we can do
- // The task will remain in "working" status, which will eventually be cleaned up
- }
- }
- finally
- {
- // Clean up task execution context
- TaskExecutionContext.Current = null;
-
- // Clean up task cancellation tracking
- _taskCancellationTokenProvider!.Complete(mcpTask.TaskId);
-
- // Dispose the per-task service scope (if one was created)
- if (taskScope is not null)
- {
- await taskScope.Value.DisposeAsync().ConfigureAwait(false);
- }
- }
- }, CancellationToken.None);
-
- // Return the task result immediately
- return new CallToolResult
- {
- Task = mcpTask
- };
- }
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs
index 6da8bbfbe..16a1452df 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs
@@ -186,56 +186,4 @@ public McpServerFilters Filters
/// when is not set in the request options.
///
public int MaxSamplingOutputTokens { get; set; } = 1000;
-
- ///
- /// Gets or sets the task store for managing asynchronous task execution.
- ///
- ///
- ///
- /// When non-null, enables explicit task support with persistence, allowing clients to:
- ///
- /// - Execute operations asynchronously by augmenting requests with task metadata
- /// - Poll for task status via tasks/get requests
- /// - Retrieve task results via tasks/result requests
- /// - List all tasks via tasks/list requests
- /// - Cancel tasks via tasks/cancel requests
- ///
- ///
- ///
- /// When null, implicit task support may still be available for async methods (returning or
- /// ), but tasks will be ephemeral and not persisted. Use
- /// for development/testing or implement for production scenarios.
- ///
- ///
- /// The server will automatically advertise task capabilities based on the presence of a task store
- /// and the detection of async server primitives (tools, prompts, resources).
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public IMcpTaskStore? TaskStore { get; set; }
-
- ///
- /// Gets or sets whether to send task status notifications to clients.
- ///
- ///
- /// to send optional notifications/tasks/status notifications when task status changes;
- /// to not send notifications. The default is .
- ///
- ///
- ///
- /// When enabled, the server will send notifications/tasks/status notifications to inform clients
- /// of task state changes. According to the MCP specification, these notifications are optional and
- /// receivers MAY send them but are not required to.
- ///
- ///
- /// Clients must not rely on receiving these notifications and should continue polling via tasks/get
- /// requests to ensure they receive status updates.
- ///
- ///
- /// Even when this is set to , notifications are only sent when
- /// is configured, as task-augmented requests require a task store.
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public bool SendTaskStatusNotifications { get; set; }
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
index d67bac18c..34e77e2b4 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs
@@ -157,7 +157,6 @@ public sealed class McpServerToolAttribute : Attribute
internal bool? _idempotent;
internal bool? _openWorld;
internal bool? _readOnly;
- internal ToolTaskSupport? _taskSupport;
///
/// Initializes a new instance of the class.
@@ -300,29 +299,4 @@ public bool ReadOnly
///
///
public string? IconSource { get; set; }
-
- ///
- /// Gets or sets the task support configuration for the tool.
- ///
- ///
- /// A value indicating how the tool supports task-based invocation.
- /// The default value is .
- ///
- ///
- ///
- /// When set to , clients must not attempt to invoke the tool as a task.
- /// When set to , clients may invoke the tool as a task or as a normal request.
- /// When set to , clients must invoke the tool as a task.
- ///
- ///
- /// If this property is not explicitly set on the attribute, the task support behavior will be determined
- /// automatically based on the tool's characteristics (e.g., async methods default to ).
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public ToolTaskSupport TaskSupport
- {
- get => _taskSupport ?? ToolTaskSupport.Forbidden;
- set => _taskSupport = value;
- }
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
index 88d718d13..b0b6b3de7 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
@@ -197,23 +197,6 @@ public sealed class McpServerToolCreateOptions
///
public JsonObject? Meta { get; set; }
- ///
- /// Gets or sets the execution hints for this tool.
- ///
- ///
- ///
- /// Execution hints provide information about how the tool should be invoked, including
- /// task support level ().
- ///
- ///
- /// If , the tool's execution settings are determined automatically based on
- /// the method signature (async methods get ; sync methods
- /// get ).
- ///
- ///
- [Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
- public ToolExecution? Execution { get; set; }
-
///
/// Creates a shallow clone of the current instance.
///
@@ -235,6 +218,5 @@ internal McpServerToolCreateOptions Clone() =>
Metadata = Metadata,
Icons = Icons,
Meta = Meta,
- Execution = Execution,
};
}
diff --git a/src/ModelContextProtocol.Core/Server/TaskExecutionContext.cs b/src/ModelContextProtocol.Core/Server/TaskExecutionContext.cs
deleted file mode 100644
index fc45835c4..000000000
--- a/src/ModelContextProtocol.Core/Server/TaskExecutionContext.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-namespace ModelContextProtocol.Server;
-
-///
-/// Represents the execution context for a task being executed by the server.
-/// This context flows with async execution and enables automatic task status updates.
-///
-internal sealed class TaskExecutionContext
-{
- ///
- /// Gets the AsyncLocal instance used to track the current task execution context.
- ///
- private static readonly AsyncLocal s_current = new();
-
- ///
- /// Gets or sets the current task execution context for the executing async flow.
- ///
- public static TaskExecutionContext? Current
- {
- get => s_current.Value;
- set => s_current.Value = value;
- }
-
- ///
- /// Gets the task ID of the currently executing task.
- ///
- public required string TaskId { get; init; }
-
- ///
- /// Gets the session ID associated with the task.
- ///
- public string? SessionId { get; init; }
-
- ///
- /// Gets the task store used to persist task state.
- ///
- public required IMcpTaskStore TaskStore { get; init; }
-
- ///
- /// Gets whether task status notifications should be sent.
- ///
- public bool SendNotifications { get; init; }
-
- ///
- /// Gets or sets the function to call when sending a task status notification.
- ///
- public Func? NotifyTaskStatusFunc { get; init; }
-}
diff --git a/src/ModelContextProtocol/McpServerOptionsSetup.cs b/src/ModelContextProtocol/McpServerOptionsSetup.cs
index 5977fae7e..c46854460 100644
--- a/src/ModelContextProtocol/McpServerOptionsSetup.cs
+++ b/src/ModelContextProtocol/McpServerOptionsSetup.cs
@@ -9,12 +9,10 @@ namespace ModelContextProtocol;
/// The individually registered tools.
/// The individually registered prompts.
/// The individually registered resources.
-/// The optional task store registered in DI.
internal sealed class McpServerOptionsSetup(
IEnumerable serverTools,
IEnumerable serverPrompts,
- IEnumerable serverResources,
- IMcpTaskStore? taskStore = null) : IConfigureOptions
+ IEnumerable serverResources) : IConfigureOptions
{
///
/// Configures the given McpServerOptions instance by setting server information
@@ -25,8 +23,6 @@ public void Configure(McpServerOptions options)
{
Throw.IfNull(options);
- options.TaskStore ??= taskStore;
-
// Collect all of the provided tools into a tools collection. If the options already has
// a collection, add to it, otherwise create a new one. We want to maintain the identity
// of an existing collection in case someone has provided their own derived type, wants
diff --git a/src/ModelContextProtocol/ModelContextProtocol.csproj b/src/ModelContextProtocol/ModelContextProtocol.csproj
index 231eb073a..07167c438 100644
--- a/src/ModelContextProtocol/ModelContextProtocol.csproj
+++ b/src/ModelContextProtocol/ModelContextProtocol.csproj
@@ -8,7 +8,7 @@
.NET SDK for the Model Context Protocol (MCP) with hosting and dependency injection extensions.
README.md
True
-
+
$(NoWarn);MCPEXP001
diff --git a/tests/Common/Utils/TestServerTransport.cs b/tests/Common/Utils/TestServerTransport.cs
index 43cd5c262..ed9b6ee72 100644
--- a/tests/Common/Utils/TestServerTransport.cs
+++ b/tests/Common/Utils/TestServerTransport.cs
@@ -46,14 +46,6 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can
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 @@ await WriteMessageAsync(new JsonRpcResponse
private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cancellationToken)
{
- // Check if the request is task-augmented (has Task metadata)
- var requestParams = JsonSerializer.Deserialize(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 sampling response
- await WriteMessageAsync(new JsonRpcResponse
- {
- 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(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);
- }
- }
-
- ///
- /// Gets or sets the task to return from tasks/get requests.
- ///
- public McpTask? MockTask { get; set; }
-
- ///
- /// Gets or sets the result to return from tasks/result requests.
- ///
- public object? MockTaskResult { get; set; }
-
- ///
- /// Gets or sets the list of tasks to return from tasks/list requests.
- ///
- 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 sampling 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),
+ Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model" }, 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)
+ private async Task ElicitAsync(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,
- };
-
+ // Return a normal elicitation response
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);
}
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index 1071ec394..bc169333f 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -3,7 +3,7 @@
True
-
+
$(NoWarn);MCPEXP001
$(NoWarn);MCP9004
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpTaskIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpTaskIntegrationTests.cs
deleted file mode 100644
index 2b74fcd14..000000000
--- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpTaskIntegrationTests.cs
+++ /dev/null
@@ -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;
-
-///
-/// Integration tests for MCP Tasks feature over HTTP transports.
-/// Tests task creation, polling, cancellation, and result retrieval.
-///
-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 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 CreateArguments(string key, object? value)
- {
- return new Dictionary
- {
- [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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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();
-
- 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 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}";
- }
- }
-}
diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs
index 9cb963a96..0765c7450 100644
--- a/tests/ModelContextProtocol.TestServer/Program.cs
+++ b/tests/ModelContextProtocol.TestServer/Program.cs
@@ -162,27 +162,6 @@ private static void ConfigureTools(McpServerOptions options, string? cliArg)
"""),
},
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 @@ private static void ConfigureTools(McpServerOptions options, string? cliArg)
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))
diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs
index a36a0a6e0..a6f37f2a6 100644
--- a/tests/ModelContextProtocol.TestSseServer/Program.cs
+++ b/tests/ModelContextProtocol.TestSseServer/Program.cs
@@ -146,27 +146,6 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
}
"""),
},
- 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 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().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);
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs
deleted file mode 100644
index ada9970cf..000000000
--- a/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs
+++ /dev/null
@@ -1,261 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using ModelContextProtocol.Client;
-using ModelContextProtocol.Protocol;
-using ModelContextProtocol.Server;
-using System.Text.Json;
-
-namespace ModelContextProtocol.Tests.Client;
-
-public class McpClientTaskMethodsTests : ClientServerTestBase
-{
- public McpClientTaskMethodsTests(ITestOutputHelper outputHelper)
- : base(outputHelper)
- {
- }
-
- protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
- {
- // Add task store for server-side task support
- var taskStore = new InMemoryMcpTaskStore();
- services.AddSingleton(taskStore);
-
- // Configure server to use the task store directly
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- // Add a simple tool for testing
- mcpServerBuilder.WithTools([McpServerTool.Create(
- async (string input, CancellationToken ct) =>
- {
- await Task.Delay(50, ct);
- return $"Processed: {input}";
- },
- new McpServerToolCreateOptions
- {
- Name = "test-tool",
- Description = "A test tool"
- })]);
- }
-
- private static IDictionary CreateArguments(string key, object? value)
- {
- // For simple strings, just create a JsonElement from a string value
- return new Dictionary
- {
- [key] = JsonDocument.Parse($"\"{value}\"").RootElement.Clone()
- };
- }
-
- [Fact]
- public async Task GetTaskAsync_ReturnsTaskStatus()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- // Create a task by calling a tool with task metadata
- var callResult = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "test-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- cancellationToken: TestContext.Current.CancellationToken);
-
- // The response should contain task metadata
- Assert.NotNull(callResult.Task);
-
- string taskId = callResult.Task.TaskId;
-
- // Now get the task status
- var task = await client.GetTaskAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.Equal(taskId, task.TaskId);
- }
-
- [Fact]
- public async Task GetTaskAsync_ThrowsForInvalidTaskId()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- await Assert.ThrowsAsync(async () =>
- await client.GetTaskAsync("", cancellationToken: TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task GetTaskResultAsync_ReturnsDeserializedResult()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- // Create a task
- var callResult = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "test-tool",
- Arguments = CreateArguments("input", "hello"),
- Task = new McpTaskMetadata()
- },
- cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(callResult.Task);
- string taskId = callResult.Task.TaskId;
-
- // Wait for task to complete and get the result
- JsonElement result = await client.GetTaskResultAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
-
- // Verify the result has the expected CallToolResult shape
- CallToolResult? toolResult = result.Deserialize(McpJsonUtilities.DefaultOptions);
- Assert.NotNull(toolResult);
- Assert.NotEmpty(toolResult.Content);
-
- TextContentBlock? textContent = toolResult.Content[0] as TextContentBlock;
- Assert.NotNull(textContent);
- Assert.Equal("Processed: hello", textContent.Text);
- }
-
- [Fact]
- public async Task GetTaskResultAsync_ThrowsForInvalidTaskId()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- await Assert.ThrowsAsync(async () =>
- await client.GetTaskResultAsync("", cancellationToken: TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task ListTasksAsync_ReturnsTasks()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- // Create a task
- var callResult = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "test-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(callResult.Task);
- string taskId = callResult.Task.TaskId;
-
- // List all tasks
- var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(tasks);
- Assert.Contains(tasks, t => t.TaskId == taskId);
- }
-
- [Fact]
- public async Task ListTasksAsync_HandlesEmptyResult()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- // List tasks (may or may not be empty depending on state)
- var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(tasks);
- }
-
- [Fact]
- public async Task ListTasksAsync_LowLevel_ReturnsRawResult()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- // Create a task first
- await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "test-tool",
- Arguments = CreateArguments("input", "task1"),
- Task = new McpTaskMetadata()
- },
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Use low-level API
- var result = await client.ListTasksAsync(new ListTasksRequestParams(), TestContext.Current.CancellationToken);
-
- Assert.NotNull(result);
- Assert.NotNull(result.Tasks);
- }
-
- [Fact]
- public async Task ListTasksAsync_LowLevel_ThrowsForNullParams()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- await Assert.ThrowsAsync(async () =>
- await client.ListTasksAsync((ListTasksRequestParams)null!, TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task CancelTaskAsync_CancelsRunningTask()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- // Create a task
- var callResult = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "test-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(callResult.Task);
- string taskId = callResult.Task.TaskId;
-
- // Cancel the task
- var canceledTask = await client.CancelTaskAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.Equal(taskId, canceledTask.TaskId);
- }
-
- [Fact]
- public async Task CancelTaskAsync_ThrowsForInvalidTaskId()
- {
- await using McpClient client = await CreateMcpClientForServer();
-
- await Assert.ThrowsAsync(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();
- for (int i = 0; i < 3; i++)
- {
- var result = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "test-tool",
- Arguments = CreateArguments("input", $"task-{i}"),
- Task = new McpTaskMetadata()
- },
- cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(result.Task);
- taskIds.Add(result.Task.TaskId);
- }
-
- // List all tasks (should handle pagination automatically if needed)
- var tasks = await client.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(tasks);
- Assert.True(tasks.Count >= taskIds.Count, "Should retrieve at least the tasks we created");
-
- // Verify all our tasks are in the result
- foreach (var taskId in taskIds)
- {
- Assert.Contains(tasks, t => t.TaskId == taskId);
- }
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTaskSamplingElicitationTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTaskSamplingElicitationTests.cs
deleted file mode 100644
index 906b4f491..000000000
--- a/tests/ModelContextProtocol.Tests/Client/McpClientTaskSamplingElicitationTests.cs
+++ /dev/null
@@ -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;
-
-///
-/// 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.
-///
-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(taskStore);
-
- // Configure server to use the task store
- services.Configure(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().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 CreateArguments(string key, object? value)
- {
- return new Dictionary
- {
- [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(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(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(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(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();
-
- 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(
- mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.NotNull(result);
- var textContent = Assert.IsType(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(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(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(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(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();
-
- 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
- {
- ["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(
- 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();
- var allowCompletion = new TaskCompletionSource();
-
- 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(TaskCreationOptions.RunContinuationsAsynchronously);
- var completedNotificationReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- var notificationsReceived = new List();
- 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(
- 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 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();
-
- 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();
-
- 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(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(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(new CreateMessageResult
- {
- Content = [new TextContentBlock { Text = "Response" }],
- Model = "model"
- });
- },
- ElicitationHandler = (request, ct) =>
- {
- return new ValueTask(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
-}
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs
index 689aba9d0..dc2eaf805 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs
@@ -284,57 +284,4 @@ public void Configure_WithCompleteHandler_CreatesCompletionsCapability()
Assert.NotNull(options.Capabilities?.Completions);
}
#endregion
-
- #region TaskStore Tests
- [Fact]
- public void TaskStore_IsPopulatedFromDI_WhenNotExplicitlySet()
- {
- var services = new ServiceCollection();
- services.AddMcpServer();
- services.AddSingleton();
-
- var options = services.BuildServiceProvider().GetRequiredService>().Value;
-
- Assert.IsType(options.TaskStore);
- }
-
- [Fact]
- public void TaskStore_ExplicitOption_TakesPrecedenceOverDI()
- {
- var explicitStore = new InMemoryMcpTaskStore();
-
- var services = new ServiceCollection();
- services.AddMcpServer(options => options.TaskStore = explicitStore);
- services.AddSingleton();
-
- var options = services.BuildServiceProvider().GetRequiredService>().Value;
-
- Assert.Same(explicitStore, options.TaskStore);
- }
-
- [Fact]
- public void TaskStore_RemainsNull_WhenNothingIsRegistered()
- {
- var services = new ServiceCollection();
- services.AddMcpServer();
-
- var options = services.BuildServiceProvider().GetRequiredService>().Value;
-
- Assert.Null(options.TaskStore);
- }
-
- [Fact]
- public void TaskStore_CanBeOverriddenToNull_AfterDIRegistration()
- {
- var services = new ServiceCollection();
- services.AddMcpServer();
- services.AddSingleton();
-
- services.Configure(options => options.TaskStore = null);
-
- var options = services.BuildServiceProvider().GetRequiredService>().Value;
-
- Assert.Null(options.TaskStore);
- }
- #endregion
}
\ No newline at end of file
diff --git a/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs b/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs
index d68902ef5..866a59c61 100644
--- a/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs
+++ b/tests/ModelContextProtocol.Tests/ExperimentalPropertySerializationTests.cs
@@ -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;
///
///
///
-/// Experimental properties (e.g. , )
+/// Experimental properties (e.g. , )
/// use an internal *Core property for serialization. A consumer's source-generated
/// cannot see internal members, so experimental data is
/// silently dropped unless the consumer chains the SDK's resolver into their options.
///
///
-/// These tests depend on and
+/// These tests depend on and
/// 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 void ExperimentalProperties_Dropped_WithConsumerContextOnly()
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 { ["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 { ["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(json, options)!;
- Assert.Equal("test-tool", deserialized.Name);
- Assert.Null(deserialized.Execution);
+ var deserialized = JsonSerializer.Deserialize(json, options)!;
+ Assert.NotNull(deserialized.Tools);
+ Assert.Null(deserialized.Extensions);
}
[Fact]
@@ -76,35 +77,36 @@ public void ExperimentalProperties_RoundTrip_WhenSdkResolverIsChained()
}
};
- var tool = new Tool
+ var capabilities = new ServerCapabilities
{
- Name = "test-tool",
- Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ Tools = new ToolsCapability(),
+ Extensions = new Dictionary { ["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(json, options)!;
- Assert.Equal("test-tool", deserialized.Name);
- Assert.NotNull(deserialized.Execution);
- Assert.Equal(ToolTaskSupport.Optional, deserialized.Execution.TaskSupport);
+ var deserialized = JsonSerializer.Deserialize(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 { ["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(json, McpJsonUtilities.DefaultOptions)!;
- Assert.NotNull(deserialized.Tasks);
+ var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions)!;
+ Assert.NotNull(deserialized.Extensions);
+ Assert.True(deserialized.Extensions.ContainsKey("io.test"));
}
}
diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
index 7f7de2a41..a9b40a412 100644
--- a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
+++ b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
@@ -35,10 +35,6 @@
-
-
-
-
diff --git a/tests/ModelContextProtocol.Tests/Protocol/CallToolRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CallToolRequestParamsTests.cs
index d2f5a09ad..ec758120f 100644
--- a/tests/ModelContextProtocol.Tests/Protocol/CallToolRequestParamsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Protocol/CallToolRequestParamsTests.cs
@@ -17,7 +17,6 @@ public static void CallToolRequestParams_SerializationRoundTrip_PreservesAllProp
["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 void CallToolRequestParams_SerializationRoundTrip_PreservesAllProp
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 void CallToolRequestParams_SerializationRoundTrip_WithMinimalPrope
Assert.NotNull(deserialized);
Assert.Equal(original.Name, deserialized.Name);
Assert.Null(deserialized.Arguments);
- Assert.Null(deserialized.Task);
Assert.Null(deserialized.Meta);
}
}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/CallToolResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CallToolResultTests.cs
index d66e03b3f..b1ac90c9d 100644
--- a/tests/ModelContextProtocol.Tests/Protocol/CallToolResultTests.cs
+++ b/tests/ModelContextProtocol.Tests/Protocol/CallToolResultTests.cs
@@ -14,13 +14,6 @@ public static void CallToolResult_SerializationRoundTrip_PreservesAllProperties(
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 void CallToolResult_SerializationRoundTrip_PreservesAllProperties(
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 void CallToolResult_SerializationRoundTrip_WithMinimalProperties()
Assert.Empty(deserialized.Content);
Assert.Null(deserialized.StructuredContent);
Assert.Null(deserialized.IsError);
- Assert.Null(deserialized.Task);
Assert.Null(deserialized.Meta);
}
}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/CancelMcpTaskRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CancelMcpTaskRequestParamsTests.cs
deleted file mode 100644
index a3b3b2ef6..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/CancelMcpTaskRequestParamsTests.cs
+++ /dev/null
@@ -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(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Equal(original.TaskId, deserialized.TaskId);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/CancelMcpTaskResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CancelMcpTaskResultTests.cs
deleted file mode 100644
index 5cf628642..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/CancelMcpTaskResultTests.cs
+++ /dev/null
@@ -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(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);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs
index cacb7e84e..82613dd53 100644
--- a/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs
+++ b/tests/ModelContextProtocol.Tests/Protocol/ClientCapabilitiesTests.cs
@@ -21,7 +21,6 @@ public static void ClientCapabilities_SerializationRoundTrip_PreservesAllPropert
Form = new FormElicitationCapability(),
Url = new UrlElicitationCapability()
},
- Tasks = new McpTasksCapability(),
Extensions = new Dictionary
{
["io.modelcontextprotocol/test"] = new object()
@@ -40,7 +39,6 @@ public static void ClientCapabilities_SerializationRoundTrip_PreservesAllPropert
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 void ClientCapabilities_SerializationRoundTrip_WithMinimalProperti
Assert.Null(deserialized.Roots);
Assert.Null(deserialized.Sampling);
Assert.Null(deserialized.Elicitation);
- Assert.Null(deserialized.Tasks);
Assert.Null(deserialized.Extensions);
}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/CreateTaskResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CreateTaskResultTests.cs
deleted file mode 100644
index 0252053cb..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/CreateTaskResultTests.cs
+++ /dev/null
@@ -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(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"]!);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitRequestParamsTests.cs
index 1d57f55ad..f8e2fedbf 100644
--- a/tests/ModelContextProtocol.Tests/Protocol/ElicitRequestParamsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitRequestParamsTests.cs
@@ -23,7 +23,6 @@ public static void ElicitRequestParams_SerializationRoundTrip_PreservesAllProper
["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 void ElicitRequestParams_SerializationRoundTrip_PreservesAllProper
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 void ElicitRequestParams_SerializationRoundTrip_UrlMode()
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 void ElicitRequestParams_SerializationRoundTrip_WithMinimalPropert
Assert.Null(deserialized.ElicitationId);
Assert.Null(deserialized.Url);
Assert.Null(deserialized.RequestedSchema);
- Assert.Null(deserialized.Task);
Assert.Null(deserialized.Meta);
}
}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/GetTaskPayloadRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/GetTaskPayloadRequestParamsTests.cs
deleted file mode 100644
index 47f427259..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/GetTaskPayloadRequestParamsTests.cs
+++ /dev/null
@@ -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(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Equal(original.TaskId, deserialized.TaskId);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/GetTaskRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/GetTaskRequestParamsTests.cs
deleted file mode 100644
index 9b3e7b1d5..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/GetTaskRequestParamsTests.cs
+++ /dev/null
@@ -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(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Equal(original.TaskId, deserialized.TaskId);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/GetTaskResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/GetTaskResultTests.cs
deleted file mode 100644
index ece58683f..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/GetTaskResultTests.cs
+++ /dev/null
@@ -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(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);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ListTasksRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ListTasksRequestParamsTests.cs
deleted file mode 100644
index 3e9022757..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/ListTasksRequestParamsTests.cs
+++ /dev/null
@@ -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(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Equal(original.Cursor, deserialized.Cursor);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ListTasksResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ListTasksResultTests.cs
deleted file mode 100644
index 8d2fbd33b..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/ListTasksResultTests.cs
+++ /dev/null
@@ -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(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);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/McpTaskMetadataTests.cs b/tests/ModelContextProtocol.Tests/Protocol/McpTaskMetadataTests.cs
deleted file mode 100644
index 82f33fbe7..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/McpTaskMetadataTests.cs
+++ /dev/null
@@ -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(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(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);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/McpTaskStatusNotificationParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/McpTaskStatusNotificationParamsTests.cs
deleted file mode 100644
index bf3cbbbf0..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/McpTaskStatusNotificationParamsTests.cs
+++ /dev/null
@@ -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(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);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/McpTaskTests.cs b/tests/ModelContextProtocol.Tests/Protocol/McpTaskTests.cs
deleted file mode 100644
index 7919e408e..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/McpTaskTests.cs
+++ /dev/null
@@ -1,160 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Text.Json;
-
-namespace ModelContextProtocol.Tests.Protocol;
-
-public static class McpTaskTests
-{
- [Fact]
- public static void McpTask_SerializationRoundTrip_PreservesAllProperties()
- {
- // Arrange
- var original = new McpTask
- {
- TaskId = "task-12345",
- Status = McpTaskStatus.Working,
- StatusMessage = "Processing request",
- CreatedAt = new DateTimeOffset(2025, 12, 9, 10, 30, 0, TimeSpan.Zero),
- LastUpdatedAt = new DateTimeOffset(2025, 12, 9, 10, 35, 0, TimeSpan.Zero),
- TimeToLive = TimeSpan.FromHours(24),
- PollInterval = TimeSpan.FromSeconds(5)
- };
-
- // Act - Serialize to JSON
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
-
- // Act - Deserialize back from JSON
- var deserialized = JsonSerializer.Deserialize(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);
- }
-
- [Fact]
- public static void McpTask_SerializationRoundTrip_WithMinimalProperties()
- {
- // Arrange
- var original = new McpTask
- {
- TaskId = "task-minimal",
- Status = McpTaskStatus.Completed,
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow
- };
-
- // Act - Serialize to JSON
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
-
- // Act - Deserialize back from JSON
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Equal(original.TaskId, deserialized.TaskId);
- Assert.Equal(original.Status, deserialized.Status);
- Assert.Null(deserialized.StatusMessage);
- Assert.Equal(original.CreatedAt, deserialized.CreatedAt);
- Assert.Equal(original.LastUpdatedAt, deserialized.LastUpdatedAt);
- Assert.Null(deserialized.TimeToLive);
- Assert.Null(deserialized.PollInterval);
- }
-
- [Fact]
- public static void McpTask_HasCorrectJsonPropertyNames()
- {
- var task = new McpTask
- {
- TaskId = "test-task",
- Status = McpTaskStatus.Working,
- StatusMessage = "Test message",
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow,
- TimeToLive = TimeSpan.FromMinutes(30),
- PollInterval = TimeSpan.FromSeconds(1)
- };
-
- string json = JsonSerializer.Serialize(task, McpJsonUtilities.DefaultOptions);
-
- Assert.Contains("\"taskId\":", json);
- Assert.Contains("\"status\":", json);
- Assert.Contains("\"statusMessage\":", json);
- Assert.Contains("\"createdAt\":", json);
- Assert.Contains("\"lastUpdatedAt\":", json);
- Assert.Contains("\"ttl\":", json);
- Assert.Contains("\"pollInterval\":", json);
- }
-
- [Fact]
- public static void McpTask_TimeToLive_SerializesAsMilliseconds()
- {
- var task = new McpTask
- {
- TaskId = "test-ttl",
- Status = McpTaskStatus.Working,
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow,
- TimeToLive = TimeSpan.FromSeconds(60)
- };
-
- string json = JsonSerializer.Serialize(task, McpJsonUtilities.DefaultOptions);
-
- Assert.Contains("\"ttl\":60000", json);
- }
-
- [Theory]
- [InlineData(McpTaskStatus.Working)]
- [InlineData(McpTaskStatus.InputRequired)]
- [InlineData(McpTaskStatus.Completed)]
- [InlineData(McpTaskStatus.Failed)]
- [InlineData(McpTaskStatus.Cancelled)]
- public static void McpTaskStatus_SerializesCorrectly(McpTaskStatus status)
- {
- var task = new McpTask
- {
- TaskId = "status-test",
- Status = status,
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow
- };
-
- string json = JsonSerializer.Serialize(task, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- Assert.NotNull(deserialized);
- Assert.Equal(status, deserialized.Status);
- }
-
- [Fact]
- public static void McpTaskStatus_HasCorrectJsonValues()
- {
- var statuses = new[]
- {
- (McpTaskStatus.Working, "working"),
- (McpTaskStatus.InputRequired, "input_required"),
- (McpTaskStatus.Completed, "completed"),
- (McpTaskStatus.Failed, "failed"),
- (McpTaskStatus.Cancelled, "cancelled")
- };
-
- foreach (var (status, expectedJson) in statuses)
- {
- var task = new McpTask
- {
- TaskId = "test",
- Status = status,
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow
- };
-
- string json = JsonSerializer.Serialize(task, McpJsonUtilities.DefaultOptions);
- Assert.Contains($"\"status\":\"{expectedJson}\"", json);
- }
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/McpTasksCapabilityTests.cs b/tests/ModelContextProtocol.Tests/Protocol/McpTasksCapabilityTests.cs
deleted file mode 100644
index 4e8caa740..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/McpTasksCapabilityTests.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Text.Json;
-
-namespace ModelContextProtocol.Tests.Protocol;
-
-public static class McpTasksCapabilityTests
-{
- [Fact]
- public static void McpTasksCapability_SerializationRoundTrip_WithAllProperties()
- {
- // Arrange
- var original = new McpTasksCapability
- {
- List = new ListMcpTasksCapability(),
- Cancel = new CancelMcpTasksCapability(),
- Requests = new RequestMcpTasksCapability
- {
- Tools = new ToolsMcpTasksCapability
- {
- Call = new CallToolMcpTasksCapability()
- },
- Sampling = new SamplingMcpTasksCapability
- {
- CreateMessage = new CreateMessageMcpTasksCapability()
- },
- Elicitation = new ElicitationMcpTasksCapability
- {
- Create = new CreateElicitationMcpTasksCapability()
- }
- }
- };
-
- // Act
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.NotNull(deserialized.List);
- Assert.NotNull(deserialized.Cancel);
- Assert.NotNull(deserialized.Requests);
- Assert.NotNull(deserialized.Requests.Tools);
- Assert.NotNull(deserialized.Requests.Tools.Call);
- Assert.NotNull(deserialized.Requests.Sampling);
- Assert.NotNull(deserialized.Requests.Sampling.CreateMessage);
- Assert.NotNull(deserialized.Requests.Elicitation);
- Assert.NotNull(deserialized.Requests.Elicitation.Create);
- }
-
- [Fact]
- public static void McpTasksCapability_SerializationRoundTrip_WithMinimalProperties()
- {
- // Arrange
- var original = new McpTasksCapability();
-
- // Act
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Null(deserialized.List);
- Assert.Null(deserialized.Cancel);
- Assert.Null(deserialized.Requests);
- }
-
- [Fact]
- public static void McpTasksCapability_HasCorrectJsonPropertyNames()
- {
- var capability = new McpTasksCapability
- {
- List = new ListMcpTasksCapability(),
- Cancel = new CancelMcpTasksCapability(),
- Requests = new RequestMcpTasksCapability
- {
- Tools = new ToolsMcpTasksCapability
- {
- Call = new CallToolMcpTasksCapability()
- }
- }
- };
-
- string json = JsonSerializer.Serialize(capability, McpJsonUtilities.DefaultOptions);
-
- Assert.Contains("\"list\":", json);
- Assert.Contains("\"cancel\":", json);
- Assert.Contains("\"requests\":", json);
- Assert.Contains("\"tools\":", json);
- Assert.Contains("\"call\":", json);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/RequestMcpTasksCapabilityTests.cs b/tests/ModelContextProtocol.Tests/Protocol/RequestMcpTasksCapabilityTests.cs
deleted file mode 100644
index 8bfcb3be4..000000000
--- a/tests/ModelContextProtocol.Tests/Protocol/RequestMcpTasksCapabilityTests.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Text.Json;
-
-namespace ModelContextProtocol.Tests.Protocol;
-
-public static class RequestMcpTasksCapabilityTests
-{
- [Fact]
- public static void RequestMcpTasksCapability_SerializationRoundTrip_ToolsOnly()
- {
- // Arrange
- var original = new RequestMcpTasksCapability
- {
- Tools = new ToolsMcpTasksCapability
- {
- Call = new CallToolMcpTasksCapability()
- }
- };
-
- // Act
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.NotNull(deserialized.Tools);
- Assert.NotNull(deserialized.Tools.Call);
- Assert.Null(deserialized.Sampling);
- Assert.Null(deserialized.Elicitation);
- }
-
- [Fact]
- public static void RequestMcpTasksCapability_SerializationRoundTrip_SamplingOnly()
- {
- // Arrange
- var original = new RequestMcpTasksCapability
- {
- Sampling = new SamplingMcpTasksCapability
- {
- CreateMessage = new CreateMessageMcpTasksCapability()
- }
- };
-
- // Act
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Null(deserialized.Tools);
- Assert.NotNull(deserialized.Sampling);
- Assert.NotNull(deserialized.Sampling.CreateMessage);
- Assert.Null(deserialized.Elicitation);
- }
-
- [Fact]
- public static void RequestMcpTasksCapability_SerializationRoundTrip_ElicitationOnly()
- {
- // Arrange
- var original = new RequestMcpTasksCapability
- {
- Elicitation = new ElicitationMcpTasksCapability
- {
- Create = new CreateElicitationMcpTasksCapability()
- }
- };
-
- // Act
- string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
-
- // Assert
- Assert.NotNull(deserialized);
- Assert.Null(deserialized.Tools);
- Assert.Null(deserialized.Sampling);
- Assert.NotNull(deserialized.Elicitation);
- Assert.NotNull(deserialized.Elicitation.Create);
- }
-
- [Fact]
- public static void RequestMcpTasksCapability_HasCorrectJsonPropertyNames()
- {
- var capability = new RequestMcpTasksCapability
- {
- Tools = new ToolsMcpTasksCapability
- {
- Call = new CallToolMcpTasksCapability()
- },
- Sampling = new SamplingMcpTasksCapability
- {
- CreateMessage = new CreateMessageMcpTasksCapability()
- },
- Elicitation = new ElicitationMcpTasksCapability
- {
- Create = new CreateElicitationMcpTasksCapability()
- }
- };
-
- string json = JsonSerializer.Serialize(capability, McpJsonUtilities.DefaultOptions);
-
- Assert.Contains("\"tools\":", json);
- Assert.Contains("\"sampling\":", json);
- Assert.Contains("\"elicitation\":", json);
- Assert.Contains("\"call\":", json);
- Assert.Contains("\"createMessage\":", json);
- Assert.Contains("\"create\":", json);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs
index a6f8265f1..7b95e911b 100644
--- a/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs
+++ b/tests/ModelContextProtocol.Tests/Protocol/ServerCapabilitiesTests.cs
@@ -15,7 +15,6 @@ public static void ServerCapabilities_SerializationRoundTrip_PreservesAllPropert
Resources = new ResourcesCapability { Subscribe = true, ListChanged = true },
Tools = new ToolsCapability { ListChanged = false },
Completions = new CompletionsCapability(),
- Tasks = new McpTasksCapability(),
Extensions = new Dictionary
{
["io.modelcontextprotocol/apps"] = new object()
@@ -35,7 +34,6 @@ public static void ServerCapabilities_SerializationRoundTrip_PreservesAllPropert
Assert.NotNull(deserialized.Tools);
Assert.False(deserialized.Tools.ListChanged);
Assert.NotNull(deserialized.Completions);
- Assert.NotNull(deserialized.Tasks);
Assert.NotNull(deserialized.Extensions);
Assert.True(deserialized.Extensions.ContainsKey("io.modelcontextprotocol/apps"));
}
@@ -55,7 +53,6 @@ public static void ServerCapabilities_SerializationRoundTrip_WithMinimalProperti
Assert.Null(deserialized.Resources);
Assert.Null(deserialized.Tools);
Assert.Null(deserialized.Completions);
- Assert.Null(deserialized.Tasks);
Assert.Null(deserialized.Extensions);
}
diff --git a/tests/ModelContextProtocol.Tests/Server/AutomaticInputRequiredStatusTests.cs b/tests/ModelContextProtocol.Tests/Server/AutomaticInputRequiredStatusTests.cs
deleted file mode 100644
index 1f5c51c6c..000000000
--- a/tests/ModelContextProtocol.Tests/Server/AutomaticInputRequiredStatusTests.cs
+++ /dev/null
@@ -1,478 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Client;
-using ModelContextProtocol.Protocol;
-using ModelContextProtocol.Server;
-using ModelContextProtocol.Tests.Utils;
-using System.IO.Pipelines;
-
-namespace ModelContextProtocol.Tests.Server;
-
-///
-/// Tests for automatic InputRequired status tracking when server-to-client
-/// requests (SampleAsync, ElicitAsync) are made during task-augmented tool execution.
-///
-public class AutomaticInputRequiredStatusTests : LoggedTest
-{
- public AutomaticInputRequiredStatusTests(ITestOutputHelper testOutputHelper)
- : base(testOutputHelper)
- {
- }
-
-#pragma warning disable MCPEXP001 // Tasks feature is experimental
-
- [Fact]
- public async Task TaskStatus_TransitionsToInputRequired_DuringSampleAsync()
- {
- // Arrange
- var taskStore = new InMemoryMcpTaskStore();
- var statusesDuringSampling = new List();
- var samplingRequestReceived = new TaskCompletionSource();
- var continueSampling = new TaskCompletionSource();
-
- await using var fixture = new InputRequiredTestFixture(
- LoggerFactory,
- configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- options.SendTaskStatusNotifications = true; // Enable notifications
- });
-
- // Tool that calls SampleAsync during execution
- builder.WithTools([McpServerTool.Create(
- async (string prompt, McpServer server, CancellationToken ct) =>
- {
- // Call SampleAsync - this should trigger InputRequired status
- var result = await server.SampleAsync(new CreateMessageRequestParams
- {
- Messages = [new SamplingMessage
- {
- Role = Role.User,
- Content = [new TextContentBlock { Text = prompt }]
- }],
- MaxTokens = 100
- }, ct);
-
- var textContent = result.Content.OfType().FirstOrDefault();
- return textContent?.Text ?? "No response";
- },
- new McpServerToolCreateOptions
- {
- Name = "sampling-tool",
- Description = "A tool that uses sampling"
- })]);
- },
- configureClient: clientOptions =>
- {
- clientOptions.Handlers = new McpClientHandlers
- {
- SamplingHandler = async (request, progress, ct) =>
- {
- // Signal that we received the sampling request
- samplingRequestReceived.TrySetResult(true);
-
- // Wait for permission to continue (so we can check status)
- await continueSampling.Task.WaitAsync(ct);
-
- return new CreateMessageResult
- {
- Content = [new TextContentBlock { Text = "Sampled response" }],
- Model = "test-model"
- };
- }
- };
- });
-
- // Act - Call the tool as a task
- var mcpTask = await fixture.Client.CallToolAsTaskAsync(
- "sampling-tool",
- arguments: new Dictionary { ["prompt"] = "Hello" },
- taskMetadata: new McpTaskMetadata(),
- progress: null,
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Wait for the sampling request to be received by the client
- await samplingRequestReceived.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
-
- // Check the task status while sampling is in progress
- var statusDuringSampling = await taskStore.GetTaskAsync(
- mcpTask.TaskId,
- cancellationToken: TestContext.Current.CancellationToken);
-
- if (statusDuringSampling is not null)
- {
- statusesDuringSampling.Add(statusDuringSampling.Status);
- }
-
- // Allow sampling to complete
- continueSampling.TrySetResult(true);
-
- // Wait for task to complete
- McpTask? finalStatus = null;
- int maxAttempts = 50;
- do
- {
- await Task.Delay(100, TestContext.Current.CancellationToken);
- finalStatus = await taskStore.GetTaskAsync(mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
- maxAttempts--;
- }
- while (finalStatus?.Status is not McpTaskStatus.Completed && maxAttempts > 0);
-
- // Assert - Status should have been InputRequired during sampling
- Assert.Contains(McpTaskStatus.InputRequired, statusesDuringSampling);
-
- // Final status should be Completed
- Assert.NotNull(finalStatus);
- Assert.Equal(McpTaskStatus.Completed, finalStatus.Status);
- }
-
- [Fact]
- public async Task TaskStatus_TransitionsToInputRequired_DuringElicitAsync()
- {
- // Arrange
- var taskStore = new InMemoryMcpTaskStore();
- var statusesDuringElicitation = new List();
- var elicitationRequestReceived = new TaskCompletionSource();
- var continueElicitation = new TaskCompletionSource();
-
- await using var fixture = new InputRequiredTestFixture(
- LoggerFactory,
- configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- options.SendTaskStatusNotifications = true;
- });
-
- // Tool that calls ElicitAsync during execution
- builder.WithTools([McpServerTool.Create(
- async (string message, McpServer server, CancellationToken ct) =>
- {
- // Call ElicitAsync - this should trigger InputRequired status
- var result = await server.ElicitAsync(new ElicitRequestParams
- {
- Message = message,
- RequestedSchema = new()
- }, ct);
-
- return result.Action == "confirm" ? "Confirmed" : "Declined";
- },
- new McpServerToolCreateOptions
- {
- Name = "elicitation-tool",
- Description = "A tool that uses elicitation"
- })]);
- },
- configureClient: clientOptions =>
- {
- clientOptions.Handlers = new McpClientHandlers
- {
- ElicitationHandler = async (request, ct) =>
- {
- // Signal that we received the elicitation request
- elicitationRequestReceived.TrySetResult(true);
-
- // Wait for permission to continue
- await continueElicitation.Task.WaitAsync(ct);
-
- return new ElicitResult { Action = "confirm" };
- }
- };
- });
-
- // Act - Call the tool as a task
- var mcpTask = await fixture.Client.CallToolAsTaskAsync(
- "elicitation-tool",
- arguments: new Dictionary { ["message"] = "Please confirm" },
- taskMetadata: new McpTaskMetadata(),
- progress: null,
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Wait for the elicitation request to be received
- await elicitationRequestReceived.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
-
- // Check the task status while elicitation is in progress
- var statusDuringElicitation = await taskStore.GetTaskAsync(
- mcpTask.TaskId,
- cancellationToken: TestContext.Current.CancellationToken);
-
- if (statusDuringElicitation is not null)
- {
- statusesDuringElicitation.Add(statusDuringElicitation.Status);
- }
-
- // Allow elicitation to complete
- continueElicitation.TrySetResult(true);
-
- // Wait for task to complete
- McpTask? finalStatus = null;
- int maxAttempts = 50;
- do
- {
- await Task.Delay(100, TestContext.Current.CancellationToken);
- finalStatus = await taskStore.GetTaskAsync(mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
- maxAttempts--;
- }
- while (finalStatus?.Status is not McpTaskStatus.Completed && maxAttempts > 0);
-
- // Assert - Status should have been InputRequired during elicitation
- Assert.Contains(McpTaskStatus.InputRequired, statusesDuringElicitation);
-
- // Final status should be Completed
- Assert.NotNull(finalStatus);
- Assert.Equal(McpTaskStatus.Completed, finalStatus.Status);
- }
-
- [Fact]
- public async Task TaskStatus_ReturnsToWorking_AfterSamplingCompletes()
- {
- // Arrange
- var taskStore = new InMemoryMcpTaskStore();
- var samplingCompleted = new TaskCompletionSource();
- var checkStatusAfterSampling = new TaskCompletionSource();
-
- await using var fixture = new InputRequiredTestFixture(
- LoggerFactory,
- configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- // Tool that calls SampleAsync and then waits
- builder.WithTools([McpServerTool.Create(
- async (string prompt, McpServer server, CancellationToken ct) =>
- {
- // Call SampleAsync
- var result = await server.SampleAsync(new CreateMessageRequestParams
- {
- Messages = [new SamplingMessage
- {
- Role = Role.User,
- Content = [new TextContentBlock { Text = prompt }]
- }],
- MaxTokens = 100
- }, ct);
-
- // Signal that sampling completed
- samplingCompleted.TrySetResult(true);
-
- // Wait so test can check status
- await checkStatusAfterSampling.Task.WaitAsync(ct);
-
- var textContent = result.Content.OfType().FirstOrDefault();
- return textContent?.Text ?? "No response";
- },
- new McpServerToolCreateOptions
- {
- Name = "sampling-tool",
- Description = "A tool that uses sampling"
- })]);
- },
- configureClient: clientOptions =>
- {
- clientOptions.Handlers = new McpClientHandlers
- {
- SamplingHandler = (request, progress, ct) =>
- {
- // Return immediately to let sampling complete
- return new ValueTask(new CreateMessageResult
- {
- Content = [new TextContentBlock { Text = "Response" }],
- Model = "test-model"
- });
- }
- };
- });
-
- // Act - Call the tool as a task
- var mcpTask = await fixture.Client.CallToolAsTaskAsync(
- "sampling-tool",
- arguments: new Dictionary { ["prompt"] = "Hello" },
- taskMetadata: new McpTaskMetadata(),
- progress: null,
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Wait for sampling to complete inside the tool
- await samplingCompleted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
-
- // Small delay to ensure status update is processed
- await Task.Delay(50, TestContext.Current.CancellationToken);
-
- // Check status after sampling completed (should be back to Working)
- var taskAfterSampling = await taskStore.GetTaskAsync(mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
-
- // Allow tool to complete
- checkStatusAfterSampling.TrySetResult(true);
-
- // Assert - Status should be Working after sampling completes (before tool completes)
- Assert.NotNull(taskAfterSampling);
- Assert.Equal(McpTaskStatus.Working, taskAfterSampling.Status);
- }
-
- [Fact]
- public async Task TaskStatus_DoesNotChangeToInputRequired_ForNonTaskExecution()
- {
- // Arrange - When a tool is NOT executed as a task, SampleAsync should not change any task status
- var taskStore = new InMemoryMcpTaskStore();
- var samplingCompleted = new TaskCompletionSource();
-
- await using var fixture = new InputRequiredTestFixture(
- LoggerFactory,
- configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- // Tool that calls SampleAsync - note it doesn't have TaskSupport.Required so can be called directly
- builder.WithTools([McpServerTool.Create(
- async (string prompt, McpServer server, CancellationToken ct) =>
- {
- var result = await server.SampleAsync(new CreateMessageRequestParams
- {
- Messages = [new SamplingMessage
- {
- Role = Role.User,
- Content = [new TextContentBlock { Text = prompt }]
- }],
- MaxTokens = 100
- }, ct);
-
- samplingCompleted.TrySetResult(true);
- var textContent = result.Content.OfType().FirstOrDefault();
- return textContent?.Text ?? "No response";
- },
- new McpServerToolCreateOptions
- {
- Name = "sampling-tool",
- Description = "A tool that uses sampling",
- Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
- })]);
- },
- configureClient: clientOptions =>
- {
- clientOptions.Handlers = new McpClientHandlers
- {
- SamplingHandler = (request, progress, ct) =>
- {
- return new ValueTask(new CreateMessageResult
- {
- Content = [new TextContentBlock { Text = "Response" }],
- Model = "test-model"
- });
- }
- };
- });
-
- // Act - Call the tool DIRECTLY (not as a task)
- var result = await fixture.Client.CallToolAsync(
- "sampling-tool",
- arguments: new Dictionary { ["prompt"] = "Hello" },
- cancellationToken: TestContext.Current.CancellationToken);
-
- await samplingCompleted.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
-
- // Assert - No task should exist (tool was not called as a task)
- var tasks = await taskStore.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Empty(tasks.Tasks);
-
- // And the result should still work
- Assert.NotNull(result);
- }
-
-#pragma warning restore MCPEXP001
-
- ///
- /// Test fixture that supports both server and client configuration for InputRequired status tests.
- ///
- private sealed class InputRequiredTestFixture : IAsyncDisposable
- {
- private readonly Pipe _clientToServerPipe = new();
- private readonly Pipe _serverToClientPipe = new();
- private readonly IServiceProvider _serviceProvider;
- private readonly McpServer _server;
- private readonly Task _serverTask;
- private readonly CancellationTokenSource _cts;
-
- public McpClient Client { get; }
- public McpServer Server => _server;
-
- public InputRequiredTestFixture(
- ILoggerFactory loggerFactory,
- Action? configureServer = null,
- Action? configureClient = null)
- {
- _cts = new CancellationTokenSource();
-
- // Configure server
- var services = new ServiceCollection();
- services.AddLogging();
- services.AddSingleton(loggerFactory);
-
- var builder = services
- .AddMcpServer()
- .WithStreamServerTransport(
- _clientToServerPipe.Reader.AsStream(),
- _serverToClientPipe.Writer.AsStream());
-
- configureServer?.Invoke(services, builder);
-
- _serviceProvider = services.BuildServiceProvider(validateScopes: true);
- _server = _serviceProvider.GetRequiredService();
- _serverTask = _server.RunAsync(_cts.Token);
-
- // Configure client
- var clientOptions = new McpClientOptions();
- configureClient?.Invoke(clientOptions);
-
- // Create client synchronously (test code)
- Client = McpClient.CreateAsync(
- new StreamClientTransport(
- serverInput: _clientToServerPipe.Writer.AsStream(),
- _serverToClientPipe.Reader.AsStream(),
- loggerFactory),
- clientOptions: clientOptions,
- loggerFactory: loggerFactory,
- cancellationToken: TestContext.Current.CancellationToken).GetAwaiter().GetResult();
- }
-
- public async ValueTask DisposeAsync()
- {
- await Client.DisposeAsync();
- await _cts.CancelAsync();
-
- _clientToServerPipe.Writer.Complete();
- _serverToClientPipe.Writer.Complete();
-
- try
- {
- await _serverTask;
- }
- catch (OperationCanceledException)
- {
- // Expected
- }
-
- if (_serviceProvider is IAsyncDisposable asyncDisposable)
- {
- await asyncDisposable.DisposeAsync();
- }
- else if (_serviceProvider is IDisposable disposable)
- {
- disposable.Dispose();
- }
-
- _cts.Dispose();
- }
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs b/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs
deleted file mode 100644
index 7d2fc5596..000000000
--- a/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs
+++ /dev/null
@@ -1,1231 +0,0 @@
-using Microsoft.Extensions.Time.Testing;
-using ModelContextProtocol.Protocol;
-using ModelContextProtocol.Server;
-using ModelContextProtocol.Tests.Utils;
-using System.Text.Json;
-using TestInMemoryMcpTaskStore = ModelContextProtocol.Tests.Internal.InMemoryMcpTaskStore;
-
-namespace ModelContextProtocol.Tests.Server;
-
-public class InMemoryMcpTaskStoreTests : LoggedTest
-{
- public InMemoryMcpTaskStoreTests(ITestOutputHelper testOutputHelper)
- : base(testOutputHelper)
- {
- }
-
- [Fact]
- public async Task CreateTaskAsync_CreatesTaskWithUniqueId()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var requestId = new RequestId("req-1");
- var request = new JsonRpcRequest { Method = "tools/call" };
-
- // Act
- var task = await store.CreateTaskAsync(metadata, requestId, request, "session-1", TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotNull(task);
- Assert.NotEmpty(task.TaskId);
- Assert.Equal(McpTaskStatus.Working, task.Status);
- Assert.NotEqual(default, task.CreatedAt);
- Assert.NotEqual(default, task.LastUpdatedAt);
- }
-
- [Fact]
- public async Task CreateTaskAsync_GeneratesUniqueTaskIds()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
-
- // Act
- var task1 = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var task2 = await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotEqual(task1.TaskId, task2.TaskId);
- }
-
- [Fact]
- public async Task CreateTaskAsync_AppliesTtlFromMetadata()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata
- {
- TimeToLive = TimeSpan.FromSeconds(5)
- };
-
- // Act
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(TimeSpan.FromSeconds(5), task.TimeToLive);
- }
-
- [Fact]
- public async Task CreateTaskAsync_CapsMaxTtl()
- {
- // Arrange
- var maxTtl = TimeSpan.FromMinutes(5);
- using var store = new InMemoryMcpTaskStore(maxTtl: maxTtl);
- var metadata = new McpTaskMetadata
- {
- TimeToLive = TimeSpan.FromHours(1) // Request 1 hour
- };
-
- // Act
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(maxTtl, task.TimeToLive);
- }
-
- [Fact]
- public async Task GetTaskAsync_ReturnsTaskById()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var created = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act
- var retrieved = await store.GetTaskAsync(created.TaskId, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotNull(retrieved);
- Assert.Equal(created.TaskId, retrieved.TaskId);
- Assert.Equal(created.Status, retrieved.Status);
- }
-
- [Fact]
- public async Task GetTaskAsync_ReturnsNullForNonexistentTask()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
-
- // Act
- var task = await store.GetTaskAsync("nonexistent-id", null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Null(task);
- }
-
- [Fact]
- public async Task GetTaskAsync_EnforcesSessionIsolation()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // Act
- var sameSession = await store.GetTaskAsync(task.TaskId, "session-1", TestContext.Current.CancellationToken);
- var differentSession = await store.GetTaskAsync(task.TaskId, "session-2", TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotNull(sameSession);
- Assert.Null(differentSession);
- }
-
- [Fact]
- public async Task StoreTaskResultAsync_StoresResultForCompletedTask()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var result = new CallToolResult { Content = [new TextContentBlock { Text = "Success" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
-
- // Act
- await store.StoreTaskResultAsync(task.TaskId, McpTaskStatus.Completed, resultElement, null, TestContext.Current.CancellationToken);
-
- // Assert
- var retrieved = await store.GetTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
- Assert.Equal(McpTaskStatus.Completed, retrieved!.Status);
- }
-
- [Fact]
- public async Task StoreTaskResultAsync_EnforcesSessionIsolation()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
- var result = new CallToolResult { Content = [new TextContentBlock { Text = "Success" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
-
- // Act & Assert
- await Assert.ThrowsAsync(
- () => store.StoreTaskResultAsync(task.TaskId, McpTaskStatus.Completed, resultElement, "session-2", TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task StoreTaskResultAsync_ThrowsForNonTerminalStatus()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var result = new CallToolResult { Content = [new TextContentBlock { Text = "Success" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
-
- // Act & Assert
- await Assert.ThrowsAsync(
- () => store.StoreTaskResultAsync(task.TaskId, McpTaskStatus.Working, resultElement, null, TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task GetTaskResultAsync_ReturnsStoredResult()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var result = new CallToolResult { Content = [new TextContentBlock { Text = "Success" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
- await store.StoreTaskResultAsync(task.TaskId, McpTaskStatus.Completed, resultElement, null, TestContext.Current.CancellationToken);
-
- // Act
- var retrieved = await store.GetTaskResultAsync(task.TaskId, null, TestContext.Current.CancellationToken);
-
- // Assert
- var callToolResult = retrieved.Deserialize(McpJsonUtilities.DefaultOptions)!;
- Assert.Single(callToolResult.Content);
- Assert.Equal("Success", ((TextContentBlock)callToolResult.Content[0]).Text);
- }
-
- [Fact]
- public async Task GetTaskResultAsync_EnforcesSessionIsolation()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
- var result = new CallToolResult { Content = [new TextContentBlock { Text = "Success" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
- await store.StoreTaskResultAsync(task.TaskId, McpTaskStatus.Completed, resultElement, "session-1", TestContext.Current.CancellationToken);
-
- // Act & Assert
- await Assert.ThrowsAsync(
- () => store.GetTaskResultAsync(task.TaskId, "session-2", TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task UpdateTaskStatusAsync_UpdatesStatus()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act
- await store.UpdateTaskStatusAsync(task.TaskId, McpTaskStatus.Working, "Processing...", null, TestContext.Current.CancellationToken);
-
- // Assert
- var updated = await store.GetTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
- Assert.Equal(McpTaskStatus.Working, updated!.Status);
- Assert.Equal("Processing...", updated.StatusMessage);
- }
-
- [Fact]
- public async Task UpdateTaskStatusAsync_UpdatesLastUpdatedAt()
- {
- // Arrange - Use FakeTimeProvider for deterministic testing
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- using var store = new TestInMemoryMcpTaskStore(
- defaultTtl: null,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: Timeout.InfiniteTimeSpan,
- pageSize: 100,
- maxTasks: null,
- maxTasksPerSession: null,
- timeProvider: fakeTime);
-
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var originalTimestamp = task.LastUpdatedAt;
-
- // Advance time to ensure timestamp changes
- fakeTime.Advance(TimeSpan.FromMilliseconds(10));
-
- // Act
- await store.UpdateTaskStatusAsync(task.TaskId, McpTaskStatus.Working, null, null, TestContext.Current.CancellationToken);
-
- // Assert
- var updated = await store.GetTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
- Assert.True(updated!.LastUpdatedAt > originalTimestamp);
- }
-
- #region Input Required Status Tests
-
- // NOTE: The InputRequired status is automatically set by the server when a tool executing
- // as a task calls SampleAsync() or ElicitAsync(). The status is set back to Working when
- // the request completes. See TaskExecutionContext for implementation details.
- // The tests below verify the store correctly handles status transitions.
-
- [Fact]
- public async Task InputRequiredStatus_SerializesCorrectly()
- {
- // Verify the input_required status serializes as expected
- var task = new McpTask
- {
- TaskId = "test-task",
- Status = McpTaskStatus.InputRequired,
- StatusMessage = "Waiting for user input",
- CreatedAt = DateTimeOffset.UtcNow,
- LastUpdatedAt = DateTimeOffset.UtcNow
- };
-
- string json = JsonSerializer.Serialize(task, McpJsonUtilities.DefaultOptions);
-
- Assert.Contains("\"status\":\"input_required\"", json);
- }
-
- [Fact]
- public async Task InputRequiredStatus_CanTransitionToWorking()
- {
- // Arrange - Spec: "From input_required: may move to working, completed, failed, or cancelled"
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Transition to input_required (testing store's status transition capability)
- var inputRequiredTask = await store.UpdateTaskStatusAsync(
- task.TaskId,
- McpTaskStatus.InputRequired,
- "Waiting for user confirmation",
- cancellationToken: TestContext.Current.CancellationToken);
-
- Assert.Equal(McpTaskStatus.InputRequired, inputRequiredTask.Status);
-
- // Act - Transition back to working
- var workingTask = await store.UpdateTaskStatusAsync(
- task.TaskId,
- McpTaskStatus.Working,
- "Processing resumed",
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(McpTaskStatus.Working, workingTask.Status);
- }
-
- [Fact]
- public async Task InputRequiredStatus_CanTransitionToCancelled()
- {
- // Arrange - Spec: Task transitions show input_required can go to terminal states
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Transition to input_required
- await store.UpdateTaskStatusAsync(
- task.TaskId,
- McpTaskStatus.InputRequired,
- "Need input",
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Act - Transition to cancelled
- var cancelledTask = await store.CancelTaskAsync(
- task.TaskId,
- cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(McpTaskStatus.Cancelled, cancelledTask.Status);
- }
-
- #endregion
-
- [Fact]
- public async Task ListTasksAsync_ReturnsAllTasks()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var task1 = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var task2 = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act
- var result = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(2, result.Tasks.Count);
- Assert.Contains(result.Tasks, t => t.TaskId == task1.TaskId);
- Assert.Contains(result.Tasks, t => t.TaskId == task2.TaskId);
- Assert.Null(result.NextCursor);
- }
-
- [Fact]
- public async Task ListTasksAsync_FiltersBySession()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var task1 = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
- var task2 = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, "session-2", TestContext.Current.CancellationToken);
-
- // Act
- var session1Result = await store.ListTasksAsync(sessionId: "session-1", cancellationToken: TestContext.Current.CancellationToken);
- var session2Result = await store.ListTasksAsync(sessionId: "session-2", cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Single(session1Result.Tasks);
- Assert.Equal(task1.TaskId, session1Result.Tasks[0].TaskId);
- Assert.Single(session2Result.Tasks);
- Assert.Equal(task2.TaskId, session2Result.Tasks[0].TaskId);
- }
-
- [Fact]
- public async Task ListTasksAsync_SupportsPagination()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
-
- // Create 150 tasks (more than page size of 100)
- for (int i = 0; i < 150; i++)
- {
- await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- }
-
- // Act - First page
- var firstPageResult = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- // Act - Second page
- var secondPageResult = await store.ListTasksAsync(cursor: firstPageResult.NextCursor, cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(100, firstPageResult.Tasks.Count);
- Assert.NotNull(firstPageResult.NextCursor);
- Assert.Equal(50, secondPageResult.Tasks.Count);
- Assert.Null(secondPageResult.NextCursor);
- }
-
- [Fact]
- public async Task CancelTaskAsync_CancelsTask()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act
- var cancelled = await store.CancelTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(McpTaskStatus.Cancelled, cancelled.Status);
- }
-
- [Fact]
- public async Task CancelTaskAsync_IsIdempotent()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // First cancellation
- await store.CancelTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
-
- // Act - Second cancellation
- var result = await store.CancelTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
-
- // Assert - Should return unchanged task, not throw
- Assert.Equal(McpTaskStatus.Cancelled, result.Status);
- }
-
- [Fact]
- public async Task CancelTaskAsync_DoesNotCancelCompletedTask()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var result = new CallToolResult { Content = [new TextContentBlock { Text = "Success" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
- await store.StoreTaskResultAsync(task.TaskId, McpTaskStatus.Completed, resultElement, null, TestContext.Current.CancellationToken);
-
- // Act
- var cancelResult = await store.CancelTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
-
- // Assert - Task remains completed
- Assert.Equal(McpTaskStatus.Completed, cancelResult.Status);
- }
-
- [Fact]
- public async Task CancelTaskAsync_EnforcesSessionIsolation()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // Act & Assert
- await Assert.ThrowsAsync(
- () => store.CancelTaskAsync(task.TaskId, "session-2", TestContext.Current.CancellationToken));
- }
-
- [Fact]
- public async Task Dispose_StopsCleanupTimer()
- {
- // Arrange - Use FakeTimeProvider for deterministic testing
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- var cleanupInterval = TimeSpan.FromMilliseconds(100);
-
- var store = new TestInMemoryMcpTaskStore(
- defaultTtl: null,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: cleanupInterval,
- pageSize: 100,
- maxTasks: null,
- maxTasksPerSession: null,
- timeProvider: fakeTime);
-
- var metadata = new McpTaskMetadata { TimeToLive = TimeSpan.FromMilliseconds(100) };
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act
- store.Dispose();
-
- // Advance time - timer should not fire after dispose
- fakeTime.Advance(TimeSpan.FromTicks(cleanupInterval.Ticks * 3));
-
- // Assert - Store should still be accessible after dispose (no exceptions)
- // The cleanup timer should have stopped
- Assert.True(true); // If we get here without exceptions, dispose worked
- }
-
- [Fact]
- public async Task CleanupExpiredTasks_RemovesExpiredTasks()
- {
- // Arrange - Use FakeTimeProvider for deterministic testing
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- var cleanupInterval = TimeSpan.FromMilliseconds(50);
- var ttl = TimeSpan.FromMilliseconds(100);
-
- using var store = new TestInMemoryMcpTaskStore(
- defaultTtl: null,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: cleanupInterval,
- pageSize: 100,
- maxTasks: null,
- maxTasksPerSession: null,
- timeProvider: fakeTime);
-
- var metadata = new McpTaskMetadata { TimeToLive = ttl };
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Verify task exists initially
- var resultBefore = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Single(resultBefore.Tasks);
-
- // Advance time past the TTL to make task expired
- fakeTime.Advance(ttl + TimeSpan.FromMilliseconds(1));
-
- // Trigger cleanup by advancing time past cleanup interval
- fakeTime.Advance(cleanupInterval);
-
- // Act - List tasks to verify cleanup happened
- var resultAfter = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Empty(resultAfter.Tasks); // Task should be cleaned up by the timer
- }
-
- [Fact]
- public async Task DefaultTtl_AppliedWhenNoTtlSpecified()
- {
- // Arrange
- var defaultTtl = TimeSpan.FromMinutes(10);
- using var store = new InMemoryMcpTaskStore(defaultTtl: defaultTtl);
- var metadata = new McpTaskMetadata(); // No TTL specified
-
- // Act
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(defaultTtl, task.TimeToLive);
- }
-
- [Fact]
- public async Task MultipleOperations_ConcurrentAccess()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var tasks = new List>();
-
- // Act - Create multiple tasks concurrently
- for (int i = 0; i < 10; i++)
- {
- int taskNum = i;
- tasks.Add(Task.Run(async () =>
- {
- var metadata = new McpTaskMetadata();
- return await store.CreateTaskAsync(metadata, new RequestId($"req-{taskNum}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- }));
- }
-
- var createdTasks = await Task.WhenAll(tasks);
-
- // Assert - All tasks should be created with unique IDs
- Assert.Equal(10, createdTasks.Length);
- Assert.Equal(10, createdTasks.Select(t => t.TaskId).Distinct().Count());
- }
-
- [Fact]
- public void Constructor_ThrowsWhenDefaultTtlExceedsMaxTtl()
- {
- // Arrange & Act & Assert
- var exception = Assert.Throws(() =>
- new InMemoryMcpTaskStore(
- defaultTtl: TimeSpan.FromHours(2),
- maxTtl: TimeSpan.FromHours(1)));
-
- Assert.Equal("defaultTtl", exception.ParamName);
- Assert.Contains("Default TTL", exception.Message);
- Assert.Contains("cannot exceed maximum TTL", exception.Message);
- }
-
- [Fact]
- public async Task CreateTaskAsync_UsesConfiguredPollInterval()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(pollInterval: TimeSpan.FromMilliseconds(2500));
- var metadata = new McpTaskMetadata();
-
- // Act
- var task = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(TimeSpan.FromMilliseconds(2500), task.PollInterval);
- }
-
- [Fact]
- public void Constructor_ThrowsWhenPollIntervalIsZero()
- {
- // Arrange & Act & Assert
- var exception = Assert.Throws(() =>
- new InMemoryMcpTaskStore(pollInterval: TimeSpan.Zero));
-
- Assert.Equal("pollInterval", exception.ParamName);
- Assert.Contains("Poll interval must be positive", exception.Message);
- }
-
- [Fact]
- public void Constructor_ThrowsWhenPollIntervalIsNegative()
- {
- // Arrange & Act & Assert
- var exception = Assert.Throws(() =>
- new InMemoryMcpTaskStore(pollInterval: TimeSpan.FromMilliseconds(-100)));
-
- Assert.Equal("pollInterval", exception.ParamName);
- Assert.Contains("Poll interval must be positive", exception.Message);
- }
-
- [Fact]
- public async Task GetTaskAsync_ReturnsDefensiveCopy()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var createdTask = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act - Get the task and modify the returned copy
- var retrievedTask = await store.GetTaskAsync(createdTask.TaskId, null, TestContext.Current.CancellationToken);
- var originalStatus = retrievedTask!.Status;
- retrievedTask.Status = McpTaskStatus.Completed;
- retrievedTask.StatusMessage = "Modified externally";
-
- // Assert - Get the task again and verify the stored state wasn't affected
- var taskAgain = await store.GetTaskAsync(createdTask.TaskId, null, TestContext.Current.CancellationToken);
- Assert.Equal(originalStatus, taskAgain!.Status);
- Assert.Null(taskAgain.StatusMessage);
- }
-
- [Fact]
- public async Task ListTasksAsync_ReturnsDefensiveCopies()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act - List tasks and modify the returned copies
- var result = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
- var firstTask = result.Tasks[0];
- var originalTaskId = firstTask.TaskId;
- firstTask.Status = McpTaskStatus.Failed;
- firstTask.StatusMessage = "Modified in list";
-
- // Assert - Get the task directly and verify the stored state wasn't affected
- var directTask = await store.GetTaskAsync(originalTaskId, null, TestContext.Current.CancellationToken);
- Assert.Equal(McpTaskStatus.Working, directTask!.Status);
- Assert.Null(directTask.StatusMessage);
- }
-
- [Fact]
- public async Task CancelTaskAsync_ReturnsDefensiveCopy()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var metadata = new McpTaskMetadata();
- var createdTask = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act - Cancel the task and modify the returned copy
- var cancelledTask = await store.CancelTaskAsync(createdTask.TaskId, null, TestContext.Current.CancellationToken);
- cancelledTask.StatusMessage = "Modified after cancel";
- cancelledTask.Status = McpTaskStatus.Completed;
-
- // Assert - Get the task again and verify it's still cancelled with no message
- var taskAgain = await store.GetTaskAsync(createdTask.TaskId, null, TestContext.Current.CancellationToken);
- Assert.Equal(McpTaskStatus.Cancelled, taskAgain!.Status);
- Assert.Null(taskAgain.StatusMessage);
- }
-
- [Fact]
- public async Task ConcurrentUpdates_HandlesContentionCorrectly()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var task = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act - Launch 100 concurrent updates to the same task
- var updateTasks = Enumerable.Range(0, 100).Select(i =>
- Task.Run(async () =>
- {
- try
- {
- await store.UpdateTaskStatusAsync(task.TaskId, McpTaskStatus.Working, $"Update {i}", null, TestContext.Current.CancellationToken);
- return true;
- }
- catch
- {
- return false;
- }
- }));
-
- var results = await Task.WhenAll(updateTasks);
-
- // Assert - All updates should succeed (retry loop handles contention)
- Assert.All(results, success => Assert.True(success));
-
- // Verify task is still in valid state (one of the updates won)
- var finalTask = await store.GetTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
- Assert.NotNull(finalTask);
- Assert.Equal(McpTaskStatus.Working, finalTask.Status);
- Assert.Matches(@"Update \d+", finalTask.StatusMessage!);
- }
-
- [Fact]
- public async Task ConcurrentStoreResult_OnlyFirstWins()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore();
- var task = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Act - Try to store results concurrently (only first should succeed)
- var storeTasks = Enumerable.Range(0, 10).Select(i =>
- Task.Run(async () =>
- {
- try
- {
- var result = new CallToolResult { Content = [new TextContentBlock { Text = $"Result {i}" }] };
- var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions);
- await store.StoreTaskResultAsync(
- task.TaskId,
- McpTaskStatus.Completed,
- resultElement,
- null,
- TestContext.Current.CancellationToken);
- return i;
- }
- catch (InvalidOperationException)
- {
- // Expected: task already in terminal state
- return -1;
- }
- }));
-
- var results = await Task.WhenAll(storeTasks);
- var successfulUpdates = results.Where(r => r >= 0).ToList();
-
- // Assert - Exactly one update should succeed, others should fail
- Assert.Single(successfulUpdates);
-
- // Verify the winning result is stored
- var finalTask = await store.GetTaskAsync(task.TaskId, null, TestContext.Current.CancellationToken);
- Assert.Equal(McpTaskStatus.Completed, finalTask!.Status);
- }
-
- [Fact]
- public async Task ListTasksAsync_PaginationWithCustomPageSize()
- {
- // Arrange - Use small page size for testing
- using var store = new InMemoryMcpTaskStore(pageSize: 10);
-
- // Create 25 tasks
- for (int i = 0; i < 25; i++)
- {
- await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- }
-
- // Act - Paginate through all tasks
- var result1 = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
- var result2 = await store.ListTasksAsync(cursor: result1.NextCursor, cancellationToken: TestContext.Current.CancellationToken);
- var result3 = await store.ListTasksAsync(cursor: result2.NextCursor, cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert
- Assert.Equal(10, result1.Tasks.Count);
- Assert.NotNull(result1.NextCursor);
- Assert.Equal(10, result2.Tasks.Count);
- Assert.NotNull(result2.NextCursor);
- Assert.Equal(5, result3.Tasks.Count);
- Assert.Null(result3.NextCursor);
-
- // Verify no duplicates across pages
- var allTaskIds = result1.Tasks.Concat(result2.Tasks).Concat(result3.Tasks).Select(t => t.TaskId).ToList();
- Assert.Equal(25, allTaskIds.Distinct().Count());
- }
-
- [Fact]
- public async Task ListTasksAsync_NoDuplicatesWithIdenticalTimestamps()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(pageSize: 5);
-
- // Create tasks with identical metadata to increase chance of timestamp collision
- var createTasks = Enumerable.Range(0, 20).Select(i =>
- store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken));
-
- await Task.WhenAll(createTasks);
-
- // Act - Collect all tasks through pagination
- var allTasks = new List();
- string? cursor = null;
- do
- {
- var result = await store.ListTasksAsync(cursor: cursor, cancellationToken: TestContext.Current.CancellationToken);
- allTasks.AddRange(result.Tasks);
- cursor = result.NextCursor;
- } while (cursor != null);
-
- // Assert - No duplicates
- var taskIds = allTasks.Select(t => t.TaskId).ToList();
- Assert.Equal(20, taskIds.Count);
- Assert.Equal(20, taskIds.Distinct().Count());
-
- // Verify tasks are properly ordered
- Assert.Equal(allTasks.OrderBy(t => t.CreatedAt).ThenBy(t => t.TaskId).Select(t => t.TaskId), taskIds);
- }
-
- [Fact]
- public async Task ListTasksAsync_ConsistentWithExpiredTasksRemovedBetweenPages()
- {
- // Arrange - Use FakeTimeProvider for deterministic testing
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- var ttl = TimeSpan.FromSeconds(1);
- using var store = new TestInMemoryMcpTaskStore(
- defaultTtl: ttl,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: Timeout.InfiniteTimeSpan,
- pageSize: 5,
- maxTasks: null,
- maxTasksPerSession: null,
- timeProvider: fakeTime);
-
- // Create 15 tasks
- for (int i = 0; i < 15; i++)
- {
- await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- }
-
- // Act - Get first page immediately
- var result1 = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- // Advance time past TTL to make tasks expire
- fakeTime.Advance(ttl + TimeSpan.FromMilliseconds(500));
-
- // Get second page after expiration
- var result2 = await store.ListTasksAsync(cursor: result1.NextCursor, cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert - First page should have 5 tasks, second page should have 0 (all expired)
- Assert.Equal(5, result1.Tasks.Count);
- Assert.NotNull(result1.NextCursor);
- Assert.Empty(result2.Tasks);
- Assert.Null(result2.NextCursor);
- }
-
- [Fact]
- public async Task ListTasksAsync_KeysetPaginationMaintainsConsistencyWithNewTasks()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(pageSize: 5);
-
- // Create 10 initial tasks
- for (int i = 0; i < 10; i++)
- {
- await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- }
-
- // Get first page
- var result1 = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(5, result1.Tasks.Count);
-
- // Add more tasks between pages (these should appear in later queries, not retroactively in page 2)
- for (int i = 10; i < 15; i++)
- {
- await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- }
-
- // Get second page using cursor from before new tasks were added
- var result2 = await store.ListTasksAsync(cursor: result1.NextCursor, cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert - Second page should have 5 tasks from original set
- Assert.Equal(5, result2.Tasks.Count);
- Assert.NotNull(result2.NextCursor);
-
- // Verify no overlap between pages
- var page1Ids = result1.Tasks.Select(t => t.TaskId).ToHashSet();
- var page2Ids = result2.Tasks.Select(t => t.TaskId).ToHashSet();
- Assert.Empty(page1Ids.Intersect(page2Ids));
- }
-
- [Fact]
- public async Task UpdateTaskStatusAsync_ConcurrentWithList_NoCorruption()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(pageSize: 10);
-
- // Create 20 tasks
- var tasks = new List();
- for (int i = 0; i < 20; i++)
- {
- var task = await store.CreateTaskAsync(new McpTaskMetadata(), new RequestId($"req-{i}"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- tasks.Add(task);
- }
-
- // Act - Concurrently list and update tasks
- var ct = TestContext.Current.CancellationToken;
- var listTask = Task.Run(async () =>
- {
- var allTasks = new List();
- string? cursor = null;
- do
- {
- var result = await store.ListTasksAsync(cursor: cursor, cancellationToken: TestContext.Current.CancellationToken);
- allTasks.AddRange(result.Tasks);
- cursor = result.NextCursor;
- await Task.Delay(10, ct); // Small delay to increase chance of interleaving
- } while (cursor != null);
- return allTasks;
- }, ct);
-
- var updateTask = Task.Run(async () =>
- {
- foreach (var task in tasks)
- {
- await store.UpdateTaskStatusAsync(task.TaskId, McpTaskStatus.Working, "Updated", null, TestContext.Current.CancellationToken);
- await Task.Delay(5, ct); // Small delay
- }
- }, ct);
-
- await Task.WhenAll(listTask, updateTask);
- var listedTasks = await listTask;
-
- // Assert - Should have listed all tasks without duplicates or corruption
- Assert.Equal(20, listedTasks.Count);
- Assert.Equal(20, listedTasks.Select(t => t.TaskId).Distinct().Count());
- }
-
- [Fact]
- public void Constructor_ThrowsForInvalidMaxTasks()
- {
- // Assert
- Assert.Throws(() => new InMemoryMcpTaskStore(maxTasks: 0));
- Assert.Throws(() => new InMemoryMcpTaskStore(maxTasks: -1));
- }
-
- [Fact]
- public void Constructor_ThrowsForInvalidMaxTasksPerSession()
- {
- // Assert
- Assert.Throws(() => new InMemoryMcpTaskStore(maxTasksPerSession: 0));
- Assert.Throws(() => new InMemoryMcpTaskStore(maxTasksPerSession: -1));
- }
-
- [Fact]
- public async Task CreateTaskAsync_EnforcesMaxTasksLimit()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(maxTasks: 3);
- var metadata = new McpTaskMetadata();
-
- // Act - Create up to the limit
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- await store.CreateTaskAsync(metadata, new RequestId("req-3"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert - Fourth task should throw
- var ex = await Assert.ThrowsAsync(() =>
- store.CreateTaskAsync(metadata, new RequestId("req-4"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken));
- Assert.Contains("Maximum number of tasks (3) has been reached", ex.Message);
- }
-
- [Fact]
- public async Task CreateTaskAsync_EnforcesMaxTasksPerSessionLimit()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(maxTasksPerSession: 2);
- var metadata = new McpTaskMetadata();
-
- // Act - Create up to the limit for session-1
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
- await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // Assert - Third task for session-1 should throw
- var ex = await Assert.ThrowsAsync(() =>
- store.CreateTaskAsync(metadata, new RequestId("req-3"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken));
- Assert.Contains("Maximum number of tasks per session (2) has been reached", ex.Message);
- Assert.Contains("session-1", ex.Message);
- }
-
- [Fact]
- public async Task CreateTaskAsync_MaxTasksPerSession_AllowsDifferentSessions()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(maxTasksPerSession: 2);
- var metadata = new McpTaskMetadata();
-
- // Act - Create 2 tasks for session-1
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
- await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // Should still be able to create tasks for session-2
- var task3 = await store.CreateTaskAsync(metadata, new RequestId("req-3"), new JsonRpcRequest { Method = "test" }, "session-2", TestContext.Current.CancellationToken);
- var task4 = await store.CreateTaskAsync(metadata, new RequestId("req-4"), new JsonRpcRequest { Method = "test" }, "session-2", TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotNull(task3);
- Assert.NotNull(task4);
- }
-
- [Fact]
- public async Task CreateTaskAsync_MaxTasksPerSession_DoesNotApplyToNullSession()
- {
- // Arrange
- using var store = new InMemoryMcpTaskStore(maxTasksPerSession: 1);
- var metadata = new McpTaskMetadata();
-
- // Act - Create multiple tasks with null session (should not be limited)
- var task1 = await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var task2 = await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
- var task3 = await store.CreateTaskAsync(metadata, new RequestId("req-3"), new JsonRpcRequest { Method = "test" }, null, TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotNull(task1);
- Assert.NotNull(task2);
- Assert.NotNull(task3);
- }
-
- [Fact]
- public async Task CreateTaskAsync_CombinesMaxTasksAndMaxTasksPerSession()
- {
- // Arrange - Global limit of 5, per-session limit of 2
- using var store = new InMemoryMcpTaskStore(maxTasks: 5, maxTasksPerSession: 2);
- var metadata = new McpTaskMetadata();
-
- // Create 2 tasks for session-1 (hits per-session limit)
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
- await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // session-1 is at its limit
- await Assert.ThrowsAsync(() =>
- store.CreateTaskAsync(metadata, new RequestId("req-3"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken));
-
- // But session-2 can still create tasks
- await store.CreateTaskAsync(metadata, new RequestId("req-4"), new JsonRpcRequest { Method = "test" }, "session-2", TestContext.Current.CancellationToken);
- await store.CreateTaskAsync(metadata, new RequestId("req-5"), new JsonRpcRequest { Method = "test" }, "session-2", TestContext.Current.CancellationToken);
-
- // Now global limit is reached (4 tasks total, but 5th would be 5)
- // Wait, we have 4 tasks, should be able to create one more
- await store.CreateTaskAsync(metadata, new RequestId("req-6"), new JsonRpcRequest { Method = "test" }, "session-3", TestContext.Current.CancellationToken);
-
- // Now at 5 tasks (global limit), should throw
- var ex = await Assert.ThrowsAsync(() =>
- store.CreateTaskAsync(metadata, new RequestId("req-7"), new JsonRpcRequest { Method = "test" }, "session-3", TestContext.Current.CancellationToken));
- Assert.Contains("Maximum number of tasks (5) has been reached", ex.Message);
- }
-
- [Fact]
- public async Task CreateTaskAsync_MaxTasksPerSession_ExcludesExpiredTasks()
- {
- // Arrange - Use FakeTimeProvider for deterministic testing
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- var shortTtl = TimeSpan.FromMilliseconds(50);
- using var store = new TestInMemoryMcpTaskStore(
- defaultTtl: shortTtl,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: Timeout.InfiniteTimeSpan,
- pageSize: 100,
- maxTasks: null,
- maxTasksPerSession: 1,
- timeProvider: fakeTime);
-
- var metadata = new McpTaskMetadata();
-
- // Create first task
- await store.CreateTaskAsync(metadata, new RequestId("req-1"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // Advance time past TTL to make the first task expire
- fakeTime.Advance(shortTtl + TimeSpan.FromMilliseconds(1));
-
- // Should be able to create another task since the first one expired
- var task2 = await store.CreateTaskAsync(metadata, new RequestId("req-2"), new JsonRpcRequest { Method = "test" }, "session-1", TestContext.Current.CancellationToken);
-
- // Assert
- Assert.NotNull(task2);
- }
-
- [Fact]
- public async Task ListTasksAsync_KeysetPaginationWorksWithIdenticalTimestamps()
- {
- // Arrange - Use a fake time provider to create tasks with identical timestamps
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- using var store = new TestInMemoryMcpTaskStore(
- defaultTtl: null,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: Timeout.InfiniteTimeSpan,
- pageSize: 5,
- maxTasks: null,
- maxTasksPerSession: null,
- timeProvider: fakeTime);
-
- // Create 10 tasks - all with the EXACT same timestamp
- var createdTasks = new List();
- for (int i = 0; i < 10; i++)
- {
- var task = await store.CreateTaskAsync(
- new McpTaskMetadata(),
- new RequestId($"req-{i}"),
- new JsonRpcRequest { Method = "test" },
- null,
- TestContext.Current.CancellationToken);
- createdTasks.Add(task);
- }
-
- // Verify all tasks have the same CreatedAt timestamp
- var firstTimestamp = createdTasks[0].CreatedAt;
- Assert.All(createdTasks, task => Assert.Equal(firstTimestamp, task.CreatedAt));
-
- // Act - Get first page
- var result1 = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert - First page should have 5 tasks
- Assert.Equal(5, result1.Tasks.Count);
- Assert.NotNull(result1.NextCursor);
-
- // Get second page using cursor
- var result2 = await store.ListTasksAsync(cursor: result1.NextCursor, cancellationToken: TestContext.Current.CancellationToken);
-
- // Assert - Second page should have 5 tasks
- Assert.Equal(5, result2.Tasks.Count);
- Assert.Null(result2.NextCursor); // No more pages
-
- // Verify no overlap between pages
- var page1Ids = result1.Tasks.Select(t => t.TaskId).ToHashSet();
- var page2Ids = result2.Tasks.Select(t => t.TaskId).ToHashSet();
- Assert.Empty(page1Ids.Intersect(page2Ids));
-
- // Verify we got all 10 tasks exactly once
- var allReturnedIds = page1Ids.Union(page2Ids).ToHashSet();
- var allCreatedIds = createdTasks.Select(t => t.TaskId).ToHashSet();
- Assert.Equal(allCreatedIds, allReturnedIds);
- }
-
- [Fact]
- public async Task ListTasksAsync_TasksCreatedAfterFirstPageWithSameTimestampAppearInSecondPage()
- {
- // Arrange - Use a fake time provider so we can control timestamps precisely
- var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
- using var store = new TestInMemoryMcpTaskStore(
- defaultTtl: null,
- maxTtl: null,
- pollInterval: null,
- cleanupInterval: Timeout.InfiniteTimeSpan,
- pageSize: 5,
- maxTasks: null,
- maxTasksPerSession: null,
- timeProvider: fakeTime);
-
- // Create initial 6 tasks - all with the same timestamp
- // (6 so that first page has 5 and cursor points to task 5)
- var initialTasks = new List();
- for (int i = 0; i < 6; i++)
- {
- var task = await store.CreateTaskAsync(
- new McpTaskMetadata(),
- new RequestId($"req-initial-{i}"),
- new JsonRpcRequest { Method = "test" },
- null,
- TestContext.Current.CancellationToken);
- initialTasks.Add(task);
- }
-
- // Get first page - should have 5 tasks with a cursor
- var result1 = await store.ListTasksAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(5, result1.Tasks.Count);
- Assert.NotNull(result1.NextCursor);
-
- // Now create 5 more tasks AFTER we got the first page cursor
- // These tasks have the SAME timestamp as the cursor (time hasn't moved)
- // Due to monotonic UUID v7 with counter, they should sort AFTER the cursor
- var laterTasks = new List();
- for (int i = 0; i < 5; i++)
- {
- var task = await store.CreateTaskAsync(
- new McpTaskMetadata(),
- new RequestId($"req-later-{i}"),
- new JsonRpcRequest { Method = "test" },
- null,
- TestContext.Current.CancellationToken);
- laterTasks.Add(task);
- }
-
- // Verify all tasks have the same timestamp
- var allTasks = initialTasks.Concat(laterTasks).ToList();
- var firstTimestamp = allTasks[0].CreatedAt;
- Assert.All(allTasks, task => Assert.Equal(firstTimestamp, task.CreatedAt));
-
- // Get ALL remaining pages
- var allSubsequentTasks = new List();
- string? cursor = result1.NextCursor;
- while (cursor != null)
- {
- var result = await store.ListTasksAsync(cursor: cursor, cancellationToken: TestContext.Current.CancellationToken);
- allSubsequentTasks.AddRange(result.Tasks);
- cursor = result.NextCursor;
- }
-
- // Verify no overlap between first page and subsequent
- var page1Ids = result1.Tasks.Select(t => t.TaskId).ToHashSet();
- var subsequentIds = allSubsequentTasks.Select(t => t.TaskId).ToHashSet();
- Assert.Empty(page1Ids.Intersect(subsequentIds));
-
- // Verify we got all tasks
- var allReturnedIds = page1Ids.Union(subsequentIds).ToHashSet();
- var allCreatedIds = allTasks.Select(t => t.TaskId).ToHashSet();
- Assert.Equal(allCreatedIds, allReturnedIds);
-
- // Most importantly: verify ALL the later tasks (created after first page) are surfaced
- // in the subsequent pages
- var laterTaskIds = laterTasks.Select(t => t.TaskId).ToHashSet();
- Assert.Superset(laterTaskIds, subsequentIds);
- }
-}
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTaskAugmentedValidationTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTaskAugmentedValidationTests.cs
deleted file mode 100644
index 4c045cb21..000000000
--- a/tests/ModelContextProtocol.Tests/Server/McpServerTaskAugmentedValidationTests.cs
+++ /dev/null
@@ -1,1012 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using ModelContextProtocol.Client;
-using ModelContextProtocol.Protocol;
-using ModelContextProtocol.Server;
-using ModelContextProtocol.Tests.Utils;
-using System.Text.Json;
-
-namespace ModelContextProtocol.Tests.Server;
-
-///
-/// Tests for validation of task-augmented tool call requests.
-///
-public class McpServerTaskAugmentedValidationTests : LoggedTest
-{
- public McpServerTaskAugmentedValidationTests(ITestOutputHelper outputHelper)
- : base(outputHelper)
- {
- }
-
- private static IDictionary CreateArguments(string key, object? value)
- {
- return new Dictionary
- {
- [key] = JsonDocument.Parse($"\"{value}\"").RootElement.Clone()
- };
- }
-
- [Fact]
- public async Task CallToolAsTask_ThrowsError_WhenNoTaskStoreConfigured()
- {
- // Arrange - Server WITHOUT task store, but with an async tool (auto-marked as taskSupport: optional)
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- // Note: NOT configuring a task store
- builder.WithTools([McpServerTool.Create(
- async (string input, CancellationToken ct) =>
- {
- await Task.Delay(10, ct);
- return $"Result: {input}";
- },
- new McpServerToolCreateOptions
- {
- Name = "async-tool",
- Description = "An async tool"
- })]);
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- // Act & Assert - Calling with task metadata should fail
- var exception = await Assert.ThrowsAsync(async () =>
- await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "async-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- TestContext.Current.CancellationToken));
-
- Assert.Contains("not supported", exception.Message, StringComparison.OrdinalIgnoreCase);
- }
-
- [Fact]
- public async Task CallToolAsTask_ThrowsError_WhenToolHasForbiddenTaskSupport()
- {
- // Arrange - Server with task store, but tool has taskSupport: forbidden (sync tool)
- var taskStore = new InMemoryMcpTaskStore();
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- // Create a synchronous tool - which will have taskSupport: forbidden (default)
- builder.WithTools([McpServerTool.Create(
- (string input) => $"Result: {input}",
- new McpServerToolCreateOptions
- {
- Name = "sync-tool",
- Description = "A synchronous tool that does not support tasks"
- })]);
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- // Act & Assert - Calling with task metadata should fail because tool doesn't support it
- var exception = await Assert.ThrowsAsync(async () =>
- await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "sync-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- TestContext.Current.CancellationToken));
-
- Assert.Contains("does not support task-augmented execution", exception.Message, StringComparison.OrdinalIgnoreCase);
- Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
- }
-
- [Fact]
- public async Task CallToolAsTask_Succeeds_WhenToolHasOptionalTaskSupport()
- {
- // Arrange - Server with task store and async tool (auto-marked as taskSupport: optional)
- var taskStore = new InMemoryMcpTaskStore();
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- builder.WithTools([McpServerTool.Create(
- async (string input, CancellationToken ct) =>
- {
- await Task.Delay(10, ct);
- return $"Result: {input}";
- },
- new McpServerToolCreateOptions
- {
- Name = "async-tool",
- Description = "An async tool with optional task support"
- })]);
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- // Act - Calling with task metadata should succeed
- var result = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "async-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- TestContext.Current.CancellationToken);
-
- // Assert - Should return a task
- Assert.NotNull(result.Task);
- Assert.NotNull(result.Task.TaskId);
- }
-
- [Fact]
- public async Task CallToolNormally_Succeeds_WhenToolHasForbiddenTaskSupport()
- {
- // Arrange - Server with task store, but calling without task metadata
- var taskStore = new InMemoryMcpTaskStore();
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- builder.WithTools([McpServerTool.Create(
- (string input) => $"Result: {input}",
- new McpServerToolCreateOptions
- {
- Name = "sync-tool",
- Description = "A synchronous tool"
- })]);
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- // Act - Calling WITHOUT task metadata should succeed
- var result = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "sync-tool",
- Arguments = CreateArguments("input", "test"),
- },
- TestContext.Current.CancellationToken);
-
- // Assert - Should return normal result
- Assert.NotNull(result.Content);
- Assert.Null(result.Task);
- }
-
- [Fact]
- public async Task CallToolNormally_ThrowsError_WhenToolHasRequiredTaskSupport()
- {
- // Arrange - Server with task store and tool with taskSupport: required
- var taskStore = new InMemoryMcpTaskStore();
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- builder.WithTools([McpServerTool.Create(
- async (string input, CancellationToken ct) =>
- {
- await Task.Delay(100, ct);
- return $"Result: {input}";
- },
- new McpServerToolCreateOptions
- {
- Name = "required-task-tool",
- Description = "A tool that requires task-augmented execution",
- Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Required }
- })]);
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- // Act & Assert - Calling WITHOUT task metadata should fail
- var exception = await Assert.ThrowsAsync(async () =>
- await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "required-task-tool",
- Arguments = CreateArguments("input", "test"),
- },
- TestContext.Current.CancellationToken));
-
- Assert.Contains("requires task-augmented execution", exception.Message, StringComparison.OrdinalIgnoreCase);
- Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
- }
-
- [Fact]
- public async Task CallToolAsTask_Succeeds_WhenToolHasRequiredTaskSupport()
- {
- // Arrange - Server with task store and tool with taskSupport: required
- var taskStore = new InMemoryMcpTaskStore();
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options =>
- {
- options.TaskStore = taskStore;
- });
-
- builder.WithTools([McpServerTool.Create(
- async (string input, CancellationToken ct) =>
- {
- await Task.Delay(10, ct);
- return $"Result: {input}";
- },
- new McpServerToolCreateOptions
- {
- Name = "required-task-tool",
- Description = "A tool that requires task-augmented execution",
- Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Required }
- })]);
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- // Act - Calling WITH task metadata should succeed
- var result = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "required-task-tool",
- Arguments = CreateArguments("input", "test"),
- Task = new McpTaskMetadata()
- },
- TestContext.Current.CancellationToken);
-
- // Assert - Should return a task
- Assert.NotNull(result.Task);
- Assert.NotNull(result.Task.TaskId);
- }
-
- [Fact]
- public async Task CallToolAsTask_WithRequiredTaskSupport_CanResolveScopedServicesFromDI()
- {
- // Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/1430:
- // ExecuteToolAsTaskAsync fires Task.Run and returns immediately, so the request-scoped
- // IServiceProvider owned by InvokeHandlerAsync is disposed before the background task
- // calls tool.InvokeAsync. The fix creates a fresh scope inside the Task.Run body so the
- // tool can resolve DI services without hitting ObjectDisposedException.
- var taskStore = new InMemoryMcpTaskStore();
- string? capturedValue = null;
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure(options => options.TaskStore = taskStore);
-
- // Register a scoped service; resolving it through a disposed scope was the bug.
- services.AddScoped();
-
- // Register the tool via the factory pattern so that Services = sp is threaded
- // through, enabling DI parameter binding at tool-creation time.
- builder.Services.AddSingleton(sp => McpServerTool.Create(
- async (ITaskToolDiService svc, CancellationToken ct) =>
- {
- await Task.Delay(10, ct);
- capturedValue = svc.GetValue();
- return capturedValue;
- },
- new McpServerToolCreateOptions
- {
- Name = "di-required-task-tool",
- Services = sp,
- Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Required }
- }));
- });
-
- await using var client = await fixture.CreateClientAsync(TestContext.Current.CancellationToken);
-
- var result = await client.CallToolAsync(
- new CallToolRequestParams
- {
- Name = "di-required-task-tool",
- Task = new McpTaskMetadata()
- },
- TestContext.Current.CancellationToken);
-
- Assert.NotNull(result.Task);
- string taskId = result.Task.TaskId;
-
- // Poll until the background task reaches a terminal state.
- McpTask taskStatus;
- int attempts = 0;
- do
- {
- await Task.Delay(50, TestContext.Current.CancellationToken);
- taskStatus = await client.GetTaskAsync(taskId, cancellationToken: TestContext.Current.CancellationToken);
- attempts++;
- }
- while (taskStatus.Status == McpTaskStatus.Working && attempts < 50);
-
- // Without the fix, the background task would fail with ObjectDisposedException when
- // resolving ITaskToolDiService, causing the task to reach McpTaskStatus.Failed.
- Assert.Equal(McpTaskStatus.Completed, taskStatus.Status);
- Assert.Equal("hello-from-di", capturedValue);
- }
-
- [Fact]
- public async Task CallToolAsTaskAsync_WithProgress_CreatesTaskSuccessfully()
- {
- // Arrange - Server with task store and a tool that reports progress
- var taskStore = new InMemoryMcpTaskStore();
-
- await using var fixture = new ServerClientFixture(LoggerFactory, configureServer: (services, builder) =>
- {
- services.AddSingleton(taskStore);
- services.Configure