using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; /// /// Represents the result of a tasks/get request, containing the full task state. /// /// /// /// This is the abstract base for status-specific task results. The concrete type returned depends on the /// task's current : /// /// — /// — /// — /// — /// — /// /// /// /// See the SEP-2663 /// specification for details. /// /// [JsonConverter(typeof(Converter))] public abstract class GetTaskResult : Result { /// Prevent external derivations. private protected GetTaskResult() { } /// /// 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 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? 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()); 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 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()); } 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.JsonContext.Default.InputRequest); } } writer.WriteEndObject(); break; } writer.WriteEndObject(); } } } /// /// Represents a task that is currently being processed by the server. /// /// /// See the SEP-2663 /// specification for details. /// public sealed class WorkingTaskResult : GetTaskResult { /// [JsonPropertyName("status")] public override McpTaskStatus Status => McpTaskStatus.Working; } /// /// Represents a task that has completed successfully, carrying the final result. /// /// /// /// The field contains the result structure matching the original request type. /// For example, a tools/call task would contain the structure. /// This includes tool calls that returned results with isError: true. /// /// /// See the SEP-2663 /// specification for details. /// /// public sealed class CompletedTaskResult : GetTaskResult { /// [JsonPropertyName("status")] public override McpTaskStatus Status => McpTaskStatus.Completed; /// /// Gets or sets the final result of the task as raw JSON. /// /// /// The structure matches the result type of the original request. /// [JsonPropertyName("result")] public required JsonElement Result { get; set; } } /// /// Represents a task that failed due to a JSON-RPC error during execution. /// /// /// /// The field contains the JSON-RPC error object that caused the failure. /// This status must not be used for non-JSON-RPC errors. /// /// /// See the SEP-2663 /// specification for details. /// /// public sealed class FailedTaskResult : GetTaskResult { /// [JsonPropertyName("status")] 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; } } /// /// Represents a task that was cancelled before completion. /// /// /// See the SEP-2663 /// specification for details. /// public sealed class CancelledTaskResult : GetTaskResult { /// [JsonPropertyName("status")] public override McpTaskStatus Status => McpTaskStatus.Cancelled; } /// /// Represents a task that requires input from the client before it can proceed. /// /// /// /// The 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 wrapping the /// server-to-client request payload. /// /// /// 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. /// /// /// See the SEP-2663 /// specification for details. /// /// public sealed class InputRequiredTaskResult : GetTaskResult { /// [JsonPropertyName("status")] 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 /// (e.g., a sampling, elicitation, or roots-list request). /// [JsonPropertyName("inputRequests")] public IDictionary? InputRequests { get; set; } }