Extract Tasks into the ModelContextProtocol.Extensions.Tasks extension package (#1693)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
This commit is contained in:
Jeff Handley 2026-07-14 15:53:58 -07:00 • committed by GitHub
commit 2ca96f548b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 2559 additions and 1895 deletions

View file

@ -0,0 +1,30 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <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; }
}

View file

@ -0,0 +1,23 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <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
{
}

View file

@ -0,0 +1,78 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the result returned by a server when it creates a task in lieu of a standard result.
/// </summary>
/// <remarks>
/// <para>
/// 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>
/// 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>
public sealed class CreateTaskResult : Result
{
/// <summary>
/// Initializes a new instance of the <see cref="CreateTaskResult"/> class.
/// </summary>
public CreateTaskResult()
{
ResultType = "task";
}
/// <summary>
/// Gets or sets the stable identifier for this task.
/// </summary>
[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, or <see langword="null"/> for unlimited.
/// </summary>
[JsonPropertyName("ttlMs")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }
/// <summary>
/// Gets or sets the suggested polling interval in milliseconds.
/// </summary>
[JsonPropertyName("pollIntervalMs")]
public long? PollIntervalMs { get; set; }
}

View file

@ -0,0 +1,27 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the parameters for a <c>tasks/get</c> request to poll for task completion.
/// </summary>
/// <remarks>
/// <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>
public sealed class GetTaskRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the identifier of the task to query.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
}

View file

@ -0,0 +1,450 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the result of a <c>tasks/get</c> request, containing the full task state.
/// </summary>
/// <remarks>
/// <para>
/// This is the abstract base for status-specific task results. The concrete type returned depends on the
/// task's current <see cref="Status"/>:
/// <list type="bullet">
/// <item><see cref="WorkingTaskResult"/> — <see cref="McpTaskStatus.Working"/></item>
/// <item><see cref="CompletedTaskResult"/> — <see cref="McpTaskStatus.Completed"/></item>
/// <item><see cref="FailedTaskResult"/> — <see cref="McpTaskStatus.Failed"/></item>
/// <item><see cref="CancelledTaskResult"/> — <see cref="McpTaskStatus.Cancelled"/></item>
/// <item><see cref="InputRequiredTaskResult"/> — <see cref="McpTaskStatus.InputRequired"/></item>
/// </list>
/// </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>
[JsonConverter(typeof(Converter))]
public abstract class GetTaskResult : Result
{
/// <summary>Prevent external derivations.</summary>
private protected GetTaskResult()
{
}
/// <summary>
/// Gets or sets the stable identifier for this task.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
/// <summary>
/// Gets or sets the current task status.
/// </summary>
[JsonPropertyName("status")]
public abstract McpTaskStatus Status { get; }
/// <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, or <see langword="null"/> for unlimited.
/// </summary>
[JsonPropertyName("ttlMs")]
public TimeSpan? TimeToLive { get; set; }
/// <summary>
/// Gets or sets the suggested polling interval in milliseconds.
/// </summary>
[JsonPropertyName("pollIntervalMs")]
public long? PollIntervalMs { get; set; }
/// <summary>
/// JSON converter that deserializes <see cref="GetTaskResult"/> to the appropriate concrete subtype
/// based on the <c>status</c> discriminator field.
/// </summary>
internal sealed class Converter : JsonConverter<GetTaskResult>
{
public override GetTaskResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject token for GetTaskResult.");
}
string? taskId = null;
string? statusString = null;
string? statusMessage = null;
DateTimeOffset? createdAt = null;
DateTimeOffset? lastUpdatedAt = null;
long? ttlMs = null;
long? pollIntervalMs = null;
string? resultType = null;
JsonObject? meta = null;
JsonElement? result = null;
JsonElement? error = null;
Dictionary<string, InputRequest>? inputRequests = null;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("Expected property name.");
}
string propertyName = reader.GetString()!;
reader.Read();
switch (propertyName)
{
case "taskId":
taskId = reader.GetString();
break;
case "status":
statusString = reader.GetString();
break;
case "statusMessage":
statusMessage = reader.GetString();
break;
case "createdAt":
createdAt = reader.GetDateTimeOffset();
break;
case "lastUpdatedAt":
lastUpdatedAt = reader.GetDateTimeOffset();
break;
case "ttlMs":
ttlMs = reader.GetInt64();
break;
case "pollIntervalMs":
pollIntervalMs = reader.GetInt64();
break;
case "resultType":
resultType = reader.GetString();
break;
case "_meta":
meta = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonObject>());
break;
case "result":
result = JsonElement.ParseValue(ref reader);
break;
case "error":
error = JsonElement.ParseValue(ref reader);
break;
case "inputRequests":
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("'inputRequests' must be a JSON object.");
}
inputRequests = new Dictionary<string, InputRequest>(StringComparer.Ordinal);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("Expected property name in 'inputRequests'.");
}
string requestKey = reader.GetString()!;
reader.Read();
var inputRequest = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.DefaultOptions.GetTypeInfo<InputRequest>())
?? throw new JsonException($"Failed to deserialize InputRequest for key '{requestKey}'.");
inputRequests[requestKey] = inputRequest;
}
break;
default:
reader.Skip();
break;
}
}
if (taskId is null)
{
throw new JsonException("Missing required 'taskId' property on GetTaskResult.");
}
if (statusString is null)
{
throw new JsonException("Missing required 'status' property on GetTaskResult.");
}
if (createdAt is null)
{
throw new JsonException("Missing required 'createdAt' property on GetTaskResult.");
}
if (lastUpdatedAt is null)
{
throw new JsonException("Missing required 'lastUpdatedAt' property on GetTaskResult.");
}
GetTaskResult taskResult = statusString switch
{
"working" => new WorkingTaskResult
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
},
"completed" => result is not null
? new CompletedTaskResult
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
Result = result.Value,
}
: throw new JsonException("Completed task is missing required 'result' property."),
"failed" => error is not null
? new FailedTaskResult
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
Error = error.Value,
}
: throw new JsonException("Failed task is missing required 'error' property."),
"cancelled" => new CancelledTaskResult
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
},
"input_required" => inputRequests is not null
? new InputRequiredTaskResult
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
InputRequests = inputRequests,
}
: throw new JsonException("Input-required task is missing required 'inputRequests' property."),
_ => throw new JsonException($"Unknown task status: '{statusString}'.")
};
taskResult.StatusMessage = statusMessage;
taskResult.TimeToLive = ttlMs is null ? null : TimeSpan.FromMilliseconds(ttlMs.Value);
taskResult.PollIntervalMs = pollIntervalMs;
taskResult.ResultType = resultType;
taskResult.Meta = meta;
return taskResult;
}
public override void Write(Utf8JsonWriter writer, GetTaskResult value, JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value.ResultType is not null)
{
writer.WriteString("resultType", value.ResultType);
}
if (value.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, value.Meta, options.GetTypeInfo<JsonObject>());
}
writer.WriteString("taskId", value.TaskId);
writer.WriteString("status", value.Status switch
{
McpTaskStatus.Working => "working",
McpTaskStatus.Completed => "completed",
McpTaskStatus.Failed => "failed",
McpTaskStatus.Cancelled => "cancelled",
McpTaskStatus.InputRequired => "input_required",
_ => throw new JsonException($"Unknown McpTaskStatus: {value.Status}")
});
if (value.StatusMessage is not null)
{
writer.WriteString("statusMessage", value.StatusMessage);
}
writer.WriteString("createdAt", value.CreatedAt);
writer.WriteString("lastUpdatedAt", value.LastUpdatedAt);
if (value.TimeToLive is not null)
{
writer.WriteNumber("ttlMs", (long)value.TimeToLive.Value.TotalMilliseconds);
}
if (value.PollIntervalMs is not null)
{
writer.WriteNumber("pollIntervalMs", value.PollIntervalMs.Value);
}
switch (value)
{
case CompletedTaskResult completed:
writer.WritePropertyName("result");
completed.Result.WriteTo(writer);
break;
case FailedTaskResult failed:
writer.WritePropertyName("error");
failed.Error.WriteTo(writer);
break;
case InputRequiredTaskResult inputRequired:
writer.WritePropertyName("inputRequests");
writer.WriteStartObject();
if (inputRequired.InputRequests is { } reqs)
{
foreach (var kvp in reqs)
{
writer.WritePropertyName(kvp.Key);
JsonSerializer.Serialize(writer, kvp.Value, McpJsonUtilities.DefaultOptions.GetTypeInfo<InputRequest>());
}
}
writer.WriteEndObject();
break;
}
writer.WriteEndObject();
}
}
}
/// <summary>
/// Represents a task that is currently being processed by the server.
/// </summary>
/// <remarks>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
/// specification for details.
/// </remarks>
public sealed class WorkingTaskResult : GetTaskResult
{
/// <inheritdoc/>
[JsonPropertyName("status")]
public override McpTaskStatus Status => McpTaskStatus.Working;
}
/// <summary>
/// Represents a task that has completed successfully, carrying the final result.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="Result"/> field contains the result structure matching the original request type.
/// For example, a <c>tools/call</c> task would contain the <see cref="CallToolResult"/> structure.
/// This includes tool calls that returned results with <c>isError: true</c>.
/// </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 CompletedTaskResult : GetTaskResult
{
/// <inheritdoc/>
[JsonPropertyName("status")]
public override McpTaskStatus Status => McpTaskStatus.Completed;
/// <summary>
/// Gets or sets the final result of the task as raw JSON.
/// </summary>
/// <remarks>
/// The structure matches the result type of the original request.
/// </remarks>
[JsonPropertyName("result")]
public required JsonElement Result { get; set; }
}
/// <summary>
/// Represents a task that failed due to a JSON-RPC error during execution.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="Error"/> field contains the JSON-RPC error object that caused the failure.
/// This status must not be used for non-JSON-RPC errors.
/// </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 FailedTaskResult : GetTaskResult
{
/// <inheritdoc/>
[JsonPropertyName("status")]
public override McpTaskStatus Status => McpTaskStatus.Failed;
/// <summary>
/// Gets or sets the JSON-RPC error that caused the task to fail.
/// </summary>
[JsonPropertyName("error")]
public required JsonElement Error { get; set; }
}
/// <summary>
/// Represents a task that was cancelled before completion.
/// </summary>
/// <remarks>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
/// specification for details.
/// </remarks>
public sealed class CancelledTaskResult : GetTaskResult
{
/// <inheritdoc/>
[JsonPropertyName("status")]
public override McpTaskStatus Status => McpTaskStatus.Cancelled;
}
/// <summary>
/// Represents a task that requires input from the client before it can proceed.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="InputRequests"/> field contains outstanding server-to-client requests
/// that the client must fulfil. Each entry is keyed by an arbitrary identifier for matching
/// requests to responses, and each value is an <see cref="InputRequest"/> wrapping the
/// server-to-client request payload.
/// </para>
/// <para>
/// Clients must treat each entry as they would the equivalent standalone server-to-client request.
/// Clients should deduplicate keys across consecutive polls to avoid presenting the same request
/// to the user or model more than once.
/// </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 InputRequiredTaskResult : GetTaskResult
{
/// <inheritdoc/>
[JsonPropertyName("status")]
public override McpTaskStatus Status => McpTaskStatus.InputRequired;
/// <summary>
/// Gets or sets the server-to-client requests that need to be fulfilled.
/// </summary>
/// <remarks>
/// Keys are arbitrary identifiers for matching requests to responses.
/// Each value is an <see cref="InputRequest"/> wrapping the server-to-client request
/// (e.g., a sampling, elicitation, or roots-list request).
/// </remarks>
[JsonPropertyName("inputRequests")]
public IDictionary<string, InputRequest>? InputRequests { get; set; }
}

View file

@ -0,0 +1,50 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the status of an MCP task.
/// </summary>
/// <remarks>
/// Tasks are durable state machines that carry information about the underlying execution state
/// of the request they augment. See the
/// <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663</see>
/// specification for details.
/// </remarks>
[JsonConverter(typeof(JsonStringEnumConverter<McpTaskStatus>))]
public enum McpTaskStatus
{
/// <summary>
/// The request is currently being processed.
/// </summary>
[JsonStringEnumMemberName("working")]
Working,
/// <summary>
/// The server needs input from the client before the task can proceed.
/// The <c>tasks/get</c> response will include outstanding requests in the <c>inputRequests</c> field.
/// </summary>
[JsonStringEnumMemberName("input_required")]
InputRequired,
/// <summary>
/// The request completed successfully and results are available.
/// This includes tool calls that returned results with <c>isError: true</c>.
/// </summary>
[JsonStringEnumMemberName("completed")]
Completed,
/// <summary>
/// The request was cancelled before completion.
/// </summary>
[JsonStringEnumMemberName("cancelled")]
Cancelled,
/// <summary>
/// The request failed due to a JSON-RPC error during execution.
/// This status must not be used for non-JSON-RPC errors.
/// </summary>
[JsonStringEnumMemberName("failed")]
Failed,
}

View file

@ -0,0 +1,83 @@
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the result of a request that supports task-augmented execution, which may be either
/// the standard result or a <see cref="CreateTaskResult"/> indicating asynchronous processing.
/// </summary>
/// <typeparam name="TResult">The standard result type for the request (e.g., <see cref="CallToolResult"/>).</typeparam>
/// <remarks>
/// <para>
/// When a server supports the <c>io.modelcontextprotocol/tasks</c> extension and the client declares
/// the extension capability on its request, the server may return a <see cref="CreateTaskResult"/>
/// instead of the standard result. This type represents that polymorphic response.
/// </para>
/// <para>
/// Use <see cref="IsTask"/> to determine which variant was returned, then access either
/// <see cref="Result"/> for the immediate result or <see cref="TaskCreated"/> for the task handle.
/// </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 class ResultOrCreatedTask<TResult> where TResult : Result
{
private readonly TResult? _result;
private readonly CreateTaskResult? _taskCreated;
/// <summary>
/// Initializes a new instance of <see cref="ResultOrCreatedTask{TResult}"/> with an immediate result.
/// </summary>
/// <param name="result">The standard result returned by the server.</param>
public ResultOrCreatedTask(TResult result)
{
if (result is null)
{
throw new ArgumentNullException(nameof(result));
}
_result = result;
}
/// <summary>
/// Initializes a new instance of <see cref="ResultOrCreatedTask{TResult}"/> with a task handle.
/// </summary>
/// <param name="taskCreated">The task creation result returned by the server.</param>
public ResultOrCreatedTask(CreateTaskResult taskCreated)
{
if (taskCreated is null)
{
throw new ArgumentNullException(nameof(taskCreated));
}
_taskCreated = taskCreated;
}
/// <summary>
/// Gets a value indicating whether the server created a task instead of returning an immediate result.
/// </summary>
public bool IsTask => _taskCreated is not null;
/// <summary>
/// Gets the immediate result, or <see langword="null"/> if the server created a task.
/// </summary>
public TResult? Result => _result;
/// <summary>
/// Gets the task creation result, or <see langword="null"/> if the server returned an immediate result.
/// </summary>
public CreateTaskResult? TaskCreated => _taskCreated;
/// <summary>
/// Implicitly converts a <typeparamref name="TResult"/> to a <see cref="ResultOrCreatedTask{TResult}"/>
/// wrapping the immediate result.
/// </summary>
/// <param name="result">The result to wrap.</param>
public static implicit operator ResultOrCreatedTask<TResult>(TResult result) => new(result);
/// <summary>
/// Implicitly converts a <see cref="CreateTaskResult"/> to a <see cref="ResultOrCreatedTask{TResult}"/>
/// wrapping the task handle.
/// </summary>
/// <param name="taskCreated">The task creation result to wrap.</param>
public static implicit operator ResultOrCreatedTask<TResult>(CreateTaskResult taskCreated) => new(taskCreated);
}

View file

@ -0,0 +1,392 @@
using ModelContextProtocol.Protocol;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the parameters for a <c>notifications/tasks</c> notification sent by the server
/// to push task status updates to the client.
/// </summary>
/// <remarks>
/// <para>
/// Each notification carries a complete task state for the current status, identical to what
/// <c>tasks/get</c> would have returned at that moment. The concrete type depends on the task's
/// current status:
/// <list type="bullet">
/// <item><see cref="WorkingTaskNotificationParams"/> — <see cref="McpTaskStatus.Working"/></item>
/// <item><see cref="CompletedTaskNotificationParams"/> — <see cref="McpTaskStatus.Completed"/></item>
/// <item><see cref="FailedTaskNotificationParams"/> — <see cref="McpTaskStatus.Failed"/></item>
/// <item><see cref="CancelledTaskNotificationParams"/> — <see cref="McpTaskStatus.Cancelled"/></item>
/// <item><see cref="InputRequiredTaskNotificationParams"/> — <see cref="McpTaskStatus.InputRequired"/></item>
/// </list>
/// </para>
/// <para>
/// To receive task status notifications, clients send a <c>subscriptions/listen</c> request
/// including the task IDs they are interested in.
/// </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>
[JsonConverter(typeof(Converter))]
public abstract class TaskStatusNotificationParams : NotificationParams
{
/// <summary>Prevent external derivations.</summary>
private protected TaskStatusNotificationParams()
{
}
/// <summary>
/// Gets or sets the stable identifier for this task.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
/// <summary>
/// Gets or sets the current task status.
/// </summary>
[JsonPropertyName("status")]
public abstract McpTaskStatus Status { get; }
/// <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, or <see langword="null"/> for unlimited.
/// </summary>
[JsonPropertyName("ttlMs")]
public TimeSpan? TimeToLive { get; set; }
/// <summary>
/// Gets or sets the suggested polling interval in milliseconds.
/// </summary>
[JsonPropertyName("pollIntervalMs")]
public long? PollIntervalMs { get; set; }
/// <summary>
/// JSON converter that deserializes <see cref="TaskStatusNotificationParams"/> to the appropriate
/// concrete subtype based on the <c>status</c> discriminator field.
/// </summary>
internal sealed class Converter : JsonConverter<TaskStatusNotificationParams>
{
public override TaskStatusNotificationParams? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject token for TaskStatusNotificationParams.");
}
string? taskId = null;
string? statusString = null;
string? statusMessage = null;
DateTimeOffset? createdAt = null;
DateTimeOffset? lastUpdatedAt = null;
long? ttlMs = null;
long? pollIntervalMs = null;
JsonObject? meta = null;
JsonElement? result = null;
JsonElement? error = null;
Dictionary<string, InputRequest>? inputRequests = null;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("Expected property name.");
}
string propertyName = reader.GetString()!;
reader.Read();
switch (propertyName)
{
case "taskId":
taskId = reader.GetString();
break;
case "status":
statusString = reader.GetString();
break;
case "statusMessage":
statusMessage = reader.GetString();
break;
case "createdAt":
createdAt = reader.GetDateTimeOffset();
break;
case "lastUpdatedAt":
lastUpdatedAt = reader.GetDateTimeOffset();
break;
case "ttlMs":
ttlMs = reader.GetInt64();
break;
case "pollIntervalMs":
pollIntervalMs = reader.GetInt64();
break;
case "_meta":
meta = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonObject>());
break;
case "result":
result = JsonElement.ParseValue(ref reader);
break;
case "error":
error = JsonElement.ParseValue(ref reader);
break;
case "inputRequests":
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("'inputRequests' must be a JSON object.");
}
inputRequests = new Dictionary<string, InputRequest>(StringComparer.Ordinal);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException("Expected property name in 'inputRequests'.");
}
string requestKey = reader.GetString()!;
reader.Read();
var inputRequest = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.DefaultOptions.GetTypeInfo<InputRequest>())
?? throw new JsonException($"Failed to deserialize InputRequest for key '{requestKey}'.");
inputRequests[requestKey] = inputRequest;
}
break;
default:
reader.Skip();
break;
}
}
if (taskId is null)
{
throw new JsonException("Missing required 'taskId' property on TaskStatusNotificationParams.");
}
if (statusString is null)
{
throw new JsonException("Missing required 'status' property on TaskStatusNotificationParams.");
}
if (createdAt is null)
{
throw new JsonException("Missing required 'createdAt' property on TaskStatusNotificationParams.");
}
if (lastUpdatedAt is null)
{
throw new JsonException("Missing required 'lastUpdatedAt' property on TaskStatusNotificationParams.");
}
TaskStatusNotificationParams notification = statusString switch
{
"working" => new WorkingTaskNotificationParams
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
},
"completed" => result is not null
? new CompletedTaskNotificationParams
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
Result = result.Value,
}
: throw new JsonException("Completed task notification is missing required 'result' property."),
"failed" => error is not null
? new FailedTaskNotificationParams
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
Error = error.Value,
}
: throw new JsonException("Failed task notification is missing required 'error' property."),
"cancelled" => new CancelledTaskNotificationParams
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
},
"input_required" => inputRequests is not null
? new InputRequiredTaskNotificationParams
{
TaskId = taskId,
CreatedAt = createdAt.Value,
LastUpdatedAt = lastUpdatedAt.Value,
InputRequests = inputRequests,
}
: throw new JsonException("Input-required task notification is missing required 'inputRequests' property."),
_ => throw new JsonException($"Unknown task status: '{statusString}'.")
};
notification.StatusMessage = statusMessage;
notification.TimeToLive = ttlMs is null ? null : TimeSpan.FromMilliseconds(ttlMs.Value);
notification.PollIntervalMs = pollIntervalMs;
notification.Meta = meta;
return notification;
}
public override void Write(Utf8JsonWriter writer, TaskStatusNotificationParams value, JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value.Meta is not null)
{
writer.WritePropertyName("_meta");
JsonSerializer.Serialize(writer, value.Meta, options.GetTypeInfo<JsonObject>());
}
writer.WriteString("taskId", value.TaskId);
writer.WriteString("status", value.Status switch
{
McpTaskStatus.Working => "working",
McpTaskStatus.Completed => "completed",
McpTaskStatus.Failed => "failed",
McpTaskStatus.Cancelled => "cancelled",
McpTaskStatus.InputRequired => "input_required",
_ => throw new JsonException($"Unknown McpTaskStatus: {value.Status}")
});
if (value.StatusMessage is not null)
{
writer.WriteString("statusMessage", value.StatusMessage);
}
writer.WriteString("createdAt", value.CreatedAt);
writer.WriteString("lastUpdatedAt", value.LastUpdatedAt);
if (value.TimeToLive is not null)
{
writer.WriteNumber("ttlMs", (long)value.TimeToLive.Value.TotalMilliseconds);
}
if (value.PollIntervalMs is not null)
{
writer.WriteNumber("pollIntervalMs", value.PollIntervalMs.Value);
}
switch (value)
{
case CompletedTaskNotificationParams completed:
writer.WritePropertyName("result");
completed.Result.WriteTo(writer);
break;
case FailedTaskNotificationParams failed:
writer.WritePropertyName("error");
failed.Error.WriteTo(writer);
break;
case InputRequiredTaskNotificationParams inputRequired:
writer.WritePropertyName("inputRequests");
writer.WriteStartObject();
if (inputRequired.InputRequests is { } reqs)
{
foreach (var kvp in reqs)
{
writer.WritePropertyName(kvp.Key);
JsonSerializer.Serialize(writer, kvp.Value, McpJsonUtilities.DefaultOptions.GetTypeInfo<InputRequest>());
}
}
writer.WriteEndObject();
break;
}
writer.WriteEndObject();
}
}
}
/// <summary>
/// Task notification for a task that is currently being processed.
/// </summary>
public sealed class WorkingTaskNotificationParams : TaskStatusNotificationParams
{
/// <inheritdoc/>
public override McpTaskStatus Status => McpTaskStatus.Working;
}
/// <summary>
/// Task notification for a task that has completed successfully.
/// </summary>
public sealed class CompletedTaskNotificationParams : TaskStatusNotificationParams
{
/// <inheritdoc/>
public override McpTaskStatus Status => McpTaskStatus.Completed;
/// <summary>
/// Gets or sets the final result of the task.
/// </summary>
[JsonPropertyName("result")]
public required JsonElement Result { get; set; }
}
/// <summary>
/// Task notification for a task that failed.
/// </summary>
public sealed class FailedTaskNotificationParams : TaskStatusNotificationParams
{
/// <inheritdoc/>
public override McpTaskStatus Status => McpTaskStatus.Failed;
/// <summary>
/// Gets or sets the JSON-RPC error that caused the task to fail.
/// </summary>
[JsonPropertyName("error")]
public required JsonElement Error { get; set; }
}
/// <summary>
/// Task notification for a task that was cancelled.
/// </summary>
public sealed class CancelledTaskNotificationParams : TaskStatusNotificationParams
{
/// <inheritdoc/>
public override McpTaskStatus Status => McpTaskStatus.Cancelled;
}
/// <summary>
/// Task notification for a task that requires input from the client.
/// </summary>
public sealed class InputRequiredTaskNotificationParams : TaskStatusNotificationParams
{
/// <inheritdoc/>
public override McpTaskStatus Status => McpTaskStatus.InputRequired;
/// <summary>
/// Gets or sets the server-to-client requests that need to be fulfilled.
/// </summary>
/// <remarks>
/// Keys are arbitrary identifiers for matching requests to responses. Each value is an
/// <see cref="InputRequest"/> wrapping the server-to-client request payload, matching
/// the typed format defined by the Multi Round-Trip Requests (MRTR) extension (SEP-2322).
/// </remarks>
[JsonPropertyName("inputRequests")]
public IDictionary<string, InputRequest>? InputRequests { get; set; }
}

View file

@ -0,0 +1,30 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the parameters for a <c>tasks/update</c> request to provide input responses
/// to outstanding server-to-client requests on a task.
/// </summary>
/// <remarks>
/// <para>
/// When a task requires input from the client (indicated by <see cref="McpTaskStatus.InputRequired"/>),
/// the server includes outstanding requests in the <c>inputRequests</c> field of the <c>tasks/get</c> response.
/// The client provides responses via the inherited <see cref="RequestParams.InputResponses"/> field in
/// <c>tasks/update</c> requests; the wire format matches the typed envelope defined by the Multi Round-Trip
/// Requests (MRTR) extension (SEP-2322).
/// </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 UpdateTaskRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the identifier of the task to update.
/// </summary>
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
}

View file

@ -0,0 +1,22 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Represents the result of a <c>tasks/update</c> request. This is an empty acknowledgement.
/// </summary>
/// <remarks>
/// <para>
/// On success, the server acknowledges the request with an empty result.
/// The acknowledgement is eventually consistent: the server may accept the responses and
/// return the ack before the task's observable status reflects them.
/// </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 UpdateTaskResult : Result
{
}