Table of Contents

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 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.

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:

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 InMemoryMcpTaskStore is a reference implementation suitable for development and single-server deployments. For production multi-server scenarios, implement IMcpTaskStore with a persistent backing store (database, Redis, etc.).

Task Store Configuration

The InMemoryMcpTaskStore constructor accepts several optional parameters:

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<T>, or ValueTask<T>:

[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 McpServerToolCreateOptions:

// 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 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:

[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 section.

Task Status Notifications

When SendTaskStatusNotifications is enabled, the server automatically sends status updates to connected clients:

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:

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 GetTaskAsync to check task status:

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:

// 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:

var tasks = await client.ListTasksAsync(cancellationToken: cancellationToken);

foreach (var task in tasks)
{
    Console.WriteLine($"{task.TaskId}: {task.Status}");
}

Cancelling Tasks

Cancel a running task:

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:

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 IMcpTaskStore with a persistent backing store:

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 McpException 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:

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 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:

[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:

// 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 demonstrates a simplified fault-tolerant approach using the file system. The FileBasedMcpTaskStore persists task state to disk, allowing tasks to survive server restarts:

// 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:

[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