using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
///
/// Represents the parameters for a notifications/tasks notification sent by the server
/// to push task status updates to the client.
///
///
///
/// Each notification carries a complete task state for the current status, identical to what
/// tasks/get would have returned at that moment. The concrete type depends on the task's
/// current status:
///
/// - —
/// - —
/// - —
/// - —
/// - —
///
///
///
/// To receive task status notifications, clients send a subscriptions/listen request
/// including the task IDs they are interested in.
///
///
/// See the SEP-2663
/// specification for details.
///
///
[JsonConverter(typeof(Converter))]
public abstract class TaskStatusNotificationParams : NotificationParams
{
/// Prevent external derivations.
private protected TaskStatusNotificationParams()
{
}
///
/// Gets or sets the stable identifier for this task.
///
[JsonPropertyName("taskId")]
public required string TaskId { get; set; }
///
/// Gets or sets the current task status.
///
[JsonPropertyName("status")]
public abstract McpTaskStatus Status { get; }
///
/// Gets or sets an optional message describing the current task state.
///
[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; set; }
///
/// Gets or sets the ISO 8601 timestamp when the task was created.
///
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; set; }
///
/// Gets or sets the ISO 8601 timestamp when the task was last updated.
///
[JsonPropertyName("lastUpdatedAt")]
public required DateTimeOffset LastUpdatedAt { get; set; }
///
/// Gets or sets the time-to-live duration from creation, or for unlimited.
///
[JsonPropertyName("ttlMs")]
public TimeSpan? TimeToLive { get; set; }
///
/// Gets or sets the suggested polling interval in milliseconds.
///
[JsonPropertyName("pollIntervalMs")]
public long? PollIntervalMs { get; set; }
///
/// JSON converter that deserializes to the appropriate
/// concrete subtype based on the status discriminator field.
///
internal sealed class Converter : JsonConverter
{
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? 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());
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(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.JsonContext.Default.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());
}
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.JsonContext.Default.InputRequest);
}
}
writer.WriteEndObject();
break;
}
writer.WriteEndObject();
}
}
}
///
/// Task notification for a task that is currently being processed.
///
public sealed class WorkingTaskNotificationParams : TaskStatusNotificationParams
{
///
public override McpTaskStatus Status => McpTaskStatus.Working;
}
///
/// Task notification for a task that has completed successfully.
///
public sealed class CompletedTaskNotificationParams : TaskStatusNotificationParams
{
///
public override McpTaskStatus Status => McpTaskStatus.Completed;
///
/// Gets or sets the final result of the task.
///
[JsonPropertyName("result")]
public required JsonElement Result { get; set; }
}
///
/// Task notification for a task that failed.
///
public sealed class FailedTaskNotificationParams : TaskStatusNotificationParams
{
///
public override McpTaskStatus Status => McpTaskStatus.Failed;
///
/// Gets or sets the JSON-RPC error that caused the task to fail.
///
[JsonPropertyName("error")]
public required JsonElement Error { get; set; }
}
///
/// Task notification for a task that was cancelled.
///
public sealed class CancelledTaskNotificationParams : TaskStatusNotificationParams
{
///
public override McpTaskStatus Status => McpTaskStatus.Cancelled;
}
///
/// Task notification for a task that requires input from the client.
///
public sealed class InputRequiredTaskNotificationParams : TaskStatusNotificationParams
{
///
public override McpTaskStatus Status => McpTaskStatus.InputRequired;
///
/// Gets or sets the server-to-client requests that need to be fulfilled.
///
///
/// Keys are arbitrary identifiers for matching requests to responses. Each value is an
/// wrapping the server-to-client request payload, matching
/// the typed format defined by the Multi Round-Trip Requests (MRTR) extension (SEP-2322).
///
[JsonPropertyName("inputRequests")]
public IDictionary? InputRequests { get; set; }
}