Implement MCP task support (SEP-1686) (#1170)

Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
Eirik Tsarpalis 2026-01-28 05:21:02 +02:00 • committed by GitHub
commit 10e4f5b20f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 11580 additions and 46 deletions

View file

@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
@ -912,6 +913,325 @@ public abstract partial class McpClient : McpSession
cancellationToken: cancellationToken);
}
/// <summary>
/// Invokes a tool on the server as a task for long-running operations.
/// </summary>
/// <param name="toolName">The name of the tool to call on the server.</param>
/// <param name="arguments">An optional dictionary of arguments to pass to the tool.</param>
/// <param name="taskMetadata">Metadata for task augmentation, including optional TTL. If <see langword="null"/>, an empty metadata is used.</param>
/// <param name="progress">An optional progress reporter for server notifications.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// An <see cref="McpTask"/> representing the created task. Use <see cref="GetTaskAsync"/> to poll for status updates
/// and <see cref="GetTaskResultAsync"/> to retrieve the final result.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="toolName"/> is <see langword="null"/>.</exception>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// The server must advertise task support via <c>capabilities.tasks.requests.tools.call</c> and the tool
/// must have <c>execution.taskSupport</c> set to <c>"optional"</c> or <c>"required"</c>.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public ValueTask<McpTask> CallToolAsTaskAsync(
string toolName,
IReadOnlyDictionary<string, object?>? arguments = null,
McpTaskMetadata? taskMetadata = null,
IProgress<ProgressNotificationValue>? 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<McpTask> SendTaskAugmentedCallToolRequestAsync(
string toolName,
IReadOnlyDictionary<string, object?>? 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<McpTask> SendTaskAugmentedCallToolRequestWithProgressAsync(
string toolName,
IReadOnlyDictionary<string, object?>? arguments,
McpTaskMetadata? taskMetadata,
IProgress<ProgressNotificationValue> 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 ? new(meta) : [];
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;
}
}
/// <summary>
/// Retrieves the current state of a specific task from the server.
/// </summary>
/// <param name="taskId">The unique identifier of the task to retrieve.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The current state of the task.</returns>
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public async ValueTask<McpTask> 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
};
}
/// <summary>
/// Retrieves the result of a completed task, blocking until the task reaches a terminal state.
/// </summary>
/// <param name="taskId">The unique identifier of the task whose result to retrieve.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The raw JSON result of the task.</returns>
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
/// <remarks>
/// 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.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public ValueTask<JsonElement> 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);
}
/// <summary>
/// Retrieves a list of all tasks from the server.
/// </summary>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A list of all tasks.</returns>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public async ValueTask<IList<McpTask>> ListTasksAsync(
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
ListTasksRequestParams requestParams = new() { Meta = options?.GetMetaForRequest() };
List<McpTask> 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;
}
/// <summary>
/// Retrieves a list of tasks from the server.
/// </summary>
/// <param name="requestParams">The request parameters to send in the request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The result of the request as provided by the server.</returns>
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The <see cref="ListTasksAsync(RequestOptions?, CancellationToken)"/> overload retrieves all tasks by automatically handling pagination.
/// This overload works with the lower-level <see cref="ListTasksRequestParams"/> and <see cref="ListTasksResult"/>, returning the raw result from the server.
/// Any pagination needs to be managed by the caller.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public ValueTask<ListTasksResult> ListTasksAsync(
ListTasksRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
return SendRequestAsync(
RequestMethods.TasksList,
requestParams,
McpJsonUtilities.JsonContext.Default.ListTasksRequestParams,
McpJsonUtilities.JsonContext.Default.ListTasksResult,
cancellationToken: cancellationToken);
}
/// <summary>
/// Cancels a running task on the server.
/// </summary>
/// <param name="taskId">The unique identifier of the task to cancel.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The updated state of the task after cancellation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
/// <remarks>
/// 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.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public async ValueTask<McpTask> 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
};
}
/// <summary>
/// Polls a task until it reaches a terminal status (completed, failed, or cancelled).
/// </summary>
/// <param name="taskId">The unique identifier of the task to poll.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The task in its terminal state.</returns>
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="taskId"/> is empty or composed entirely of whitespace.</exception>
/// <remarks>
/// <para>
/// This method repeatedly calls <see cref="GetTaskAsync"/> until the task reaches a terminal status.
/// It respects the <see cref="McpTask.PollInterval"/> returned by the server to determine how long
/// to wait between polling attempts.
/// </para>
/// <para>
/// For retrieving the actual result of a completed task, use <see cref="GetTaskResultAsync"/>.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public async ValueTask<McpTask> 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;
}
/// <summary>
/// Sets the logging level for the server to control which log messages are sent to the client.
/// </summary>

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol;
using System.Diagnostics.CodeAnalysis;
namespace ModelContextProtocol.Client;
@ -85,4 +86,25 @@ public class McpClientHandlers
/// </para>
/// </remarks>
public Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>>? SamplingHandler { get; set; }
/// <summary>
/// Gets or sets the handler for processing <see cref="NotificationMethods.TaskStatusNotification"/> notifications.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// The handler receives the updated <see cref="McpTask"/> object containing the current task state,
/// including its status, status message, and timestamps.
/// </para>
/// <para>
/// This handler is typically used to update UI or trigger actions based on task progress
/// without requiring explicit polling.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public Func<McpTask, CancellationToken, ValueTask>? TaskStatusHandler { get; set; }
}

View file

@ -20,6 +20,7 @@ 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 CancellationTokenSource? _connectCts;
@ -46,6 +47,12 @@ internal sealed partial class McpClientImpl : McpClient
_options = options;
_logger = loggerFactory?.CreateLogger<McpClient>() ?? NullLogger<McpClient>.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();
@ -62,22 +69,89 @@ internal sealed partial class McpClientImpl : McpClient
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)
{
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);
// 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);
}
_options.Capabilities ??= new();
_options.Capabilities.Sampling ??= new();
@ -97,11 +171,45 @@ internal sealed partial class McpClientImpl : McpClient
if (elicitationHandler is not null)
{
requestHandlers.Set(
RequestMethods.ElicitationCreate,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);
// 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);
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);
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,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);
}
_options.Capabilities ??= new();
_options.Capabilities.Elicitation ??= new();
@ -112,6 +220,276 @@ internal sealed partial class McpClientImpl : McpClient
_options.Capabilities.Elicitation.Form = new();
}
}
// Register task handlers if a task store is configured
if (taskStore is not null)
{
RegisterTaskHandlers(requestHandlers, taskStore);
}
}
/// <summary>
/// Executes an operation as a task, creating the task immediately and running the operation asynchronously.
/// </summary>
private async ValueTask<JsonElement> ExecuteAsTaskAsync(
IMcpTaskStore taskStore,
McpTaskMetadata taskMetadata,
JsonRpcRequest jsonRpcRequest,
Func<CancellationToken, Task<JsonElement>> 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);
}
/// <summary>
/// Sends a task status notification to the connected server.
/// </summary>
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);
}
/// <summary>
/// Registers handlers for task-related requests from the server.
/// </summary>
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();
}
}
/// <inheritdoc/>
@ -263,6 +641,7 @@ internal sealed partial class McpClientImpl : McpClient
_disposed = true;
_taskCancellationTokenProvider?.Dispose();
await _sessionHandler.DisposeAsync().ConfigureAwait(false);
await _transport.DisposeAsync().ConfigureAwait(false);
}

View file

@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol;
using System.Diagnostics.CodeAnalysis;
namespace ModelContextProtocol.Client;
@ -78,4 +79,36 @@ public sealed class McpClientOptions
field = value;
}
}
/// <summary>
/// Gets or sets the task store for managing client-side tasks.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// If not set, task-augmented requests will not be supported, and the client will not advertise
/// task capabilities to the server.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public IMcpTaskStore? TaskStore { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the client should send task status notifications to the server.
/// </summary>
/// <value>
/// <see langword="true"/> to send task status notifications; <see langword="false"/> otherwise.
/// The default is <see langword="true"/>.
/// </value>
/// <remarks>
/// When enabled and a <see cref="TaskStore"/> is configured, the client will send optional
/// <c>notifications/tasks/status</c> notifications to inform the server of task state changes.
/// Servers MUST NOT rely on receiving these notifications and should continue polling via <c>tasks/get</c>.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public bool SendTaskStatusNotifications { get; set; } = true;
}

View file

@ -173,7 +173,7 @@ public sealed class McpClientTool : AIFunction
/// The base <see cref="AIFunction.InvokeAsync"/> method is overridden to invoke this <see cref="CallAsync"/> method.
/// The only difference in behavior is that <see cref="AIFunction.InvokeAsync"/> serializes the resulting <see cref="CallToolResult"/>"/>
/// such that the <see cref="object"/> returned is a <see cref="JsonElement"/> containing the serialized <see cref="CallToolResult"/>.
/// This <see cref="CallToolResult"/> method is intended to be called directly by user code, whereas the base <see cref="AIFunction.InvokeAsync"/>
/// This <see cref="CallAsync"/> method is intended to be called directly by user code, whereas the base <see cref="AIFunction.InvokeAsync"/>
/// is intended to be used polymorphically via the base class, typically as part of an <see cref="IChatClient"/> operation.
/// </remarks>
/// <exception cref="McpException">The server could not find the requested tool, or the server encountered an error while processing the request.</exception>

View file

@ -108,10 +108,12 @@ public static partial class McpJsonUtilities
[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))]
@ -142,6 +144,22 @@ public static partial class McpJsonUtilities
[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))]
@ -161,6 +179,7 @@ public static partial class McpJsonUtilities
// Other MCP Types
[JsonSerializable(typeof(IReadOnlyDictionary<string, object>))]
[JsonSerializable(typeof(ProgressToken))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(ProtectedResourceMetadata))]
[JsonSerializable(typeof(AuthorizationServerMetadata))]

View file

@ -0,0 +1,127 @@
using System.Collections.Concurrent;
namespace ModelContextProtocol;
/// <summary>
/// Provides cancellation tokens for running MCP tasks, enabling TTL-based
/// automatic cancellation and explicit task cancellation.
/// </summary>
/// <remarks>
/// <para>
/// This class provides lifecycle management for <see cref="CancellationTokenSource"/> instances
/// associated with running tasks. Each task gets its own CTS that can be:
/// </para>
/// <list type="bullet">
/// <item><description>Automatically cancelled when the task's TTL expires</description></item>
/// <item><description>Explicitly cancelled via the <see cref="Cancel"/> method</description></item>
/// <item><description>Cleaned up when the task completes via <see cref="Complete"/></description></item>
/// </list>
/// <para>
/// Both <c>McpClient</c> and <c>McpServer</c> use this class to manage task cancellation
/// independently of request cancellation tokens.
/// </para>
/// </remarks>
internal sealed class McpTaskCancellationTokenProvider : IDisposable
{
private readonly ConcurrentDictionary<string, CancellationTokenSource> _runningTasks = new();
private bool _disposed;
/// <summary>
/// Registers a new task and returns a cancellation token for use during execution.
/// </summary>
/// <param name="taskId">The unique identifier of the task.</param>
/// <param name="timeToLive">
/// Optional TTL duration. If specified, the returned token will be automatically
/// cancelled when the TTL expires.
/// </param>
/// <returns>
/// A <see cref="CancellationToken"/> that will be cancelled when the TTL expires,
/// when <see cref="Cancel"/> is called, or when this provider is disposed.
/// </returns>
/// <exception cref="ObjectDisposedException">The provider has been disposed.</exception>
/// <exception cref="InvalidOperationException">A task with the same ID is already registered.</exception>
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;
}
/// <summary>
/// Attempts to cancel a running task.
/// </summary>
/// <param name="taskId">The unique identifier of the task to cancel.</param>
/// <remarks>
/// This method signals cancellation but does not remove the task from tracking.
/// The task executor should call <see cref="Complete"/> when it observes
/// the cancellation and finishes cleanup.
/// </remarks>
public void Cancel(string taskId)
{
if (_runningTasks.TryGetValue(taskId, out var cts))
{
cts.Cancel();
}
}
/// <summary>
/// Marks a task as complete and releases its associated resources.
/// </summary>
/// <param name="taskId">The unique identifier of the task that has completed.</param>
/// <remarks>
/// This method should be called from a <c>finally</c> block in the task execution
/// to ensure proper cleanup regardless of success, failure, or cancellation.
/// </remarks>
public void Complete(string taskId)
{
if (_runningTasks.TryRemove(taskId, out var cts))
{
cts.Dispose();
}
}
/// <summary>
/// Cancels all running tasks and releases all resources.
/// </summary>
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();
}
}

View file

@ -8,6 +8,8 @@
<Description>Core .NET SDK for the Model Context Protocol (MCP)</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<!-- Suppress the experimental tasks warning -->
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">

View file

@ -25,4 +25,14 @@ public sealed class CallToolRequestParams : RequestParams
/// </remarks>
[JsonPropertyName("arguments")]
public IDictionary<string, JsonElement>? Arguments { get; set; }
/// <summary>
/// Gets or sets optional task metadata to augment this request with task execution.
/// </summary>
/// <remarks>
/// When present, indicates that the requestor wants this operation executed as a task.
/// The receiver must support task augmentation for this specific request type.
/// </remarks>
[JsonPropertyName("task")]
public McpTaskMetadata? Task { get; set; }
}

View file

@ -56,4 +56,15 @@ public sealed class CallToolResult : Result
/// </remarks>
[JsonPropertyName("isError")]
public bool? IsError { get; set; }
/// <summary>
/// Gets or sets the task data for the newly created task.
/// </summary>
/// <remarks>
/// This property is populated only for task-augmented tool calls. When present, the other properties
/// (<see cref="Content"/>, <see cref="StructuredContent"/>, <see cref="IsError"/>) may not be populated.
/// The actual tool result can be retrieved later via <c>tasks/result</c>.
/// </remarks>
[JsonPropertyName("task")]
public McpTask? Task { get; set; }
}

View file

@ -0,0 +1,84 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the parameters for a tasks/cancel request to explicitly cancel a task.
/// </summary>
/// <remarks>
/// <para>
/// Receivers must reject cancellation requests for tasks already in a terminal status
/// (<see cref="McpTaskStatus.Completed"/>, <see cref="McpTaskStatus.Failed"/>, or
/// <see cref="McpTaskStatus.Cancelled"/>) with error code -32602 (Invalid params).
/// </para>
/// <para>
/// Upon receiving a valid cancellation request, receivers should attempt to stop the task
/// execution and must transition the task to <see cref="McpTaskStatus.Cancelled"/> status
/// before sending the response.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class CancelMcpTaskRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the unique identifier of the task to cancel.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
}
/// <summary>
/// Represents the result of a tasks/cancel request.
/// </summary>
/// <remarks>
/// The result contains the updated task state after cancellation. The task will be in
/// <see cref="McpTaskStatus.Cancelled"/> status if the cancellation was successful.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class CancelMcpTaskResult : Result
{
/// <summary>
/// Gets or sets the task ID.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
/// <summary>
/// Gets or sets the current status of the task (should be <see cref="McpTaskStatus.Cancelled"/>).
/// </summary>
[JsonPropertyName("status")]
public required McpTaskStatus Status { get; set; }
/// <summary>
/// Gets or sets an optional message describing the cancellation.
/// </summary>
[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; set; }
/// <summary>
/// Gets or sets the ISO 8601 timestamp when the task was created.
/// </summary>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets or sets the ISO 8601 timestamp when the task status was last updated.
/// </summary>
[JsonPropertyName("lastUpdatedAt")]
public required DateTimeOffset LastUpdatedAt { get; set; }
/// <summary>
/// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
/// </summary>
[JsonPropertyName("ttl")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }
/// <summary>
/// Gets or sets the suggested time between status checks.
/// </summary>
[JsonPropertyName("pollInterval")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? PollInterval { get; set; }
}

View file

@ -66,4 +66,20 @@ public sealed class ClientCapabilities
/// </summary>
[JsonPropertyName("elicitation")]
public ElicitationCapability? Elicitation { get; set; }
/// <summary>
/// Gets or sets the client's tasks capability for supporting task-augmented requests.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// See <see cref="McpTasksCapability"/> for details on configuring which operations support tasks.
/// </para>
/// </remarks>
[JsonPropertyName("tasks")]
public McpTasksCapability? Tasks { get; set; }
}

View file

@ -120,4 +120,14 @@ public sealed class CreateMessageRequestParams : RequestParams
/// </summary>
[JsonPropertyName("toolChoice")]
public ToolChoice? ToolChoice { get; set; }
/// <summary>
/// Gets or sets optional task metadata to augment this request with task execution.
/// </summary>
/// <remarks>
/// When present, indicates that the requestor wants this operation executed as a task.
/// The receiver must support task augmentation for this specific request type.
/// </remarks>
[JsonPropertyName("task")]
public McpTaskMetadata? Task { get; set; }
}

View file

@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the response to a task-augmented request.
/// </summary>
/// <remarks>
/// <para>
/// When a client sends a request with a <c>task</c> parameter, the server immediately returns
/// a <see cref="CreateTaskResult"/> containing the created task information instead of the
/// normal result type. The actual result can be retrieved later via <c>tasks/result</c>.
/// </para>
/// <para>
/// This type is returned for any task-augmented request including <c>tools/call</c>,
/// <c>sampling/createMessage</c>, and <c>elicitation/create</c>.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class CreateTaskResult : Result
{
/// <summary>
/// Gets or sets the task data for the newly created task.
/// </summary>
[JsonPropertyName("task")]
public McpTask Task { get; set; } = null!;
}

View file

@ -92,6 +92,16 @@ public sealed class ElicitRequestParams : RequestParams
[JsonPropertyName("requestedSchema")]
public RequestSchema? RequestedSchema { get; set; }
/// <summary>
/// Gets or sets optional task metadata to augment this request with task execution.
/// </summary>
/// <remarks>
/// When present, indicates that the requestor wants this operation executed as a task.
/// The receiver must support task augmentation for this specific request type.
/// </remarks>
[JsonPropertyName("task")]
public McpTaskMetadata? Task { get; set; }
/// <summary>Represents a request schema used in a form mode elicitation request.</summary>
public class RequestSchema
{

View file

@ -0,0 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the parameters for a tasks/result request to retrieve the result of a completed task.
/// </summary>
/// <remarks>
/// <para>
/// This request blocks until the task reaches a terminal status (<see cref="McpTaskStatus.Completed"/>,
/// <see cref="McpTaskStatus.Failed"/>, or <see cref="McpTaskStatus.Cancelled"/>).
/// </para>
/// <para>
/// The result structure matches the original request type (e.g., <see cref="CallToolResult"/> for tools/call).
/// This is distinct from the initial <see cref="Result"/> response, which contains only task data.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class GetTaskPayloadRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the unique identifier of the task whose result to retrieve.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
}

View file

@ -0,0 +1,77 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the parameters for a tasks/get request to retrieve task status.
/// </summary>
/// <remarks>
/// Requestors poll for task completion by sending tasks/get requests. They should
/// respect the <see cref="McpTask.PollInterval"/> provided in responses when determining
/// polling frequency.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class GetTaskRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the unique identifier of the task to retrieve.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
}
/// <summary>
/// Represents the result of a tasks/get request.
/// </summary>
/// <remarks>
/// The result contains the current state of the task, including its status, timestamps,
/// and any status message.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class GetTaskResult : Result
{
/// <summary>
/// Gets or sets the task ID.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
/// <summary>
/// Gets or sets the current status of the task.
/// </summary>
[JsonPropertyName("status")]
public required McpTaskStatus Status { get; set; }
/// <summary>
/// Gets or sets an optional human-readable message describing the current state.
/// </summary>
[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; set; }
/// <summary>
/// Gets or sets the ISO 8601 timestamp when the task was created.
/// </summary>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets or sets the ISO 8601 timestamp when the task status was last updated.
/// </summary>
[JsonPropertyName("lastUpdatedAt")]
public required DateTimeOffset LastUpdatedAt { get; set; }
/// <summary>
/// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
/// </summary>
[JsonPropertyName("ttl")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }
/// <summary>
/// Gets or sets the suggested time between status checks.
/// </summary>
[JsonPropertyName("pollInterval")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? PollInterval { get; set; }
}

View file

@ -0,0 +1,34 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the parameters for a tasks/list request to retrieve a list of tasks.
/// </summary>
/// <remarks>
/// This operation supports cursor-based pagination. Receivers should use cursor-based
/// pagination to limit the number of tasks returned in a single response.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class ListTasksRequestParams : PaginatedRequestParams
{
// Inherits Cursor property from PaginatedRequestParams
}
/// <summary>
/// Represents the result of a tasks/list request.
/// </summary>
/// <remarks>
/// The result contains an array of task objects and an optional cursor for pagination.
/// If <see cref="PaginatedResult.NextCursor"/> is present, more tasks are available.
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class ListTasksResult : PaginatedResult
{
/// <summary>
/// Gets or sets the list of tasks.
/// </summary>
[JsonPropertyName("tasks")]
public required McpTask[] Tasks { get; set; }
}

View file

@ -0,0 +1,104 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents an MCP task, which is a durable state machine carrying information
/// about the underlying execution state of a request.
/// </summary>
/// <remarks>
/// <para>
/// Tasks are useful for representing expensive computations and batch processing requests.
/// Each task is uniquely identifiable by a receiver-generated task ID.
/// </para>
/// <para>
/// Tasks follow a defined lifecycle through the <see cref="Status"/> property. They begin
/// in the <see cref="McpTaskStatus.Working"/> status and may transition through various states
/// before reaching a terminal status (<see cref="McpTaskStatus.Completed"/>, <see cref="McpTaskStatus.Failed"/>,
/// or <see cref="McpTaskStatus.Cancelled"/>).
/// </para>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/utilities/tasks.mdx">tasks specification</see> for details.
/// </para>
/// </remarks>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class McpTask
{
/// <summary>
/// Gets or sets the unique identifier for the task.
/// </summary>
/// <remarks>
/// Task IDs are generated by the receiver when creating a task and must be unique
/// among all tasks controlled by that receiver.
/// </remarks>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
/// <summary>
/// Gets or sets the current state of the task execution.
/// </summary>
[JsonPropertyName("status")]
public required McpTaskStatus Status { get; set; }
/// <summary>
/// Gets or sets an optional human-readable message describing the current state.
/// </summary>
/// <remarks>
/// This message can be present for any status, including error details for failed tasks.
/// </remarks>
[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; set; }
/// <summary>
/// Gets or sets the ISO 8601 timestamp when the task was created.
/// </summary>
/// <remarks>
/// Receivers must include this timestamp in all task responses to indicate when
/// the task was created.
/// </remarks>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// Gets or sets the ISO 8601 timestamp when the task status was last updated.
/// </summary>
/// <remarks>
/// Receivers must include this timestamp in all task responses to indicate when
/// the task was last updated.
/// </remarks>
[JsonPropertyName("lastUpdatedAt")]
public required DateTimeOffset LastUpdatedAt { get; set; }
/// <summary>
/// Gets or sets the time to live (retention duration) from creation before the task may be deleted.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// Receivers may override the requested TTL duration and must include the actual TTL
/// duration (or null for unlimited) in task responses.
/// </para>
/// </remarks>
[JsonPropertyName("ttl")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }
/// <summary>
/// Gets or sets the suggested time between status checks.
/// </summary>
/// <remarks>
/// Requestors should respect this value when provided to avoid excessive polling.
/// This value is optional and may not be present in all task responses.
/// </remarks>
[JsonPropertyName("pollInterval")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? PollInterval { get; set; }
private string DebuggerDisplay => $"Task {TaskId}: {Status}" + (StatusMessage != null ? $" - {StatusMessage}" : "");
}

View file

@ -0,0 +1,41 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents metadata for augmenting a request with task execution.
/// </summary>
/// <remarks>
/// <para>
/// 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 <see cref="Result"/> containing task data
/// instead of the actual operation result.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
public sealed class McpTaskMetadata
{
/// <summary>
/// Gets or sets the requested time to live (retention duration) to retain the task from creation.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// A null value indicates no specific retention requirement. The actual TTL used by the
/// receiver will be returned in the <see cref="McpTask.TimeToLive"/> property.
/// </para>
/// </remarks>
[JsonPropertyName("ttl")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }
}

Some files were not shown because too many files have changed in this diff Show more