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
54 changed files with 8190 additions and 11405 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -887,6 +887,13 @@ public abstract partial class McpClient : McpSession
|
|||
/// <returns>The <see cref="CallToolResult"/> from the tool execution.</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>
|
||||
/// This overload supports the tasks extension transparently. If the server responds with a
|
||||
/// task handle rather than an immediate result, this method polls <c>tasks/get</c> until the
|
||||
/// task completes, dispatching any <see cref="McpTaskStatus.InputRequired"/> entries through
|
||||
/// the client's registered sampling and elicitation handlers along the way. Use
|
||||
/// <see cref="CallToolRawAsync"/> to disable automatic polling.
|
||||
/// </remarks>
|
||||
public ValueTask<CallToolResult> CallToolAsync(
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
|
|
@ -957,342 +964,216 @@ public abstract partial class McpClient : McpSession
|
|||
/// <returns>The result of the request.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
public ValueTask<CallToolResult> CallToolAsync(
|
||||
/// <remarks>
|
||||
/// This method automatically includes the <c>io.modelcontextprotocol/tasks</c> extension capability
|
||||
/// in the request metadata. If the server returns a task handle instead of an immediate result,
|
||||
/// this method transparently polls <c>tasks/get</c> until the task completes, fails, or is cancelled.
|
||||
/// Use <see cref="CallToolRawAsync"/>
|
||||
/// to receive the raw <see cref="ResultOrCreatedTask{TResult}"/> without automatic polling.
|
||||
/// </remarks>
|
||||
public async ValueTask<CallToolResult> CallToolAsync(
|
||||
CallToolRequestParams requestParams,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.ToolsCall,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult,
|
||||
cancellationToken: cancellationToken);
|
||||
var augmented = await CallToolRawAsync(requestParams, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!augmented.IsTask)
|
||||
{
|
||||
return augmented.Result!;
|
||||
}
|
||||
|
||||
return await PollTaskToCompletionAsync(augmented.TaskCreated!, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a tool on the server as a task for long-running operations.
|
||||
/// Polls a task until it reaches a terminal state and returns the final <see cref="CallToolResult"/>.
|
||||
/// </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)
|
||||
private async ValueTask<CallToolResult> PollTaskToCompletionAsync(
|
||||
CreateTaskResult taskCreated,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Throw.IfNull(toolName);
|
||||
// If the server claims InputRequired but never publishes new input requests after we have
|
||||
// already responded to everything it asked for, treat that as a stuck task. The client
|
||||
// can still cancel earlier via cancellationToken; this guard prevents an unbounded poll
|
||||
// loop when the server is misbehaving. The threshold is configurable via
|
||||
// McpClientOptions.MaxConsecutiveStuckPolls.
|
||||
int maxConsecutiveStuckPolls = MaxConsecutiveStuckPolls;
|
||||
|
||||
var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
string taskId = taskCreated.TaskId;
|
||||
long pollIntervalMs = taskCreated.PollIntervalMs ?? 1000;
|
||||
HashSet<string>? resolvedRequestKeys = null;
|
||||
bool isFirstPoll = true;
|
||||
int consecutiveStuckPolls = 0;
|
||||
|
||||
if (progress is null)
|
||||
while (true)
|
||||
{
|
||||
return SendTaskAugmentedCallToolRequestAsync(toolName, arguments, taskMetadata, options?.GetMetaForRequest(), serializerOptions, cancellationToken);
|
||||
}
|
||||
// Skip the delay before the first poll: many tasks complete almost immediately and we
|
||||
// don't want to pay the poll interval as gratuitous latency.
|
||||
if (!isFirstPoll)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(pollIntervalMs), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
isFirstPoll = false;
|
||||
|
||||
return SendTaskAugmentedCallToolRequestWithProgressAsync(toolName, arguments, taskMetadata, progress, options?.GetMetaForRequest(), serializerOptions, cancellationToken);
|
||||
var taskResult = await GetTaskAsync(taskId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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);
|
||||
// Update poll interval if the server changed it.
|
||||
if (taskResult.PollIntervalMs is { } newInterval)
|
||||
{
|
||||
pollIntervalMs = newInterval;
|
||||
}
|
||||
|
||||
return result.Task;
|
||||
}
|
||||
switch (taskResult)
|
||||
{
|
||||
case CompletedTaskResult completed:
|
||||
return JsonSerializer.Deserialize(completed.Result, McpJsonUtilities.JsonContext.Default.CallToolResult)
|
||||
?? throw new JsonException("Failed to deserialize CallToolResult from completed 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"));
|
||||
case FailedTaskResult failed:
|
||||
throw new McpException($"Task '{taskId}' failed: {failed.Error}");
|
||||
|
||||
await using var _ = RegisterNotificationHandler(NotificationMethods.ProgressNotification,
|
||||
(notification, cancellationToken) =>
|
||||
{
|
||||
if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn &&
|
||||
pn.ProgressToken == progressToken)
|
||||
case CancelledTaskResult:
|
||||
throw new OperationCanceledException($"Task '{taskId}' was cancelled by the server.");
|
||||
|
||||
case InputRequiredTaskResult inputRequired:
|
||||
// Dedup: only resolve input requests we haven't already responded to.
|
||||
var newRequests = new Dictionary<string, InputRequest>();
|
||||
if (inputRequired.InputRequests is { } incomingRequests)
|
||||
{
|
||||
progress.Report(pn.Progress);
|
||||
foreach (var kvp in incomingRequests)
|
||||
{
|
||||
if (resolvedRequestKeys is null || !resolvedRequestKeys.Contains(kvp.Key))
|
||||
{
|
||||
newRequests[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}).ConfigureAwait(false);
|
||||
if (newRequests.Count > 0)
|
||||
{
|
||||
consecutiveStuckPolls = 0;
|
||||
|
||||
JsonObject metaWithProgress = meta is not null ? (JsonObject)meta.DeepClone() : [];
|
||||
metaWithProgress["progressToken"] = progressToken.ToString();
|
||||
IDictionary<string, InputResponse> inputResponses;
|
||||
try
|
||||
{
|
||||
inputResponses = await ResolveInputRequestsAsync(newRequests, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The input handler failed (e.g., ElicitationHandler threw or no handler was registered).
|
||||
// Best-effort cancel of the server-side task so it doesn't stay stuck in InputRequired
|
||||
// until TTL expires.
|
||||
try
|
||||
{
|
||||
await CancelTaskAsync(taskId, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow secondary failures; we're already propagating the original exception.
|
||||
}
|
||||
|
||||
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);
|
||||
throw;
|
||||
}
|
||||
|
||||
return result.Task;
|
||||
await UpdateTaskAsync(new UpdateTaskRequestParams
|
||||
{
|
||||
TaskId = taskId,
|
||||
InputResponses = inputResponses,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
resolvedRequestKeys ??= new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var key in inputResponses.Keys)
|
||||
{
|
||||
resolvedRequestKeys.Add(key);
|
||||
}
|
||||
}
|
||||
else if (++consecutiveStuckPolls >= maxConsecutiveStuckPolls)
|
||||
{
|
||||
// Best-effort cancel of the server-side task so it doesn't leak until TTL expires.
|
||||
try
|
||||
{
|
||||
await CancelTaskAsync(taskId, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow secondary failures; we're already propagating an exception.
|
||||
}
|
||||
|
||||
throw new McpException(
|
||||
$"Task '{taskId}' has remained in '{McpTaskStatus.InputRequired}' for {maxConsecutiveStuckPolls} consecutive polls " +
|
||||
"without publishing new input requests after all previously requested inputs were resolved.");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case WorkingTaskResult:
|
||||
// Continue polling.
|
||||
consecutiveStuckPolls = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new McpException(
|
||||
$"Unexpected task result type '{taskResult.GetType().Name}' for task '{taskId}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current state of a specific task from the server.
|
||||
/// Invokes a tool on the server with task extension support, returning the raw response
|
||||
/// without automatic polling. The caller is responsible for handling task lifecycle.
|
||||
/// </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="requestParams">The request parameters to send. The tasks extension capability will be injected into the request metadata.</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="McpException">The request failed or the server returned an error response.</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>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</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>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
[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>
|
||||
/// <returns>A <see cref="ResultOrCreatedTask{TResult}"/> that is either an immediate result or a task handle.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</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.
|
||||
/// <para>
|
||||
/// Unlike <see cref="CallToolAsync(CallToolRequestParams, CancellationToken)"/>, this method does not
|
||||
/// automatically poll for task completion. If the server returns a <see cref="CreateTaskResult"/>,
|
||||
/// the caller must manage polling via <see cref="GetTaskAsync(string, CancellationToken)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public ValueTask<ListTasksResult> ListTasksAsync(
|
||||
ListTasksRequestParams requestParams,
|
||||
public async ValueTask<ResultOrCreatedTask<CallToolResult>> CallToolRawAsync(
|
||||
CallToolRequestParams 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>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</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
|
||||
var paramsWithMeta = new CallToolRequestParams
|
||||
{
|
||||
TaskId = result.TaskId,
|
||||
Status = result.Status,
|
||||
StatusMessage = result.StatusMessage,
|
||||
CreatedAt = result.CreatedAt,
|
||||
LastUpdatedAt = result.LastUpdatedAt,
|
||||
TimeToLive = result.TimeToLive,
|
||||
PollInterval = result.PollInterval
|
||||
Name = requestParams.Name,
|
||||
Arguments = requestParams.Arguments,
|
||||
Meta = GetMetaWithTaskCapability(requestParams.Meta),
|
||||
};
|
||||
}
|
||||
|
||||
/// <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
|
||||
JsonRpcRequest jsonRpcRequest = new()
|
||||
{
|
||||
task = await GetTaskAsync(taskId, options, cancellationToken).ConfigureAwait(false);
|
||||
Method = RequestMethods.ToolsCall,
|
||||
Params = JsonSerializer.SerializeToNode(paramsWithMeta, McpJsonUtilities.JsonContext.Default.CallToolRequestParams),
|
||||
};
|
||||
|
||||
// If task is in a terminal state, we're done
|
||||
if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
JsonRpcResponse response = await SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 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);
|
||||
// Discriminate based on resultType field.
|
||||
if (response.Result is JsonObject resultObj &&
|
||||
resultObj.TryGetPropertyValue("resultType", out var resultTypeNode) &&
|
||||
resultTypeNode?.GetValue<string>() == "task")
|
||||
{
|
||||
var taskCreated = resultObj.Deserialize(McpJsonUtilities.JsonContext.Default.CreateTaskResult)
|
||||
?? throw new JsonException("Failed to deserialize CreateTaskResult from response.");
|
||||
return new ResultOrCreatedTask<CallToolResult>(taskCreated);
|
||||
}
|
||||
while (true);
|
||||
|
||||
return task;
|
||||
var callToolResult = JsonSerializer.Deserialize(response.Result, McpJsonUtilities.JsonContext.Default.CallToolResult)
|
||||
?? throw new JsonException("Failed to deserialize CallToolResult from response.");
|
||||
return new ResultOrCreatedTask<CallToolResult>(callToolResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1347,6 +1228,106 @@ public abstract partial class McpClient : McpSession
|
|||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current state of a task from the server.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The stable 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>A <see cref="GetTaskResult"/> subtype representing the current task state.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
public ValueTask<GetTaskResult> GetTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(taskId);
|
||||
|
||||
return GetTaskAsync(new GetTaskRequestParams { TaskId = taskId }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current state of a task 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>A <see cref="GetTaskResult"/> subtype representing the current task state.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
public ValueTask<GetTaskResult> GetTaskAsync(
|
||||
GetTaskRequestParams requestParams,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.TasksGet,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetTaskRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetTaskResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides input responses to a task that is in the <see cref="McpTaskStatus.InputRequired"/> state.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">The request parameters containing the task ID and input responses.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The result acknowledging the update.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
public ValueTask<UpdateTaskResult> UpdateTaskAsync(
|
||||
UpdateTaskRequestParams requestParams,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.TasksUpdate,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.UpdateTaskRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.UpdateTaskResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests cancellation of an in-progress task on the server.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The stable 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 result acknowledging the cancellation request.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="taskId"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
public ValueTask<CancelTaskResult> CancelTaskAsync(
|
||||
string taskId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(taskId);
|
||||
|
||||
return CancelTaskAsync(new CancelTaskRequestParams { TaskId = taskId }, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests cancellation of an in-progress task on 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 acknowledging the cancellation request.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
|
||||
public ValueTask<CancelTaskResult> CancelTaskAsync(
|
||||
CancelTaskRequestParams requestParams,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.TasksCancel,
|
||||
requestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CancelTaskRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CancelTaskResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Converts a dictionary with <see cref="object"/> values to a dictionary with <see cref="JsonElement"/> values.</summary>
|
||||
private static Dictionary<string, JsonElement>? ToArgumentsDictionary(
|
||||
IReadOnlyDictionary<string, object?>? arguments, JsonSerializerOptions options)
|
||||
|
|
@ -1365,4 +1346,33 @@ public abstract partial class McpClient : McpSession
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Per SEP-2663 §51, the per-request opt-in uses the SEP-2575 capabilities envelope:
|
||||
// _meta/io.modelcontextprotocol/clientCapabilities/extensions/io.modelcontextprotocol/tasks = {}
|
||||
// TODO: replace the literal with a shared NotificationMethods.ClientCapabilitiesMetaKey once
|
||||
// the SEP-2575 plumbing lands and drop the local consts.
|
||||
private const string ClientCapabilitiesMetaKey = "io.modelcontextprotocol/clientCapabilities";
|
||||
private const string ExtensionsKey = "extensions";
|
||||
|
||||
private static JsonObject GetMetaWithTaskCapability(JsonObject? existingMeta)
|
||||
{
|
||||
JsonObject meta = existingMeta is not null
|
||||
? (JsonObject)existingMeta.DeepClone()
|
||||
: [];
|
||||
|
||||
if (meta[ClientCapabilitiesMetaKey] is not JsonObject capsRoot)
|
||||
{
|
||||
capsRoot = [];
|
||||
meta[ClientCapabilitiesMetaKey] = capsRoot;
|
||||
}
|
||||
|
||||
if (capsRoot[ExtensionsKey] is not JsonObject extensionsRoot)
|
||||
{
|
||||
extensionsRoot = [];
|
||||
capsRoot[ExtensionsKey] = extensionsRoot;
|
||||
}
|
||||
|
||||
extensionsRoot.TryAdd(McpExtensions.Tasks, new JsonObject());
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
|
@ -71,6 +72,26 @@ public abstract partial class McpClient : McpSession
|
|||
/// </remarks>
|
||||
public abstract Task<ClientCompletionDetails> Completion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves input requests embedded in an <see cref="InputRequiredTaskResult"/> by dispatching
|
||||
/// each request to the appropriate registered handler.
|
||||
/// </summary>
|
||||
/// <param name="inputRequests">
|
||||
/// The input requests from the task, keyed by request identifier. Each value is an
|
||||
/// <see cref="InputRequest"/> wrapping the server-to-client request payload.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A dictionary of responses keyed by the same identifiers as the input requests.</returns>
|
||||
private protected abstract ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
|
||||
IDictionary<string, InputRequest> inputRequests, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum number of consecutive stuck-in-<see cref="McpTaskStatus.InputRequired"/> polls
|
||||
/// allowed by <see cref="PollTaskToCompletionAsync"/> before the client cancels and throws.
|
||||
/// Sourced from <see cref="McpClientOptions.MaxConsecutiveStuckPolls"/>.
|
||||
/// </summary>
|
||||
private protected abstract int MaxConsecutiveStuckPolls { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more tool definitions in the client's tool cache, enabling the transport
|
||||
/// to send <c>Mcp-Param-*</c> headers for those tools without requiring a prior <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/> call.
|
||||
|
|
|
|||
|
|
@ -86,25 +86,4 @@ public sealed 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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
private readonly McpClientOptions _options;
|
||||
private readonly McpSessionHandler _sessionHandler;
|
||||
private readonly SemaphoreSlim _disposeLock = new(1, 1);
|
||||
private readonly McpTaskCancellationTokenProvider? _taskCancellationTokenProvider;
|
||||
private readonly ConcurrentDictionary<string, Tool> _toolCache = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, byte> _registeredToolNames = new(StringComparer.Ordinal);
|
||||
|
||||
|
|
@ -51,12 +50,6 @@ 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();
|
||||
|
||||
|
|
@ -101,95 +94,26 @@ 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)
|
||||
{
|
||||
// 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) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.SamplingCreateMessage);
|
||||
|
||||
// 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) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.SamplingCreateMessage);
|
||||
return samplingHandler(
|
||||
request,
|
||||
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
||||
}
|
||||
requestHandlers.Set(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, _, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.SamplingCreateMessage);
|
||||
return 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();
|
||||
|
|
@ -213,54 +137,16 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
if (elicitationHandler is not null)
|
||||
{
|
||||
// If task store is configured, wrap the handler to support task-augmented requests
|
||||
if (taskStore is not null)
|
||||
{
|
||||
requestHandlers.Set(
|
||||
RequestMethods.ElicitationCreate,
|
||||
async (request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.ElicitationCreate);
|
||||
|
||||
// Check if this is a task-augmented request
|
||||
if (request?.Task is { } taskMetadata)
|
||||
{
|
||||
// Create task in store and return immediately
|
||||
return await ExecuteAsTaskAsync(
|
||||
taskStore,
|
||||
taskMetadata,
|
||||
jsonRpcRequest,
|
||||
async ct =>
|
||||
{
|
||||
var result = await elicitationHandler(request, ct).ConfigureAwait(false);
|
||||
result = ElicitResult.WithDefaults(request, result);
|
||||
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||
},
|
||||
options.SendTaskStatusNotifications,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Normal synchronous execution - serialize result to JsonElement
|
||||
var elicitResult = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
elicitResult = ElicitResult.WithDefaults(request, elicitResult);
|
||||
return JsonSerializer.SerializeToElement(elicitResult, McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.JsonElement); // Return JsonElement to support both ElicitResult and CreateTaskResult
|
||||
}
|
||||
else
|
||||
{
|
||||
requestHandlers.Set(
|
||||
RequestMethods.ElicitationCreate,
|
||||
async (request, _, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.ElicitationCreate);
|
||||
var result = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
return ElicitResult.WithDefaults(request, result);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||
}
|
||||
requestHandlers.Set(
|
||||
RequestMethods.ElicitationCreate,
|
||||
async (request, _, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.ElicitationCreate);
|
||||
var result = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
return ElicitResult.WithDefaults(request, result);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||
|
||||
_options.Capabilities ??= new();
|
||||
_options.Capabilities.Elicitation ??= new();
|
||||
|
|
@ -271,276 +157,6 @@ 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/>
|
||||
|
|
@ -562,7 +178,10 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
public override Task<ClientCompletionDetails> Completion => _sessionHandler.CompletionTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private async ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
|
||||
private protected override int MaxConsecutiveStuckPolls => _options.MaxConsecutiveStuckPolls;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override async ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
|
||||
IDictionary<string, InputRequest> inputRequests,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
@ -929,7 +548,6 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
_disposed = true;
|
||||
|
||||
_taskCancellationTokenProvider?.Dispose();
|
||||
await _sessionHandler.DisposeAsync().ConfigureAwait(false);
|
||||
await _transport.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -97,34 +97,40 @@ public sealed class McpClientOptions
|
|||
}
|
||||
|
||||
/// <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.
|
||||
/// Gets or sets the maximum number of consecutive task polls during which a task may report
|
||||
/// <see cref="McpTaskStatus.InputRequired"/> without publishing any new input requests, before
|
||||
/// the client treats the task as stuck, issues a best-effort <c>tasks/cancel</c>, and throws
|
||||
/// an <see cref="McpException"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to send task status notifications; <see langword="false"/> otherwise.
|
||||
/// The default is <see langword="true"/>.
|
||||
/// The maximum number of consecutive stuck polls allowed. The default value is <c>60</c>.
|
||||
/// </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>.
|
||||
/// <para>
|
||||
/// This guard prevents an unbounded poll loop when the server keeps a task in
|
||||
/// <see cref="McpTaskStatus.InputRequired"/> but never publishes new input requests after the
|
||||
/// client has already responded to every previously surfaced request. It only affects the
|
||||
/// long-poll path used by <see cref="McpClient.CallToolAsync(CallToolRequestParams, CancellationToken)"/>;
|
||||
/// it does not affect direct <see cref="McpClient.GetTaskAsync(string, CancellationToken)"/> calls.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Callers should size this value with the configured server-side poll interval in mind: the
|
||||
/// effective wall-clock timeout is roughly <c>MaxConsecutiveStuckPolls * pollIntervalMs</c>.
|
||||
/// Setting this to a very small value can cause false positives for servers that are slow to
|
||||
/// surface follow-up input requests; setting it too large can mask misbehaving servers.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
public bool SendTaskStatusNotifications { get; set; } = true;
|
||||
/// <exception cref="ArgumentOutOfRangeException">The value is less than <c>1</c>.</exception>
|
||||
public int MaxConsecutiveStuckPolls
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (value < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, "must be greater than or equal to 1.");
|
||||
}
|
||||
field = value;
|
||||
}
|
||||
} = 60;
|
||||
}
|
||||
|
|
|
|||
2244
src/ModelContextProtocol.Core/CompatibilitySuppressions.xml
Normal file
2244
src/ModelContextProtocol.Core/CompatibilitySuppressions.xml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -96,6 +96,7 @@ public static partial class McpJsonUtilities
|
|||
[JsonSerializable(typeof(JsonRpcNotification))]
|
||||
[JsonSerializable(typeof(JsonRpcResponse))]
|
||||
[JsonSerializable(typeof(JsonRpcError))]
|
||||
[JsonSerializable(typeof(JsonRpcErrorDetail))]
|
||||
|
||||
// MCP Notification Params
|
||||
[JsonSerializable(typeof(CancelledNotificationParams))]
|
||||
|
|
@ -108,12 +109,16 @@ public static partial class McpJsonUtilities
|
|||
[JsonSerializable(typeof(ResourceUpdatedNotificationParams))]
|
||||
[JsonSerializable(typeof(RootsListChangedNotificationParams))]
|
||||
[JsonSerializable(typeof(ToolListChangedNotificationParams))]
|
||||
[JsonSerializable(typeof(McpTaskStatusNotificationParams))]
|
||||
[JsonSerializable(typeof(TaskStatusNotificationParams))]
|
||||
[JsonSerializable(typeof(WorkingTaskNotificationParams))]
|
||||
[JsonSerializable(typeof(CompletedTaskNotificationParams))]
|
||||
[JsonSerializable(typeof(FailedTaskNotificationParams))]
|
||||
[JsonSerializable(typeof(CancelledTaskNotificationParams))]
|
||||
[JsonSerializable(typeof(InputRequiredTaskNotificationParams))]
|
||||
|
||||
// MCP Request Params / Results
|
||||
[JsonSerializable(typeof(CallToolRequestParams))]
|
||||
[JsonSerializable(typeof(CallToolResult))]
|
||||
[JsonSerializable(typeof(CreateTaskResult))]
|
||||
[JsonSerializable(typeof(CompleteRequestParams))]
|
||||
[JsonSerializable(typeof(CompleteResult))]
|
||||
[JsonSerializable(typeof(CreateMessageRequestParams))]
|
||||
|
|
@ -151,21 +156,18 @@ public static partial class McpJsonUtilities
|
|||
[JsonSerializable(typeof(IDictionary<string, InputRequest>))]
|
||||
[JsonSerializable(typeof(IDictionary<string, InputResponse>))]
|
||||
|
||||
// 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))]
|
||||
[JsonSerializable(typeof(WorkingTaskResult))]
|
||||
[JsonSerializable(typeof(CompletedTaskResult))]
|
||||
[JsonSerializable(typeof(FailedTaskResult))]
|
||||
[JsonSerializable(typeof(CancelledTaskResult))]
|
||||
[JsonSerializable(typeof(InputRequiredTaskResult))]
|
||||
[JsonSerializable(typeof(UpdateTaskRequestParams))]
|
||||
[JsonSerializable(typeof(UpdateTaskResult))]
|
||||
[JsonSerializable(typeof(CancelTaskRequestParams))]
|
||||
[JsonSerializable(typeof(CancelTaskResult))]
|
||||
[JsonSerializable(typeof(CreateTaskResult))]
|
||||
|
||||
// MCP Content
|
||||
[JsonSerializable(typeof(ContentBlock))]
|
||||
|
|
@ -184,9 +186,9 @@ public static partial class McpJsonUtilities
|
|||
[JsonSerializable(typeof(TextResourceContents))]
|
||||
|
||||
// Other MCP Types
|
||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||
[JsonSerializable(typeof(IReadOnlyDictionary<string, object>))]
|
||||
[JsonSerializable(typeof(ProgressToken))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
|
||||
[JsonSerializable(typeof(ProtectedResourceMetadata))]
|
||||
[JsonSerializable(typeof(AuthorizationServerMetadata))]
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<PackageId>ModelContextProtocol.Core</PackageId>
|
||||
<Description>Core .NET SDK for the Model Context Protocol (MCP)</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<!-- Suppress the experimental tasks warning -->
|
||||
<!-- Suppress the experimental extensions warning -->
|
||||
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="..\Common\CancellableStreamReader\**\*.cs" />
|
||||
<PackageReference Include="Microsoft.Bcl.Memory" />
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<PackageReference Include="System.Threading.Channels" />
|
||||
|
|
|
|||
|
|
@ -26,24 +26,4 @@ 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>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
[JsonIgnore]
|
||||
public McpTaskMetadata? Task
|
||||
{
|
||||
get => TaskCore;
|
||||
set => TaskCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("task")]
|
||||
internal McpTaskMetadata? TaskCore { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,25 +64,4 @@ 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>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
[JsonIgnore]
|
||||
public McpTask? Task
|
||||
{
|
||||
get => TaskCore;
|
||||
set => TaskCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("task")]
|
||||
internal McpTask? TaskCore { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the parameters for a <c>tasks/cancel</c> request to signal intent to cancel an in-progress task.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Cancellation is cooperative: the request signals intent, and the server decides whether and when to honor it.
|
||||
/// A server is not obligated to actually stop the work; it is only obligated to acknowledge the request.
|
||||
/// Eventual transition to <see cref="McpTaskStatus.Cancelled"/> is not guaranteed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <c>notifications/cancelled</c> notification must not be used for task cancellation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
|
||||
/// specification for details.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class CancelTaskRequestParams : RequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the task to cancel.
|
||||
/// </summary>
|
||||
[JsonPropertyName("taskId")]
|
||||
public required string TaskId { get; set; }
|
||||
}
|
||||
22
src/ModelContextProtocol.Core/Protocol/CancelTaskResult.cs
Normal file
22
src/ModelContextProtocol.Core/Protocol/CancelTaskResult.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a <c>tasks/cancel</c> request. This is an empty acknowledgement.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The server acknowledges the request with an empty result. Cancellation processing is
|
||||
/// eventually consistent — the task's observable status may remain <see cref="McpTaskStatus.Working"/>
|
||||
/// after the ack, and may ultimately reach a terminal status other than <see cref="McpTaskStatus.Cancelled"/>
|
||||
/// if the work finished before cancellation could take effect.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
|
||||
/// specification for details.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class CancelTaskResult : Result
|
||||
{
|
||||
}
|
||||
|
|
@ -68,32 +68,6 @@ public sealed class ClientCapabilities
|
|||
[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>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
[JsonIgnore]
|
||||
public McpTasksCapability? Tasks
|
||||
{
|
||||
get => TasksCore;
|
||||
set => TasksCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("tasks")]
|
||||
internal McpTasksCapability? TasksCore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional MCP extensions that the client supports.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -153,24 +153,4 @@ public sealed class CreateMessageRequestParams : RequestParams
|
|||
/// </remarks>
|
||||
[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>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
[JsonIgnore]
|
||||
public McpTaskMetadata? Task
|
||||
{
|
||||
get => TaskCore;
|
||||
set => TaskCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("task")]
|
||||
internal McpTaskMetadata? TaskCore { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,68 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response to a task-augmented request.
|
||||
/// Represents the result returned by a server when it creates a task in lieu of a standard result.
|
||||
/// </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>.
|
||||
/// A server returns <see cref="CreateTaskResult"/> instead of the standard result shape (e.g., <see cref="CallToolResult"/>)
|
||||
/// to indicate that the request will be processed asynchronously. The client then uses
|
||||
/// <see cref="TaskId"/> for subsequent <c>tasks/get</c>, <c>tasks/update</c>, and <c>tasks/cancel</c> calls.
|
||||
/// </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>.
|
||||
/// A server must not return <see cref="CreateTaskResult"/> to a client that did not include the
|
||||
/// <c>io.modelcontextprotocol/tasks</c> extension capability on its request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
|
||||
/// specification for details.
|
||||
/// </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.
|
||||
/// Gets or sets the stable identifier for this task.
|
||||
/// </summary>
|
||||
[JsonPropertyName("task")]
|
||||
public McpTask Task { get; set; } = null!;
|
||||
[JsonPropertyName("taskId")]
|
||||
public required string TaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current task status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required McpTaskStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional message describing the current task 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 was last updated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lastUpdatedAt")]
|
||||
public required DateTimeOffset LastUpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time-to-live duration from creation in milliseconds, or <see langword="null"/> for unlimited.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ttlMs")]
|
||||
public long? TtlMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the suggested polling interval in milliseconds.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pollIntervalMs")]
|
||||
public long? PollIntervalMs { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,26 +92,6 @@ 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>
|
||||
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
|
||||
[JsonIgnore]
|
||||
public McpTaskMetadata? Task
|
||||
{
|
||||
get => TaskCore;
|
||||
set => TaskCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("task")]
|
||||
internal McpTaskMetadata? TaskCore { get; set; }
|
||||
|
||||
/// <summary>Represents a request schema used in a form mode elicitation request.</summary>
|
||||
public sealed class RequestSchema
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
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; }
|
||||
}
|
||||
|
|
@ -1,77 +1,26 @@
|
|||
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.
|
||||
/// Represents the parameters for a <c>tasks/get</c> request to poll for task completion.
|
||||
/// </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.
|
||||
/// <para>
|
||||
/// Clients poll for task completion by sending <c>tasks/get</c> requests.
|
||||
/// Clients should respect the <see cref="GetTaskResult.PollIntervalMs"/> provided in responses
|
||||
/// when determining polling frequency.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
|
||||
/// specification for details.
|
||||
/// </para>
|
||||
/// </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.
|
||||
/// Gets or sets the identifier of the task to query.
|
||||
/// </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; }
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue