Implement SEP-2663 Tasks Extension (#1579)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
ed07fdb6f4
commit
dbb7a20176
1 changed files with 8190 additions and 11405 deletions
|
|
@ -67,58 +67,33 @@ public abstract partial class McpServer : McpSession
|
|||
/// then returns to <see cref="McpTaskStatus.Working"/> when the response is received.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public async ValueTask<CreateMessageResult> SampleAsync(
|
||||
public ValueTask<CreateMessageResult> SampleAsync(
|
||||
CreateMessageRequestParams requestParams,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
// If executing inside a background task, redirect sampling through the task store.
|
||||
// Capability checks (ThrowIfSamplingUnsupported) are intentionally skipped here because the
|
||||
// client opted into the tasks extension when submitting the originating request, and input
|
||||
// requests are delivered through the tasks/get response channel rather than as direct
|
||||
// server->client requests. See SendRequestViaTaskAsync remarks.
|
||||
if (McpTaskExecutionContext.Current.Value is { } taskContext)
|
||||
{
|
||||
return SendRequestViaTaskAsync(taskContext, RequestMethods.SamplingCreateMessage, requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
ThrowIfSamplingUnsupported();
|
||||
|
||||
return await SendRequestWithTaskStatusTrackingAsync(
|
||||
return SendRequestAsync(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
|
||||
"Waiting for sampling response",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client as a task, allowing the server to poll for completion.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">The parameters for the sampling request.</param>
|
||||
/// <param name="taskMetadata">The task metadata specifying TTL and other task-related options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>An <see cref="McpTask"/> representing the created task on the client.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> or <paramref name="taskMetadata"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling or task-augmented sampling.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
/// <remarks>
|
||||
/// Use <see cref="GetTaskAsync"/> to poll for task status and <see cref="GetTaskResultAsync{TResult}"/>
|
||||
/// (with <see cref="CreateMessageResult"/>) to retrieve the final result when the task completes.
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<McpTask> SampleAsTaskAsync(
|
||||
CreateMessageRequestParams requestParams,
|
||||
McpTaskMetadata taskMetadata,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
Throw.IfNull(taskMetadata);
|
||||
ThrowIfSamplingUnsupported();
|
||||
ThrowIfTasksUnsupportedForSampling();
|
||||
|
||||
// Set the task metadata on the request
|
||||
requestParams.Task = taskMetadata;
|
||||
|
||||
var result = await SendRequestAsync(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateTaskResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Task;
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -301,6 +276,20 @@ public abstract partial class McpServer : McpSession
|
|||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
// If executing inside a background task, redirect through the task store.
|
||||
// Capability checks (ThrowIfRootsUnsupported) are intentionally skipped here because the
|
||||
// client opted into the tasks extension when submitting the originating request, and input
|
||||
// requests are delivered through the tasks/get response channel rather than as direct
|
||||
// server->client requests. See SendRequestViaTaskAsync remarks.
|
||||
if (McpTaskExecutionContext.Current.Value is { } taskContext)
|
||||
{
|
||||
return SendRequestViaTaskAsync(taskContext, RequestMethods.RootsList, requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
ThrowIfRootsUnsupported();
|
||||
|
||||
return SendRequestAsync(
|
||||
|
|
@ -339,352 +328,33 @@ public abstract partial class McpServer : McpSession
|
|||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
// If executing inside a background task, redirect elicitation through the task store.
|
||||
// Capability checks (ThrowIfElicitationUnsupported) are intentionally skipped here because
|
||||
// the client opted into the tasks extension when submitting the originating request, and
|
||||
// input requests are delivered through the tasks/get response channel rather than as
|
||||
// direct server->client requests. See SendRequestViaTaskAsync remarks.
|
||||
if (McpTaskExecutionContext.Current.Value is { } taskContext)
|
||||
{
|
||||
var taskResult = await SendRequestViaTaskAsync(taskContext, RequestMethods.ElicitationCreate, requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return taskResult ?? new ElicitResult { Action = "cancel" };
|
||||
}
|
||||
|
||||
ThrowIfElicitationUnsupported(requestParams);
|
||||
|
||||
var result = await SendRequestWithTaskStatusTrackingAsync(
|
||||
var result = await SendRequestAsync(
|
||||
RequestMethods.ElicitationCreate,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult,
|
||||
"Waiting for user input",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return ElicitResult.WithDefaults(requestParams, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests additional information from the user via the client as a task, allowing the server to poll for completion.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">The parameters for the elicitation request.</param>
|
||||
/// <param name="taskMetadata">The task metadata specifying TTL and other task-related options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>An <see cref="McpTask"/> representing the created task on the client.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> or <paramref name="taskMetadata"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support elicitation or task-augmented elicitation.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
/// <remarks>
|
||||
/// Use <see cref="GetTaskAsync"/> to poll for task status and <see cref="GetTaskResultAsync{TResult}"/>
|
||||
/// (with <see cref="ElicitResult"/>) to retrieve the final result when the task completes.
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<McpTask> ElicitAsTaskAsync(
|
||||
ElicitRequestParams requestParams,
|
||||
McpTaskMetadata taskMetadata,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
Throw.IfNull(taskMetadata);
|
||||
ThrowIfElicitationUnsupported(requestParams);
|
||||
ThrowIfTasksUnsupportedForElicitation();
|
||||
|
||||
// Set the task metadata on the request
|
||||
requestParams.Task = taskMetadata;
|
||||
|
||||
var result = await SendRequestAsync(
|
||||
RequestMethods.ElicitationCreate,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateTaskResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current state of a specific task from the client.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The unique identifier of the task to retrieve.</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>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<McpTask> GetTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(taskId);
|
||||
ThrowIfTasksUnsupported();
|
||||
|
||||
var result = await SendRequestAsync(
|
||||
RequestMethods.TasksGet,
|
||||
new GetTaskRequestParams { TaskId = taskId },
|
||||
McpJsonUtilities.JsonContext.Default.GetTaskRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetTaskResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Convert GetTaskResult to McpTask
|
||||
return new McpTask
|
||||
{
|
||||
TaskId = result.TaskId,
|
||||
Status = result.Status,
|
||||
StatusMessage = result.StatusMessage,
|
||||
CreatedAt = result.CreatedAt,
|
||||
LastUpdatedAt = result.LastUpdatedAt,
|
||||
TimeToLive = result.TimeToLive,
|
||||
PollInterval = result.PollInterval
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the result of a completed task from the client, blocking until the task reaches a terminal state.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type to deserialize the task result into.</typeparam>
|
||||
/// <param name="taskId">The unique identifier of the task whose result to retrieve.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional serializer options for deserializing the result.</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 task, deserialized into type <typeparamref name="TResult"/>.</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>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method sends a tasks/result request to the client, which will block until the task completes if it hasn't already.
|
||||
/// The client handles all polling logic internally.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For sampling tasks, use <see cref="CreateMessageResult"/> as <typeparamref name="TResult"/>.
|
||||
/// For elicitation tasks, use <see cref="ElicitResult"/> as <typeparamref name="TResult"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<TResult?> GetTaskResultAsync<TResult>(
|
||||
string taskId,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(taskId);
|
||||
ThrowIfTasksUnsupported();
|
||||
|
||||
var result = await SendRequestAsync(
|
||||
RequestMethods.TasksResult,
|
||||
new GetTaskPayloadRequestParams { TaskId = taskId },
|
||||
McpJsonUtilities.JsonContext.Default.GetTaskPayloadRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.JsonElement,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var serializerOptions = jsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
var typeInfo = serializerOptions.GetTypeInfo<TResult>();
|
||||
return result.Deserialize(typeInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of all tasks from the client.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks or task listing.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<IList<McpTask>> ListTasksAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfTasksUnsupported();
|
||||
ThrowIfTaskListingUnsupported();
|
||||
|
||||
List<McpTask>? tasks = null;
|
||||
ListTasksRequestParams requestParams = new();
|
||||
do
|
||||
{
|
||||
var taskResults = await ListTasksAsync(requestParams, cancellationToken).ConfigureAwait(false);
|
||||
if (tasks is null)
|
||||
{
|
||||
tasks = new List<McpTask>(taskResults.Tasks.Count);
|
||||
}
|
||||
|
||||
foreach (var mcpTask in taskResults.Tasks)
|
||||
{
|
||||
tasks.Add(mcpTask);
|
||||
}
|
||||
|
||||
requestParams.Cursor = taskResults.NextCursor;
|
||||
}
|
||||
while (requestParams.Cursor is not null);
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of tasks from the client.
|
||||
/// </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 client.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks or task listing.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
/// <remarks>
|
||||
/// The <see cref="ListTasksAsync(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 client.
|
||||
/// 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);
|
||||
ThrowIfTasksUnsupported();
|
||||
ThrowIfTaskListingUnsupported();
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.TasksList,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListTasksRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListTasksResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a running task on the client.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The unique identifier of the task to cancel.</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>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks or task cancellation.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</exception>
|
||||
/// <remarks>
|
||||
/// Cancelling a task requests that the client stop execution. The client may not immediately cancel the task,
|
||||
/// and may choose to allow the task to complete if it's close to finishing.
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<McpTask> CancelTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(taskId);
|
||||
ThrowIfTasksUnsupported();
|
||||
ThrowIfTaskCancellationUnsupported();
|
||||
|
||||
var result = await SendRequestAsync(
|
||||
RequestMethods.TasksCancel,
|
||||
new CancelMcpTaskRequestParams { TaskId = taskId },
|
||||
McpJsonUtilities.JsonContext.Default.CancelMcpTaskRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CancelMcpTaskResult,
|
||||
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 on the client until it reaches a terminal state.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The unique identifier of the task to poll.</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>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks.</exception>
|
||||
/// <exception cref="McpException">The request failed or the client returned an error response.</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 client to determine how long
|
||||
/// to wait between polling attempts.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For retrieving the actual result of a completed task, use <see cref="GetTaskResultAsync{TResult}"/>
|
||||
/// or <see cref="WaitForTaskResultAsync{TResult}"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<McpTask> PollTaskUntilCompleteAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(taskId);
|
||||
|
||||
McpTask task;
|
||||
do
|
||||
{
|
||||
task = await GetTaskAsync(taskId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// If task is in a terminal state, we're done
|
||||
if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait for the poll interval before checking again (default to 1 second)
|
||||
var pollInterval = task.PollInterval ?? TimeSpan.FromSeconds(1);
|
||||
await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
while (true);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a task on the client to complete and retrieves its result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type to deserialize the task result into.</typeparam>
|
||||
/// <param name="taskId">The unique identifier of the task whose result to retrieve.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional serializer options for deserializing the result.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A tuple containing the final task state and its result.</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>
|
||||
/// <exception cref="InvalidOperationException">The client does not support tasks.</exception>
|
||||
/// <exception cref="McpException">The task failed or was cancelled.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method combines <see cref="PollTaskUntilCompleteAsync"/> and <see cref="GetTaskResultAsync{TResult}"/>
|
||||
/// to provide a convenient way to wait for a task to complete and retrieve its result in a single call.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the task completes with a status of <see cref="McpTaskStatus.Failed"/> or <see cref="McpTaskStatus.Cancelled"/>,
|
||||
/// an <see cref="McpException"/> is thrown.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For sampling tasks, use <see cref="CreateMessageResult"/> as <typeparamref name="TResult"/>.
|
||||
/// For elicitation tasks, use <see cref="ElicitResult"/> as <typeparamref name="TResult"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public async ValueTask<(McpTask Task, TResult? Result)> WaitForTaskResultAsync<TResult>(
|
||||
string taskId,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(taskId);
|
||||
|
||||
// Poll until task reaches terminal state
|
||||
var task = await PollTaskUntilCompleteAsync(taskId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check for failure or cancellation
|
||||
if (task.Status == McpTaskStatus.Failed)
|
||||
{
|
||||
throw new McpException($"Task '{taskId}' failed: {task.StatusMessage ?? "Unknown error"}");
|
||||
}
|
||||
|
||||
if (task.Status == McpTaskStatus.Cancelled)
|
||||
{
|
||||
throw new McpException($"Task '{taskId}' was cancelled");
|
||||
}
|
||||
|
||||
// Retrieve the result
|
||||
var result = await GetTaskResultAsync<TResult>(taskId, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (task, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests additional information from the user via the client, constructing a request schema from the
|
||||
/// public serializable properties of <typeparamref name="T"/> and deserializing the response into <typeparamref name="T"/>.
|
||||
|
|
@ -747,6 +417,26 @@ public abstract partial class McpServer : McpSession
|
|||
return new ElicitResult<T> { Action = raw.Action, Content = typed };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a task status notification to the connected client.
|
||||
/// </summary>
|
||||
/// <param name="notificationParams">The task status notification parameters to send.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous send operation.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="notificationParams"/> is <see langword="null"/>.</exception>
|
||||
public Task SendTaskStatusNotificationAsync(
|
||||
TaskStatusNotificationParams notificationParams,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(notificationParams);
|
||||
|
||||
return SendNotificationAsync(
|
||||
NotificationMethods.TaskStatusNotification,
|
||||
notificationParams,
|
||||
McpJsonUtilities.JsonContext.Default.TaskStatusNotificationParams,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a request schema for elicitation based on the public serializable properties of <paramref name="type"/>.
|
||||
/// </summary>
|
||||
|
|
@ -896,6 +586,88 @@ public abstract partial class McpServer : McpSession
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a scope that redirects server-initiated requests (elicitation, sampling, list roots) through
|
||||
/// the task store as input requests for the duration of the scope. Use this when executing tool logic
|
||||
/// in the background as a task, so that any server-to-client requests are surfaced to the client via
|
||||
/// the task's <see cref="McpTaskStatus.InputRequired"/> state instead of direct JSON-RPC messages.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The task ID in the store.</param>
|
||||
/// <param name="store">The task store to write input requests to.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> that restores the previous context when disposed.</returns>
|
||||
[Experimental(Experimentals.Extensions_DiagnosticId, UrlFormat = Experimentals.Extensions_Url)]
|
||||
public IDisposable CreateMcpTaskScope(
|
||||
string taskId,
|
||||
IMcpTaskStore store)
|
||||
{
|
||||
Throw.IfNull(taskId);
|
||||
Throw.IfNull(store);
|
||||
|
||||
var previous = McpTaskExecutionContext.Current.Value;
|
||||
McpTaskExecutionContext.Current.Value = new McpTaskExecutionContext
|
||||
{
|
||||
TaskId = taskId,
|
||||
Store = store,
|
||||
};
|
||||
return new McpTaskExecutionContext.Scope(previous);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a server-initiated request through the task store as an input request, then awaits the response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When executing inside a task scope, capability negotiation checks (such as
|
||||
/// <see cref="ThrowIfSamplingUnsupported"/>, <see cref="ThrowIfRootsUnsupported"/>, and
|
||||
/// <see cref="ThrowIfElicitationUnsupported"/>) are intentionally skipped by the callers
|
||||
/// of this helper. The task channel itself is the negotiated capability: the client opted
|
||||
/// in to the tasks extension when it submitted the originating request, and is responsible
|
||||
/// for handling or rejecting the input requests surfaced through <c>tasks/get</c>.
|
||||
/// </remarks>
|
||||
private async ValueTask<TResponse> SendRequestViaTaskAsync<TRequest, TResponse>(
|
||||
McpTaskExecutionContext taskContext,
|
||||
string method,
|
||||
TRequest request,
|
||||
JsonTypeInfo<TRequest> requestTypeInfo,
|
||||
JsonTypeInfo<TResponse> responseTypeInfo,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var requestId = Guid.NewGuid().ToString("N");
|
||||
var paramsJson = JsonSerializer.SerializeToElement(request, requestTypeInfo);
|
||||
|
||||
var inputRequest = new InputRequest
|
||||
{
|
||||
Method = method,
|
||||
Params = paramsJson,
|
||||
};
|
||||
|
||||
var tcs = new TaskCompletionSource<InputResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
void handler(InputResponseReceivedEventArgs args)
|
||||
{
|
||||
if (args.TaskId == taskContext.TaskId && args.RequestId == requestId)
|
||||
{
|
||||
tcs.TrySetResult(args.Response);
|
||||
}
|
||||
}
|
||||
|
||||
taskContext.Store.InputResponseReceived += handler;
|
||||
try
|
||||
{
|
||||
await taskContext.Store.SetInputRequestsAsync(
|
||||
taskContext.TaskId,
|
||||
new Dictionary<string, InputRequest> { [requestId] = inputRequest },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response.Deserialize(responseTypeInfo)!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
taskContext.Store.InputResponseReceived -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfElicitationUnsupported(ElicitRequestParams request)
|
||||
{
|
||||
if (ClientCapabilities is null)
|
||||
|
|
@ -940,120 +712,6 @@ public abstract partial class McpServer : McpSession
|
|||
}
|
||||
}
|
||||
|
||||
private void ThrowIfTasksUnsupportedForSampling()
|
||||
{
|
||||
if (ClientCapabilities?.Tasks?.Requests?.Sampling?.CreateMessage is null)
|
||||
{
|
||||
if (ClientCapabilities is null)
|
||||
{
|
||||
throw new InvalidOperationException("Task-augmented sampling is not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support task-augmented sampling requests.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfTasksUnsupportedForElicitation()
|
||||
{
|
||||
if (ClientCapabilities?.Tasks?.Requests?.Elicitation?.Create is null)
|
||||
{
|
||||
if (ClientCapabilities is null)
|
||||
{
|
||||
throw new InvalidOperationException("Task-augmented elicitation is not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support task-augmented elicitation requests.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfTasksUnsupported()
|
||||
{
|
||||
if (ClientCapabilities?.Tasks is null)
|
||||
{
|
||||
if (ClientCapabilities is null)
|
||||
{
|
||||
throw new InvalidOperationException("Tasks are not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support tasks.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfTaskListingUnsupported()
|
||||
{
|
||||
if (ClientCapabilities?.Tasks?.List is null)
|
||||
{
|
||||
throw new InvalidOperationException("Client does not support task listing.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfTaskCancellationUnsupported()
|
||||
{
|
||||
if (ClientCapabilities?.Tasks?.Cancel is null)
|
||||
{
|
||||
throw new InvalidOperationException("Client does not support task cancellation.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request to the client, automatically updating task status to InputRequired during
|
||||
/// the request when called within a task execution context.
|
||||
/// </summary>
|
||||
private async ValueTask<TResult> SendRequestWithTaskStatusTrackingAsync<TParams, TResult>(
|
||||
string method,
|
||||
TParams requestParams,
|
||||
JsonTypeInfo<TParams> paramsTypeInfo,
|
||||
JsonTypeInfo<TResult> resultTypeInfo,
|
||||
string inputRequiredMessage,
|
||||
CancellationToken cancellationToken)
|
||||
where TParams : RequestParams
|
||||
where TResult : notnull
|
||||
{
|
||||
var taskContext = TaskExecutionContext.Current;
|
||||
|
||||
// If we're not in a task execution context, just send the request normally
|
||||
if (taskContext is null)
|
||||
{
|
||||
return await SendRequestAsync(method, requestParams, paramsTypeInfo, resultTypeInfo, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Update task status to InputRequired
|
||||
var inputRequiredTask = await taskContext.TaskStore.UpdateTaskStatusAsync(
|
||||
taskContext.TaskId,
|
||||
Protocol.McpTaskStatus.InputRequired,
|
||||
inputRequiredMessage,
|
||||
taskContext.SessionId,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send notification if enabled
|
||||
if (taskContext.SendNotifications && taskContext.NotifyTaskStatusFunc is not null)
|
||||
{
|
||||
_ = taskContext.NotifyTaskStatusFunc(inputRequiredTask, CancellationToken.None);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Send the actual request
|
||||
return await SendRequestAsync(method, requestParams, paramsTypeInfo, resultTypeInfo, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Update task status back to Working
|
||||
var workingTask = await taskContext.TaskStore.UpdateTaskStatusAsync(
|
||||
taskContext.TaskId,
|
||||
Protocol.McpTaskStatus.Working,
|
||||
null, // Clear status message
|
||||
taskContext.SessionId,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send notification if enabled
|
||||
if (taskContext.SendNotifications && taskContext.NotifyTaskStatusFunc is not null)
|
||||
{
|
||||
_ = taskContext.NotifyTaskStatusFunc(workingTask, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
|
||||
private sealed class SamplingChatClient(McpServer server, JsonSerializerOptions serializerOptions) : IChatClient
|
||||
{
|
||||
|
|
@ -1091,50 +749,6 @@ public abstract partial class McpServer : McpSession
|
|||
void IDisposable.Dispose() { } // nop
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a task status notification to the connected client.
|
||||
/// </summary>
|
||||
/// <param name="task">The task whose status changed.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous notification operation.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="task"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The notification failed or the client returned an error response.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method sends an optional status notification to inform the client of task state changes.
|
||||
/// According to the MCP specification, receivers MAY send this notification but are not required to.
|
||||
/// Clients must not rely on receiving these notifications and should continue polling via tasks/get.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The notification is sent using the standard <c>notifications/tasks/status</c> method and includes
|
||||
/// the full task state information.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public Task NotifyTaskStatusAsync(
|
||||
McpTask task,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(task);
|
||||
|
||||
var notificationParams = new McpTaskStatusNotificationParams
|
||||
{
|
||||
TaskId = task.TaskId,
|
||||
Status = task.Status,
|
||||
StatusMessage = task.StatusMessage,
|
||||
CreatedAt = task.CreatedAt,
|
||||
LastUpdatedAt = task.LastUpdatedAt,
|
||||
TimeToLive = task.TimeToLive,
|
||||
PollInterval = task.PollInterval
|
||||
};
|
||||
|
||||
return SendNotificationAsync(
|
||||
NotificationMethods.TaskStatusNotification,
|
||||
notificationParams,
|
||||
McpJsonUtilities.JsonContext.Default.McpTaskStatusNotificationParams,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="ILoggerProvider"/> implementation for creating loggers
|
||||
/// that send logging message notifications to the client for logged messages.
|
||||
|
|
|
|||
Loading…
Reference in a new issue