599 lines
21 KiB
Markdown
599 lines
21 KiB
Markdown
---
|
|
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.
|
|
|
|
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()
|
|
.WithTools<MyTools>();
|
|
```
|
|
|
|
The <xref:ModelContextProtocol.InMemoryMcpTaskStore> is a reference implementation suitable for development and single-server deployments. For production multi-server scenarios, implement <xref:ModelContextProtocol.IMcpTaskStore> 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
|
|
);
|
|
```
|
|
|
|
### Tool Task Support
|
|
|
|
Tools automatically advertise task support when they return `Task`, `ValueTask`, `Task<T>`, or `ValueTask<T>`:
|
|
|
|
```csharp
|
|
[McpServerToolType]
|
|
public class MyTools
|
|
{
|
|
// This tool automatically supports task-augmented calls
|
|
// because it returns Task<string> (async method)
|
|
[McpServerTool, Description("Processes a large dataset")]
|
|
public static async Task<string> 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 <xref:ModelContextProtocol.Server.McpServerToolCreateOptions>:
|
|
|
|
```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 <xref:ModelContextProtocol.IMcpTaskStore> 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<McpTask> StartBackgroundJob(
|
|
[Description("Number of items to process")] int itemCount,
|
|
RequestContext<CallToolRequestParams> 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<string, JsonElement>
|
|
{
|
|
["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 <xref:ModelContextProtocol.Client.McpClient.GetTaskAsync*> 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<CallToolResult>(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 <xref:ModelContextProtocol.IMcpTaskStore> with a persistent backing store:
|
|
|
|
```csharp
|
|
public class DatabaseTaskStore : IMcpTaskStore
|
|
{
|
|
private readonly IDbConnection _db;
|
|
|
|
public DatabaseTaskStore(IDbConnection db) => _db = db;
|
|
|
|
public async Task<McpTask> 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<McpTask?> GetTaskAsync(
|
|
string taskId,
|
|
string? sessionId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// Retrieve from database with session isolation
|
|
return await _db.QuerySingleOrDefaultAsync<McpTask>(
|
|
"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 <xref:ModelContextProtocol.McpException> with these error codes:
|
|
|
|
| Error Code | Scenario |
|
|
|------------|----------|
|
|
| `InvalidParams` | Invalid or nonexistent task ID |
|
|
| `InvalidRequest` | 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 (McpException ex) when (ex.ErrorCode == McpErrorCode.InvalidParams)
|
|
{
|
|
Console.WriteLine($"Task not found: {taskId}");
|
|
}
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
<!-- TODO: Remove mlc-disable block after merging to main -->
|
|
<!-- mlc-disable -->
|
|
See the [LongRunningTasks sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/LongRunningTasks) for a complete working example demonstrating:
|
|
<!-- mlc-enable -->
|
|
|
|
- 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<McpTask> SubmitJob(
|
|
[Description("The job parameters")] string jobInput,
|
|
RequestContext<CallToolRequestParams> 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
|
|
|
|
<!-- TODO: Remove mlc-disable block after merging to main -->
|
|
<!-- mlc-disable -->
|
|
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:
|
|
<!-- mlc-enable -->
|
|
|
|
```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()
|
|
.WithTools<TaskTools>();
|
|
```
|
|
|
|
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<McpTask> SubmitJob(
|
|
[Description("A label for the job")] string jobName,
|
|
RequestContext<CallToolRequestParams> 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
|
|
|
|
- <xref:ModelContextProtocol.IMcpTaskStore>
|
|
- <xref:ModelContextProtocol.InMemoryMcpTaskStore>
|
|
- <xref:ModelContextProtocol.Protocol.McpTask>
|
|
- <xref:ModelContextProtocol.Protocol.McpTaskStatus>
|
|
- [MCP Tasks Specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)
|