diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 9020d2fbe..cda842bb4 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -70,6 +70,7 @@ + @@ -78,6 +79,7 @@ + diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index a4a71dd48..864df6859 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -45,3 +45,4 @@ When APIs are marked as obsolete, a diagnostic is emitted to warn users that the | `MCP9004` | In place | opts into the legacy SSE transport which has no built-in HTTP-level backpressure. Use Streamable HTTP instead. See [Stateless — Legacy SSE transport](xref:stateless#legacy-sse-transport) for details. | | `MCP9005` | In place | The Roots, Sampling, and Logging features are deprecated as of specification version 2026-07-28 and may be removed in a future version. See SEP-2577 for more information. | | `MCP9006` | In place | The stateful Streamable HTTP configuration knobs on — `EventStreamStore`, `SessionMigrationHandler`, `PerSessionExecutionContext`, `IdleTimeout`, and `MaxIdleSessionCount` — only apply when `Stateless = false`. Starting with the `2026-07-28` protocol revision, Streamable HTTP no longer supports sessions, and the SDK now defaults `Stateless` to `true`. These knobs remain available for back-compat with the legacy stateful Streamable HTTP transport but new code should target the stateless path. | +| `MCP9007` | In place | The source-compatible `2025-11-30` Tasks APIs in `ModelContextProtocol.Legacy.Tasks-2025-11-30` are retained only to assist migrations from SDK 1.x. Use `ModelContextProtocol.Extensions.Tasks` for new code. | diff --git a/src/Common/McpProtocolVersions.cs b/src/Common/McpProtocolVersions.cs index 09b1f5b3d..0140f8353 100644 --- a/src/Common/McpProtocolVersions.cs +++ b/src/Common/McpProtocolVersions.cs @@ -24,6 +24,15 @@ internal static class McpProtocolVersions /// public const string November2025ProtocolVersion = "2025-11-25"; + /// + /// The 2025-11-30 MCP protocol revision that introduced the experimental Tasks protocol. + /// + /// + /// This revision is retained only so compatibility packages can negotiate and implement + /// historical experimental protocol features. Core does not implement Tasks for this revision. + /// + public const string November2025TasksProtocolVersion = "2025-11-30"; + /// The 2025-06-18 MCP protocol revision. public const string June2025ProtocolVersion = "2025-06-18"; @@ -42,6 +51,7 @@ internal static class McpProtocolVersions March2025ProtocolVersion, June2025ProtocolVersion, November2025ProtocolVersion, + November2025TasksProtocolVersion, ]; /// diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs index 61a0613df..7dbee695a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs @@ -53,7 +53,7 @@ public sealed class McpClientOptions /// /// /// Supported values are 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, - /// and 2026-07-28. + /// 2025-11-30, and 2026-07-28. /// /// /// When (the default), the client prefers the latest revision (2026-07-28), diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index 2828602fc..bc1fc3463 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; using ModelContextProtocol.Client; using ModelContextProtocol.Server; @@ -84,4 +85,15 @@ public sealed class ClientCapabilities /// [JsonPropertyName("extensions")] public IDictionary? Extensions { get; set; } + + /// + /// Gets or sets unrecognized capability properties for protocol extensions that define + /// top-level capability names. + /// + /// + /// Extension packages should prefer when the negotiated protocol + /// supports it. This property preserves capability names defined by historical protocol revisions. + /// + [JsonExtensionData] + public IDictionary? AdditionalProperties { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index 95cfa078d..4bef4efb5 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; using ModelContextProtocol.Server; @@ -82,4 +83,15 @@ public sealed class ServerCapabilities /// [JsonPropertyName("extensions")] public IDictionary? Extensions { get; set; } + + /// + /// Gets or sets unrecognized capability properties for protocol extensions that define + /// top-level capability names. + /// + /// + /// Extension packages should prefer when the negotiated protocol + /// supports it. This property preserves capability names defined by historical protocol revisions. + /// + [JsonExtensionData] + public IDictionary? AdditionalProperties { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index 274d53be3..76217520d 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -145,6 +145,12 @@ public JsonElement? OutputSchema [JsonPropertyName("_meta")] public JsonObject? Meta { get; set; } + /// + /// Gets or sets unrecognized top-level tool properties defined by protocol extensions. + /// + [JsonExtensionData] + public IDictionary? AdditionalProperties { get; set; } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay { diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 82b6ceb9d..e7d9910b1 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -264,6 +264,7 @@ internal Tool BuildLegacyWireProtocolTool() Annotations = ProtocolTool.Annotations, Icons = ProtocolTool.Icons, Meta = ProtocolTool.Meta, + AdditionalProperties = ProtocolTool.AdditionalProperties, }; } diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 8b38421c4..111ba3139 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -192,6 +192,14 @@ private JsonRpcMessageFilter PrependMetaReadingFilter(JsonRpcMessageFilter inner bool hasProtocolVersionMeta = HasMetaKey(request, MetaKeys.ProtocolVersion); bool hasReservedPerRequestMeta = TryGetPerRequestMetadataKey(request, out var reservedPerRequestMetaKey); + // Initialize-handshake protocols establish their version once per session. Surface that + // established version on later raw requests so extension handlers can gate their wire behavior. + if (context?.ProtocolVersion is null && _negotiatedProtocolVersion is { } negotiatedProtocolVersion) + { + context ??= request.Context = new JsonRpcMessageContext(); + context.ProtocolVersion = negotiatedProtocolVersion; + } + if (context?.ProtocolVersion is { } protocolVersion) { bool protocolVersionAlreadyEstablished = _negotiatedProtocolVersion is not null; @@ -994,6 +1002,7 @@ private void ConfigureExperimentalAndExtensions(McpServerOptions options) { ServerCapabilities.Experimental = options.Capabilities?.Experimental; ServerCapabilities.Extensions = options.Capabilities?.Extensions; + ServerCapabilities.AdditionalProperties = options.Capabilities?.AdditionalProperties; } private void ConfigureCustomRequestHandlers(McpServerOptions options) @@ -1011,19 +1020,48 @@ private void ConfigureCustomRequestHandlers(McpServerOptions options) throw new InvalidOperationException( $"A custom request handler registered through {nameof(McpServerOptions)}.{nameof(McpServerOptions.RequestHandlers)} has a null or empty {nameof(McpServerRequestHandler.Method)}."); } + } + + foreach (var groupedHandlers in customHandlers.GroupBy(static entry => entry.Method, StringComparer.Ordinal)) + { + McpServerRequestHandler[] handlers = [.. groupedHandlers]; + string method = handlers[0].Method; // Custom handlers are registered after all built-in handlers, so a method already present - // belongs to a built-in method (e.g. initialize, tools/call) or an earlier custom handler. - // Silently overwriting it would bypass the built-in handler's filters and protocol gating, - // so reject the collision instead. - if (_requestHandlers.ContainsKey(entry.Method)) + // belongs to a built-in method (e.g. initialize or tools/call). Silently overwriting it + // would bypass the built-in handler's filters and protocol gating, so reject the collision. + if (_requestHandlers.ContainsKey(method)) { throw new InvalidOperationException( $"A custom request handler registered through {nameof(McpServerOptions)}.{nameof(McpServerOptions.RequestHandlers)} " + - $"uses the method '{entry.Method}', which is already handled by the server. Custom handlers cannot replace built-in methods or other custom handlers."); + $"uses the method '{method}', which is already handled by the server. Custom handlers cannot replace built-in methods or other custom handlers."); } - SetRawHandler(entry.Method, entry.Handler); + if (handlers.Length == 1) + { + SetRawHandler(method, handlers[0].Handler); + continue; + } + + if (handlers.Any(static handler => handler.IsApplicable is null)) + { + throw new InvalidOperationException( + $"Multiple custom request handlers are registered for method '{method}'. " + + $"Each handler must specify {nameof(McpServerRequestHandler.IsApplicable)} to share a method."); + } + + SetRawHandler(method, async (request, cancellationToken) => + { + foreach (var handler in handlers) + { + if (handler.IsApplicable!(request)) + { + return await handler.Handler(request, cancellationToken).ConfigureAwait(false); + } + } + + throw new McpProtocolException($"Method '{method}' is not available.", McpErrorCode.MethodNotFound); + }); } #pragma warning restore MCPEXP002 } diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index a84bd600e..fc494c8f7 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -35,8 +35,8 @@ public sealed class McpServerOptions /// /// /// The protocol version defines which features and message formats this server supports. Supported - /// values are 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, and - /// 2026-07-28. + /// values are 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, + /// 2025-11-30, and 2026-07-28. /// /// /// If , the server supports all of the versions listed above. For clients using diff --git a/src/ModelContextProtocol.Core/Server/McpServerRequestHandler.cs b/src/ModelContextProtocol.Core/Server/McpServerRequestHandler.cs index be845f599..7ff26811d 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerRequestHandler.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerRequestHandler.cs @@ -24,6 +24,16 @@ public sealed class McpServerRequestHandler /// public required string Method { get; init; } + /// + /// Gets an optional predicate that determines whether this handler applies to an incoming request. + /// + /// + /// When multiple custom handlers register the same method, each handler must specify this + /// predicate. The first applicable handler is invoked, allowing extensions to share a method + /// name while dispatching by negotiated protocol version or another request characteristic. + /// + public Func? IsApplicable { get; init; } + /// /// Gets the handler function that processes incoming requests for the specified method. /// diff --git a/src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs b/src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs index 06073ce17..cdd727b69 100644 --- a/src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs +++ b/src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs @@ -62,9 +62,9 @@ public void PostConfigure(string? name, McpServerOptions options) } options.RequestHandlers ??= new List(); - options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksGet, Handler = HandleGetTask }); - options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksUpdate, Handler = HandleUpdateTask }); - options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksCancel, Handler = HandleCancelTask }); + options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksGet, IsApplicable = IsJuly2026OrLaterProtocolRequest, Handler = HandleGetTask }); + options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksUpdate, IsApplicable = IsJuly2026OrLaterProtocolRequest, Handler = HandleUpdateTask }); + options.RequestHandlers.Add(new McpServerRequestHandler { Method = TasksProtocol.MethodTasksCancel, IsApplicable = IsJuly2026OrLaterProtocolRequest, Handler = HandleCancelTask }); // Use a filter rather than a handler so it wraps around Core's tool dispatch. // This ensures it intercepts tool calls BEFORE the tool is invoked, allowing diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Client/LegacyTasksClientExtensions.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Client/LegacyTasksClientExtensions.cs new file mode 100644 index 000000000..9b7786763 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Client/LegacyTasksClientExtensions.cs @@ -0,0 +1,260 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Provides client APIs for the 2025-11-30 Tasks draft. +public static class LegacyTasksClientExtensions +{ + /// Configures a client to negotiate and advertise the legacy Tasks draft. + public static McpClientOptions EnableLegacyTasks(this McpClientOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.ProtocolVersion is not null && + !string.Equals(options.ProtocolVersion, LegacyTasksProtocol.ProtocolVersion, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"Legacy Tasks requires protocol version '{LegacyTasksProtocol.ProtocolVersion}', but the client is configured for '{options.ProtocolVersion}'."); + } + + options.ProtocolVersion = LegacyTasksProtocol.ProtocolVersion; + AddLegacyTasksCapability(options); + return options; + } + + /// + /// Advertises support for the legacy Tasks draft without changing protocol negotiation. + /// + /// + /// Call this method when using with a client whose + /// remains unset. The client will continue to + /// prefer the newest protocol revision and use Core's normal fallback behavior. + /// + public static McpClientOptions EnableTasksMigration(this McpClientOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + AddLegacyTasksCapability(options); + return options; + } + + private static void AddLegacyTasksCapability(McpClientOptions options) + { + options.Capabilities ??= new(); + options.Capabilities.AdditionalProperties ??= new Dictionary(); + options.Capabilities.AdditionalProperties[LegacyTasksProtocol.CapabilityName] = CreateCapabilityElement(); + } + + /// Calls a tool and returns either its immediate result or a created legacy task. + public static async ValueTask CallToolAsLegacyTaskAsync( + this McpClient client, + CallToolRequestParams requestParams, + McpLegacyTaskMetadata? taskMetadata = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (requestParams is null) + { + throw new ArgumentNullException(nameof(requestParams)); + } + + ThrowIfLegacyTasksUnavailable(client, nameof(CallToolAsLegacyTaskAsync)); + + JsonObject parameters = new() + { + ["name"] = requestParams.Name, + [LegacyTasksProtocol.TaskPropertyName] = JsonSerializer.SerializeToNode( + taskMetadata ?? new McpLegacyTaskMetadata(), + LegacyTasksJsonContext.Default.McpLegacyTaskMetadata), + }; + + if (requestParams.Arguments is not null) + { + parameters["arguments"] = JsonSerializer.SerializeToNode( + requestParams.Arguments, + McpJsonUtilities.DefaultOptions.GetTypeInfo>()); + } + + if (requestParams.Meta is not null) + { + parameters["_meta"] = requestParams.Meta.DeepClone(); + } + + JsonRpcResponse response = await client.SendRequestAsync( + new JsonRpcRequest + { + Method = RequestMethods.ToolsCall, + Params = parameters, + }, + cancellationToken).ConfigureAwait(false); + + if (response.Result is JsonObject result && result.ContainsKey(LegacyTasksProtocol.TaskPropertyName)) + { + var created = result.Deserialize(LegacyTasksJsonContext.Default.CreateLegacyTaskResult) + ?? throw new JsonException("The legacy task creation response was empty."); + return new LegacyTaskCallResult(created.Task); + } + + var callResult = JsonSerializer.Deserialize( + response.Result, + McpJsonUtilities.DefaultOptions.GetTypeInfo()) + ?? throw new JsonException("The tools/call response was empty."); + return new LegacyTaskCallResult(callResult); + } + + /// Gets the status of a legacy task. + public static ValueTask GetLegacyTaskAsync( + this McpClient client, + string taskId, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (taskId is null) + { + throw new ArgumentNullException(nameof(taskId)); + } + + ThrowIfLegacyTasksUnavailable(client, nameof(GetLegacyTaskAsync)); + + return client.SendRequestAsync( + LegacyTasksProtocol.GetTaskMethod, + new GetLegacyTaskRequestParams { TaskId = taskId }, + LegacyTasksJsonContext.Default.Options, + cancellationToken: cancellationToken); + } + + /// Lists legacy tasks exposed by the connected server. + public static ValueTask ListLegacyTasksAsync( + this McpClient client, + string? cursor = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + ThrowIfLegacyTasksUnavailable(client, nameof(ListLegacyTasksAsync)); + + return client.SendRequestAsync( + LegacyTasksProtocol.ListTasksMethod, + new ListLegacyTasksRequestParams { Cursor = cursor }, + LegacyTasksJsonContext.Default.Options, + cancellationToken: cancellationToken); + } + + /// Waits for and gets the raw payload of a terminal legacy task. + public static async ValueTask GetLegacyTaskPayloadAsync( + this McpClient client, + string taskId, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (taskId is null) + { + throw new ArgumentNullException(nameof(taskId)); + } + + ThrowIfLegacyTasksUnavailable(client, nameof(GetLegacyTaskPayloadAsync)); + + JsonRpcResponse response = await client.SendRequestAsync( + new JsonRpcRequest + { + Method = LegacyTasksProtocol.GetTaskResultMethod, + Params = JsonSerializer.SerializeToNode( + new GetLegacyTaskPayloadRequestParams { TaskId = taskId }, + LegacyTasksJsonContext.Default.GetLegacyTaskPayloadRequestParams), + }, + cancellationToken).ConfigureAwait(false); + + using var document = JsonDocument.Parse(response.Result?.ToJsonString() ?? "null"); + return document.RootElement.Clone(); + } + + /// Cancels a legacy task. + public static ValueTask CancelLegacyTaskAsync( + this McpClient client, + string taskId, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (taskId is null) + { + throw new ArgumentNullException(nameof(taskId)); + } + + ThrowIfLegacyTasksUnavailable(client, nameof(CancelLegacyTaskAsync)); + + return client.SendRequestAsync( + LegacyTasksProtocol.CancelTaskMethod, + new CancelLegacyTaskRequestParams { TaskId = taskId }, + LegacyTasksJsonContext.Default.Options, + cancellationToken: cancellationToken); + } + + private static JsonElement CreateCapabilityElement() => + JsonSerializer.SerializeToElement( + new JsonObject + { + ["list"] = new JsonObject(), + ["cancel"] = new JsonObject(), + }, + McpJsonUtilities.DefaultOptions.GetTypeInfo()); + + private static void ThrowIfLegacyTasksUnavailable(McpClient client, string operation) + { + if (!string.Equals(client.NegotiatedProtocolVersion, LegacyTasksProtocol.ProtocolVersion, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"'{operation}' requires negotiated protocol version '{LegacyTasksProtocol.ProtocolVersion}', " + + $"but the connected server negotiated '{client.NegotiatedProtocolVersion ?? "(none)"}'."); + } + + if (client.ServerCapabilities.AdditionalProperties?.ContainsKey(LegacyTasksProtocol.CapabilityName) is not true) + { + throw new InvalidOperationException("The connected server does not advertise legacy Tasks support."); + } + } +} + +/// Represents either an immediate tool result or a created legacy task. +public sealed class LegacyTaskCallResult +{ + internal LegacyTaskCallResult(CallToolResult result) => Result = result; + + internal LegacyTaskCallResult(McpLegacyTask task) => Task = task; + + /// Gets whether the server created a task. + public bool IsTask => Task is not null; + + /// Gets the immediate tool result when is false. + public CallToolResult? Result { get; } + + /// Gets the created task when is true. + public McpLegacyTask? Task { get; } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Client/McpTaskMigrationClient.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Client/McpTaskMigrationClient.cs new file mode 100644 index 000000000..9394409d1 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Client/McpTaskMigrationClient.cs @@ -0,0 +1,235 @@ +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Client; +using ModelContextProtocol.Extensions.Tasks; +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Identifies the Tasks protocol implementation selected after client negotiation. +public enum McpTaskMigrationMode +{ + /// The 2025-11-30 legacy Tasks draft is active. + Legacy, + + /// The 2026-07-28 or later Tasks extension is active. + Modern, +} + +/// +/// Provides a post-negotiation facade over the legacy and modern MCP Tasks implementations. +/// +/// +/// Create an instance after connecting an . The facade does not alter +/// protocol negotiation; it selects the implementation from the client's negotiated version. +/// +public sealed class McpTaskMigrationClient +{ + private readonly McpClient _client; + + internal McpTaskMigrationClient(McpClient client, McpTaskMigrationMode mode) + { + _client = client; + Mode = mode; + } + + /// Gets the Tasks implementation selected for this connection. + public McpTaskMigrationMode Mode { get; } + + /// + /// Gets whether a tool should use task-augmented execution for the selected Tasks implementation. + /// + /// + /// For the legacy draft, this method reads the execution.taskSupport property from the + /// tool returned by tools/list. For the modern Tasks extension, task support is a + /// server-level extension capability and applies to all tools registered with that extension. + /// + public bool SupportsTaskExecution(McpClientTool tool) + { + if (tool is null) + { + throw new ArgumentNullException(nameof(tool)); + } + + if (Mode == McpTaskMigrationMode.Modern) + { + return _client.ServerCapabilities.Extensions?.ContainsKey(TasksProtocol.ExtensionId) is true; + } + + return tool.ProtocolTool.AdditionalProperties?.TryGetValue("execution", out JsonElement execution) is true && + execution.ValueKind == JsonValueKind.Object && + execution.TryGetProperty("taskSupport", out JsonElement taskSupport) && + taskSupport.ValueKind == JsonValueKind.String && + string.Equals(taskSupport.GetString(), "optional", StringComparison.Ordinal); + } + + /// + /// Executes a listed tool using task polling only when the selected protocol indicates that the tool supports it. + /// + /// The tool description returned from ListToolsAsync. + /// The parameters for the tool invocation. + /// The maximum number of unchanged task status polls to allow. + /// The cancellation token for the invocation. + /// The direct or task-polled tool result. + public ValueTask ExecuteToolAsync( + McpClientTool tool, + CallToolRequestParams requestParams, + int maxConsecutiveStuckPolls = 60, + CancellationToken cancellationToken = default) + { + if (tool is null) + { + throw new ArgumentNullException(nameof(tool)); + } + + if (requestParams is null) + { + throw new ArgumentNullException(nameof(requestParams)); + } + + if (!string.Equals(tool.ProtocolTool.Name, requestParams.Name, StringComparison.Ordinal)) + { + throw new ArgumentException( + $"The request tool name '{requestParams.Name}' does not match the listed tool name '{tool.ProtocolTool.Name}'.", + nameof(requestParams)); + } + + return SupportsTaskExecution(tool) + ? CallToolWithPollingAsync(requestParams, maxConsecutiveStuckPolls, cancellationToken) + : _client.CallToolAsync(requestParams, cancellationToken); + } + + /// Calls a tool and returns either its immediate result or a task created by the selected implementation. + public async ValueTask CallToolAsTaskAsync( + CallToolRequestParams requestParams, + CancellationToken cancellationToken = default) + { + if (requestParams is null) + { + throw new ArgumentNullException(nameof(requestParams)); + } + + if (Mode == McpTaskMigrationMode.Modern) + { + var modernResult = await _client.CallToolAsTaskAsync(requestParams, cancellationToken).ConfigureAwait(false); + return modernResult.IsTask + ? new McpTaskMigrationCallResult(Mode, modernResult.TaskCreated!) + : new McpTaskMigrationCallResult(Mode, modernResult.Result!); + } + + var legacyResult = await _client.CallToolAsLegacyTaskAsync(requestParams, cancellationToken: cancellationToken).ConfigureAwait(false); + return legacyResult.IsTask + ? new McpTaskMigrationCallResult(Mode, legacyResult.Task!) + : new McpTaskMigrationCallResult(Mode, legacyResult.Result!); + } + + /// Calls a tool and waits for a task result when the selected implementation creates a task. + public async ValueTask CallToolWithPollingAsync( + CallToolRequestParams requestParams, + int maxConsecutiveStuckPolls = 60, + CancellationToken cancellationToken = default) + { + if (requestParams is null) + { + throw new ArgumentNullException(nameof(requestParams)); + } + + if (Mode == McpTaskMigrationMode.Modern) + { + return await _client.CallToolWithPollingAsync( + requestParams, + maxConsecutiveStuckPolls, + cancellationToken).ConfigureAwait(false); + } + + var legacyResult = await _client.CallToolAsLegacyTaskAsync(requestParams, cancellationToken: cancellationToken).ConfigureAwait(false); + if (!legacyResult.IsTask) + { + return legacyResult.Result!; + } + + JsonElement payload = await _client.GetLegacyTaskPayloadAsync(legacyResult.Task!.TaskId, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(payload, McpJsonUtilities.DefaultOptions.GetTypeInfo()) + ?? throw new JsonException("The legacy task result payload was empty."); + } +} + +/// Provides factory methods for . +public static class McpTaskMigrationClientExtensions +{ + /// + /// Creates a Tasks migration facade for a connected client. + /// + /// The connected MCP client. + /// + /// An optional logger that records servers for which the legacy Tasks draft was selected. + /// + /// A facade bound to the Tasks implementation for the negotiated protocol version. + /// + /// The client did not negotiate the legacy Tasks protocol or a version supporting the modern Tasks extension. + /// + public static McpTaskMigrationClient CreateTaskMigrationClient(this McpClient client, ILogger? logger = null) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (McpProtocolVersions.IsJuly2026OrLaterProtocolVersion(client.NegotiatedProtocolVersion)) + { + return new McpTaskMigrationClient(client, McpTaskMigrationMode.Modern); + } + + if (string.Equals(client.NegotiatedProtocolVersion, LegacyTasksProtocol.ProtocolVersion, StringComparison.Ordinal)) + { + logger?.LogInformation( + "Using the legacy MCP Tasks implementation for server {ServerName} {ServerVersion} negotiated at protocol version {ProtocolVersion}.", + client.ServerInfo.Name, + client.ServerInfo.Version, + client.NegotiatedProtocolVersion); + return new McpTaskMigrationClient(client, McpTaskMigrationMode.Legacy); + } + + throw new InvalidOperationException( + $"Tasks migration requires protocol version '{LegacyTasksProtocol.ProtocolVersion}' or " + + $"'{McpProtocolVersions.July2026ProtocolVersion}' or later, but the connected server negotiated " + + $"'{client.NegotiatedProtocolVersion ?? "(none)"}'."); + } +} + +/// Represents either an immediate tool result or a task created by the selected Tasks implementation. +public sealed class McpTaskMigrationCallResult +{ + internal McpTaskMigrationCallResult(McpTaskMigrationMode mode, CallToolResult result) + { + Mode = mode; + Result = result; + } + + internal McpTaskMigrationCallResult(McpTaskMigrationMode mode, McpLegacyTask task) + { + Mode = mode; + LegacyTask = task; + } + + internal McpTaskMigrationCallResult(McpTaskMigrationMode mode, ModelContextProtocol.Extensions.Tasks.CreateTaskResult task) + { + Mode = mode; + ModernTask = task; + } + + /// Gets the Tasks implementation that produced this result. + public McpTaskMigrationMode Mode { get; } + + /// Gets whether the server created a task. + public bool IsTask => LegacyTask is not null || ModernTask is not null; + + /// Gets the immediate tool result when is . + public CallToolResult? Result { get; } + + /// Gets the task created by the 2025-11-30 legacy Tasks draft. + public McpLegacyTask? LegacyTask { get; } + + /// Gets the task created by the 2026-07-28 or later Tasks extension. + public ModelContextProtocol.Extensions.Tasks.CreateTaskResult? ModernTask { get; } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyMcpClientTaskExtensions.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyMcpClientTaskExtensions.cs new file mode 100644 index 000000000..28ff5554b --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyMcpClientTaskExtensions.cs @@ -0,0 +1,299 @@ +using ModelContextProtocol.Legacy.Tasks; +using ModelContextProtocol.Protocol; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using LegacyTasks = ModelContextProtocol.Legacy.Tasks; + +namespace ModelContextProtocol.Client; + +/// Provides source-compatible client APIs for the 2025-11-30 Tasks draft. +/// +/// These extension methods retain the 1.x method names for a source migration. They require a +/// client that negotiated 2025-11-30; use the modern Tasks extension for newer protocols. +/// +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public static class LegacyMcpClientTaskExtensions +{ + /// Invokes a tool as a legacy task. + [RequiresDynamicCode("The legacy object-based argument signature requires runtime JSON serialization.")] + [RequiresUnreferencedCode("The legacy object-based argument signature requires runtime JSON serialization.")] + public static async ValueTask CallToolAsTaskAsync( + this McpClient client, + string toolName, + IReadOnlyDictionary? arguments = null, + McpTaskMetadata? taskMetadata = null, + IProgress? progress = null, + RequestOptions? options = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (toolName is null) + { + throw new ArgumentNullException(nameof(toolName)); + } + + var requestParams = new CallToolRequestParams + { + Name = toolName, + Arguments = SerializeArguments(arguments, options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions), + Meta = options?.GetMetaForRequest(), + }; + + if (progress is null) + { + return ToCompatibilityTask(await client.CallToolAsLegacyTaskAsync( + requestParams, + ToLegacyTaskMetadata(taskMetadata), + cancellationToken).ConfigureAwait(false)); + } + + var progressToken = new ProgressToken(Guid.NewGuid().ToString("N")); + requestParams.Meta = requestParams.Meta is null ? [] : (JsonObject)requestParams.Meta.DeepClone(); + requestParams.Meta["progressToken"] = progressToken.ToString(); + + await using var registration = client.RegisterNotificationHandler( + NotificationMethods.ProgressNotification, + (notification, _) => + { + if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions.GetTypeInfo()) is { } progressNotification && + progressNotification.ProgressToken == progressToken) + { + progress.Report(progressNotification.Progress); + } + + return default; + }); + + return ToCompatibilityTask(await client.CallToolAsLegacyTaskAsync( + requestParams, + ToLegacyTaskMetadata(taskMetadata), + cancellationToken).ConfigureAwait(false)); + } + + /// Gets the state of a legacy task. + public static async ValueTask GetTaskAsync( + this McpClient client, + string taskId, + RequestOptions? options = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("The task identifier must not be empty.", nameof(taskId)); + } + + return ToCompatibilityTask(await client.GetLegacyTaskAsync(taskId, cancellationToken).ConfigureAwait(false)); + } + + /// Gets the result payload of a legacy task. + public static ValueTask GetTaskResultAsync( + this McpClient client, + string taskId, + RequestOptions? options = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("The task identifier must not be empty.", nameof(taskId)); + } + + return client.GetLegacyTaskPayloadAsync(taskId, cancellationToken); + } + + /// Lists every legacy task visible to the client. + public static async ValueTask> ListTasksAsync( + this McpClient client, + RequestOptions? options = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + List tasks = []; + string? cursor = null; + do + { + var page = await client.ListLegacyTasksAsync(cursor, cancellationToken).ConfigureAwait(false); + tasks.AddRange(page.Tasks.Select(ToCompatibilityTask)); + cursor = page.NextCursor; + } + while (cursor is not null); + + return tasks; + } + + /// Lists one page of legacy tasks. + public static async ValueTask ListTasksAsync( + this McpClient client, + ListTasksRequestParams requestParams, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (requestParams is null) + { + throw new ArgumentNullException(nameof(requestParams)); + } + + var page = await client.ListLegacyTasksAsync(requestParams.Cursor, cancellationToken).ConfigureAwait(false); + return new ListTasksResult + { + Tasks = page.Tasks.Select(ToCompatibilityTask).ToList(), + NextCursor = page.NextCursor, + }; + } + + /// Cancels a legacy task. + public static async ValueTask CancelTaskAsync( + this McpClient client, + string taskId, + RequestOptions? options = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("The task identifier must not be empty.", nameof(taskId)); + } + + return ToCompatibilityTask(await client.CancelLegacyTaskAsync(taskId, cancellationToken).ConfigureAwait(false)); + } + + /// Polls a legacy task until it reaches a terminal state. + public static async ValueTask PollTaskUntilCompleteAsync( + this McpClient client, + string taskId, + RequestOptions? options = null, + CancellationToken cancellationToken = default) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (string.IsNullOrWhiteSpace(taskId)) + { + throw new ArgumentException("The task identifier must not be empty.", nameof(taskId)); + } + + while (true) + { + var task = await client.GetTaskAsync(taskId, options, cancellationToken).ConfigureAwait(false); + if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled) + { + return task; + } + + await Task.Delay(task.PollInterval ?? TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + } + } + + [RequiresDynamicCode("The legacy object-based argument signature requires runtime JSON serialization.")] + [RequiresUnreferencedCode("The legacy object-based argument signature requires runtime JSON serialization.")] + private static IDictionary? SerializeArguments( + IReadOnlyDictionary? arguments, + JsonSerializerOptions serializerOptions) + { + if (arguments is null) + { + return null; + } + + var serializedArguments = new Dictionary(arguments.Count, StringComparer.Ordinal); + foreach (var argument in arguments) + { + serializedArguments.Add(argument.Key, JsonSerializer.SerializeToElement(argument.Value, serializerOptions)); + } + + return serializedArguments; + } + + private static LegacyTasks.McpLegacyTaskMetadata ToLegacyTaskMetadata(McpTaskMetadata? metadata) => + new() { TimeToLive = metadata?.TimeToLive }; + + private static McpTask ToCompatibilityTask(LegacyTasks.McpLegacyTask task) => + ToCompatibilityTask( + task.TaskId, + task.Status, + task.StatusMessage, + task.CreatedAt, + task.LastUpdatedAt, + task.TimeToLive, + task.PollInterval); + + private static McpTask ToCompatibilityTask(LegacyTasks.GetLegacyTaskResult task) => + ToCompatibilityTask( + task.TaskId, + task.Status, + task.StatusMessage, + task.CreatedAt, + task.LastUpdatedAt, + task.TimeToLive, + task.PollInterval); + + private static McpTask ToCompatibilityTask(LegacyTasks.CancelLegacyTaskResult task) => + ToCompatibilityTask( + task.TaskId, + task.Status, + task.StatusMessage, + task.CreatedAt, + task.LastUpdatedAt, + task.TimeToLive, + task.PollInterval); + + private static McpTask ToCompatibilityTask( + string taskId, + LegacyTasks.McpLegacyTaskStatus status, + string? statusMessage, + DateTimeOffset createdAt, + DateTimeOffset lastUpdatedAt, + TimeSpan? timeToLive, + TimeSpan? pollInterval) => + new() + { + TaskId = taskId, + Status = status switch + { + LegacyTasks.McpLegacyTaskStatus.Working => McpTaskStatus.Working, + LegacyTasks.McpLegacyTaskStatus.InputRequired => McpTaskStatus.InputRequired, + LegacyTasks.McpLegacyTaskStatus.Completed => McpTaskStatus.Completed, + LegacyTasks.McpLegacyTaskStatus.Failed => McpTaskStatus.Failed, + LegacyTasks.McpLegacyTaskStatus.Cancelled => McpTaskStatus.Cancelled, + _ => throw new InvalidOperationException($"Unknown legacy task status '{status}'."), + }, + StatusMessage = statusMessage, + CreatedAt = createdAt, + LastUpdatedAt = lastUpdatedAt, + TimeToLive = timeToLive, + PollInterval = pollInterval, + }; + + private static McpTask ToCompatibilityTask(LegacyTasks.LegacyTaskCallResult result) => + result.Task is { } task + ? ToCompatibilityTask(task) + : throw new InvalidOperationException("The legacy server returned an immediate tool result instead of a task."); +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyTasksApiObsoletion.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyTasksApiObsoletion.cs new file mode 100644 index 000000000..5de4281b2 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyTasksApiObsoletion.cs @@ -0,0 +1,8 @@ +namespace ModelContextProtocol.Legacy.Tasks; + +internal static class LegacyTasksApiObsoletion +{ + public const string DiagnosticId = "MCP9007"; + public const string Message = "The 2025-11-30 Tasks APIs are retained only for source migration from MCP C# SDK 1.x. Use ModelContextProtocol.Extensions.Tasks for new code."; + public const string Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#obsolete-apis"; +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyTasksProtocolCompatibility.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyTasksProtocolCompatibility.cs new file mode 100644 index 000000000..3186e8262 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Compatibility/LegacyTasksProtocolCompatibility.cs @@ -0,0 +1,362 @@ +using ModelContextProtocol.Legacy.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol; + +/// Represents a task from the 2025-11-30 Tasks draft. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class McpTask +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the task status. + [JsonPropertyName("status")] + public required McpTaskStatus Status { get; set; } + + /// Gets or sets an optional task status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status-polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents a status from the 2025-11-30 Tasks draft. +[JsonConverter(typeof(LegacyCompatibilityTaskStatusConverter))] +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public enum McpTaskStatus +{ + /// The task is executing. + Working, + + /// The task requires input. + InputRequired, + + /// The task completed successfully. + Completed, + + /// The task failed. + Failed, + + /// The task was cancelled. + Cancelled, +} + +/// Represents metadata that augments a legacy task request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class McpTaskMetadata +{ + /// Gets or sets the requested task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } +} + +/// Represents the alternate result returned for a task-augmented request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CreateTaskResult : Result +{ + /// Gets or sets the created task. + [JsonPropertyName("task")] + public required McpTask Task { get; set; } +} + +/// Represents parameters for a legacy tasks/get request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class GetTaskRequestParams : RequestParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } +} + +/// Represents the result of a legacy tasks/get request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class GetTaskResult : Result +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the task status. + [JsonPropertyName("status")] + public required McpTaskStatus Status { get; set; } + + /// Gets or sets an optional task status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status-polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents parameters for a legacy tasks/result request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class GetTaskPayloadRequestParams : RequestParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } +} + +/// Represents parameters for a legacy tasks/list request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class ListTasksRequestParams +{ + /// Gets or sets the optional pagination cursor. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } +} + +/// Represents the result of a legacy tasks/list request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class ListTasksResult +{ + /// Gets or sets the tasks in this legacy task page. + [JsonPropertyName("tasks")] + public required IList Tasks { get; set; } + + /// Gets or sets the optional next-page cursor. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } +} + +/// Represents parameters for a legacy tasks/cancel request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CancelMcpTaskRequestParams : RequestParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } +} + +/// Represents the result of a legacy tasks/cancel request. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CancelMcpTaskResult : Result +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the task status. + [JsonPropertyName("status")] + public required McpTaskStatus Status { get; set; } + + /// Gets or sets an optional task status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status-polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents a legacy task status notification. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class McpTaskStatusNotificationParams : NotificationParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the task status. + [JsonPropertyName("status")] + public required McpTaskStatus Status { get; set; } + + /// Gets or sets an optional task status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status-polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyCompatibilityTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents the legacy top-level Tasks capability. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class McpTasksCapability +{ + /// Gets or sets list support. + [JsonPropertyName("list")] + public ListMcpTasksCapability? List { get; set; } + + /// Gets or sets cancel support. + [JsonPropertyName("cancel")] + public CancelMcpTasksCapability? Cancel { get; set; } + + /// Gets or sets task-augmented request support. + [JsonPropertyName("requests")] + public RequestMcpTasksCapability? Requests { get; set; } +} + +/// Represents legacy task support by request family. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class RequestMcpTasksCapability +{ + /// Gets or sets tool task support. + [JsonPropertyName("tools")] + public ToolsMcpTasksCapability? Tools { get; set; } + + /// Gets or sets sampling task support. + [JsonPropertyName("sampling")] + public SamplingMcpTasksCapability? Sampling { get; set; } + + /// Gets or sets elicitation task support. + [JsonPropertyName("elicitation")] + public ElicitationMcpTasksCapability? Elicitation { get; set; } +} + +/// Represents legacy tool task support. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class ToolsMcpTasksCapability +{ + /// Gets or sets task support for tools/call. + [JsonPropertyName("call")] + public CallToolMcpTasksCapability? Call { get; set; } +} + +/// Represents legacy sampling task support. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class SamplingMcpTasksCapability +{ + /// Gets or sets task support for sampling/createMessage. + [JsonPropertyName("createMessage")] + public CreateMessageMcpTasksCapability? CreateMessage { get; set; } +} + +/// Represents legacy elicitation task support. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class ElicitationMcpTasksCapability +{ + /// Gets or sets task support for elicitation/create. + [JsonPropertyName("create")] + public CreateElicitationMcpTasksCapability? Create { get; set; } +} + +/// Represents legacy list-task support. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class ListMcpTasksCapability; + +/// Represents legacy cancel-task support. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CancelMcpTasksCapability; + +/// Represents legacy task support for tools/call. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CallToolMcpTasksCapability; + +/// Represents legacy task support for sampling/createMessage. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CreateMessageMcpTasksCapability; + +/// Represents legacy task support for elicitation/create. +[Obsolete(LegacyTasksApiObsoletion.Message, DiagnosticId = LegacyTasksApiObsoletion.DiagnosticId, UrlFormat = LegacyTasksApiObsoletion.Url)] +public sealed class CreateElicitationMcpTasksCapability; + +internal sealed class LegacyCompatibilityTaskStatusConverter : JsonConverter +{ + public override McpTaskStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.GetString() switch + { + "working" => McpTaskStatus.Working, + "input_required" => McpTaskStatus.InputRequired, + "completed" => McpTaskStatus.Completed, + "failed" => McpTaskStatus.Failed, + "cancelled" => McpTaskStatus.Cancelled, + var value => throw new JsonException($"Unknown legacy task status '{value}'."), + }; + + public override void Write(Utf8JsonWriter writer, McpTaskStatus value, JsonSerializerOptions options) => + writer.WriteStringValue(value switch + { + McpTaskStatus.Working => "working", + McpTaskStatus.InputRequired => "input_required", + McpTaskStatus.Completed => "completed", + McpTaskStatus.Failed => "failed", + McpTaskStatus.Cancelled => "cancelled", + _ => throw new JsonException($"Unknown legacy task status '{value}'."), + }); +} + +internal sealed class LegacyCompatibilityTimeSpanMillisecondsConverter : JsonConverter +{ + public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.Number when reader.TryGetInt64(out long milliseconds) => TimeSpan.FromMilliseconds(milliseconds), + _ => throw new JsonException("Legacy task durations must be integer milliseconds."), + }; + + public override void Write(Utf8JsonWriter writer, TimeSpan? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteNumberValue((long)value.Value.TotalMilliseconds); + } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/LegacyTasksJsonContext.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/LegacyTasksJsonContext.cs new file mode 100644 index 000000000..a020d0487 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/LegacyTasksJsonContext.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Provides source-generated JSON serialization metadata for legacy Tasks messages. +[JsonSourceGenerationOptions( + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(McpLegacyTask))] +[JsonSerializable(typeof(McpLegacyTaskMetadata))] +[JsonSerializable(typeof(CreateLegacyTaskResult))] +[JsonSerializable(typeof(GetLegacyTaskRequestParams))] +[JsonSerializable(typeof(GetLegacyTaskResult))] +[JsonSerializable(typeof(ListLegacyTasksRequestParams))] +[JsonSerializable(typeof(ListLegacyTasksResult))] +[JsonSerializable(typeof(GetLegacyTaskPayloadRequestParams))] +[JsonSerializable(typeof(CancelLegacyTaskRequestParams))] +[JsonSerializable(typeof(CancelLegacyTaskResult))] +[JsonSerializable(typeof(LegacyTaskStatusNotificationParams))] +public sealed partial class LegacyTasksJsonContext : JsonSerializerContext +{ +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/LegacyTasksProtocol.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/LegacyTasksProtocol.cs new file mode 100644 index 000000000..3aae60436 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/LegacyTasksProtocol.cs @@ -0,0 +1,31 @@ +namespace ModelContextProtocol.Legacy.Tasks; + +/// +/// Constants for the experimental MCP Tasks protocol revision from 2025-11-30. +/// +public static class LegacyTasksProtocol +{ + /// The protocol revision that defined this legacy Tasks draft. + public const string ProtocolVersion = "2025-11-30"; + + /// The top-level capability property used by the legacy Tasks draft. + public const string CapabilityName = "tasks"; + + /// The legacy request augmentation property. + public const string TaskPropertyName = "task"; + + /// The request method for reading a task's status. + public const string GetTaskMethod = "tasks/get"; + + /// The request method for listing tasks. + public const string ListTasksMethod = "tasks/list"; + + /// The request method for retrieving a completed task's payload. + public const string GetTaskResultMethod = "tasks/result"; + + /// The request method for cancelling a task. + public const string CancelTaskMethod = "tasks/cancel"; + + /// The optional notification sent when a task's status changes. + public const string TaskStatusNotificationMethod = "notifications/tasks/status"; +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/ModelContextProtocol.Legacy.Tasks-2025-11-30.csproj b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/ModelContextProtocol.Legacy.Tasks-2025-11-30.csproj new file mode 100644 index 000000000..c9d6ef8c9 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/ModelContextProtocol.Legacy.Tasks-2025-11-30.csproj @@ -0,0 +1,50 @@ + + + + net10.0;net9.0;net8.0;netstandard2.0 + true + true + ModelContextProtocol.Legacy.Tasks-2025-11-30 + Compatibility implementation for the MCP Tasks draft from 2025-11-30 + README.md + $(NoWarn);CS0436;MCPEXP001;MCPEXP002;MCP9007 + false + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Protocol/LegacyTaskMessages.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Protocol/LegacyTaskMessages.cs new file mode 100644 index 000000000..9d6a3f17d --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Protocol/LegacyTaskMessages.cs @@ -0,0 +1,158 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Represents the result returned when a task-augmented request is accepted. +public sealed class CreateLegacyTaskResult : Result +{ + /// Gets or sets the newly created task. + [JsonPropertyName("task")] + public required McpLegacyTask Task { get; set; } +} + +/// Represents the parameters for a legacy tasks/get request. +public sealed class GetLegacyTaskRequestParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } +} + +/// Represents the result of a legacy tasks/get request. +public sealed class GetLegacyTaskResult +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the legacy task status. + [JsonPropertyName("status")] + public required McpLegacyTaskStatus Status { get; set; } + + /// Gets or sets a human-readable status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents the parameters for a legacy tasks/list request. +public sealed class ListLegacyTasksRequestParams +{ + /// Gets or sets the optional pagination cursor. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } +} + +/// Represents the result of a legacy tasks/list request. +public sealed class ListLegacyTasksResult +{ + /// Gets or sets the tasks in this legacy task page. + [JsonPropertyName("tasks")] + public required IList Tasks { get; set; } + + /// Gets or sets the optional next-page cursor. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } +} + +/// Represents the parameters for a legacy tasks/result request. +public sealed class GetLegacyTaskPayloadRequestParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } +} + +/// Represents the parameters for a legacy tasks/cancel request. +public sealed class CancelLegacyTaskRequestParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } +} + +/// Represents the result of a legacy tasks/cancel request. +public sealed class CancelLegacyTaskResult +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the task status. + [JsonPropertyName("status")] + public required McpLegacyTaskStatus Status { get; set; } + + /// Gets or sets a human-readable status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents a legacy task status notification. +public sealed class LegacyTaskStatusNotificationParams +{ + /// Gets or sets the task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the legacy task status. + [JsonPropertyName("status")] + public required McpLegacyTaskStatus Status { get; set; } + + /// Gets or sets a human-readable status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the task retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Protocol/LegacyTaskModels.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Protocol/LegacyTaskModels.cs new file mode 100644 index 000000000..9fd6a92d4 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Protocol/LegacyTaskModels.cs @@ -0,0 +1,114 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Represents the state of a task in the 2025-11-30 Tasks draft. +public sealed class McpLegacyTask +{ + /// Gets or sets the unique task identifier. + [JsonPropertyName("taskId")] + public required string TaskId { get; set; } + + /// Gets or sets the legacy task status. + [JsonPropertyName("status")] + public required McpLegacyTaskStatus Status { get; set; } + + /// Gets or sets a human-readable status message. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } + + /// Gets or sets when the task was created. + [JsonPropertyName("createdAt")] + public required DateTimeOffset CreatedAt { get; set; } + + /// Gets or sets when the task was last updated. + [JsonPropertyName("lastUpdatedAt")] + public required DateTimeOffset LastUpdatedAt { get; set; } + + /// Gets or sets the retention period requested or assigned to the task. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } + + /// Gets or sets the suggested status polling interval. + [JsonPropertyName("pollInterval")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? PollInterval { get; set; } +} + +/// Represents the status of a task in the 2025-11-30 Tasks draft. +[JsonConverter(typeof(LegacyTaskStatusConverter))] +public enum McpLegacyTaskStatus +{ + /// The task is executing. + Working, + + /// The task needs additional input. + InputRequired, + + /// The task completed successfully. + Completed, + + /// The task completed with an error. + Failed, + + /// The task was cancelled. + Cancelled, +} + +/// Provides request metadata that asks a peer to execute a request as a task. +public sealed class McpLegacyTaskMetadata +{ + /// Gets or sets the requested retention period. + [JsonPropertyName("ttl")] + [JsonConverter(typeof(LegacyTimeSpanMillisecondsConverter))] + public TimeSpan? TimeToLive { get; set; } +} + +internal sealed class LegacyTaskStatusConverter : JsonConverter +{ + public override McpLegacyTaskStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.GetString() switch + { + "working" => McpLegacyTaskStatus.Working, + "input_required" => McpLegacyTaskStatus.InputRequired, + "completed" => McpLegacyTaskStatus.Completed, + "failed" => McpLegacyTaskStatus.Failed, + "cancelled" => McpLegacyTaskStatus.Cancelled, + var value => throw new JsonException($"Unknown legacy task status '{value}'."), + }; + + public override void Write(Utf8JsonWriter writer, McpLegacyTaskStatus value, JsonSerializerOptions options) => + writer.WriteStringValue(value switch + { + McpLegacyTaskStatus.Working => "working", + McpLegacyTaskStatus.InputRequired => "input_required", + McpLegacyTaskStatus.Completed => "completed", + McpLegacyTaskStatus.Failed => "failed", + McpLegacyTaskStatus.Cancelled => "cancelled", + _ => throw new JsonException($"Unknown legacy task status '{value}'."), + }); +} + +internal sealed class LegacyTimeSpanMillisecondsConverter : JsonConverter +{ + public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.Number when reader.TryGetInt64(out long milliseconds) => TimeSpan.FromMilliseconds(milliseconds), + _ => throw new JsonException("Legacy task durations must be integer milliseconds."), + }; + + public override void Write(Utf8JsonWriter writer, TimeSpan? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteNumberValue((long)value.Value.TotalMilliseconds); + } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/README.md b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/README.md new file mode 100644 index 000000000..f5a5b9e42 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/README.md @@ -0,0 +1,52 @@ +# ModelContextProtocol.Legacy.Tasks-2025-11-30 + +This proof-of-concept package adds server and client support for the experimental MCP Tasks draft negotiated as `2025-11-30`. It is intended only for compatibility with peers that implemented that draft. + +Use `WithLegacyTasks` to enable server-side support. The package can be referenced together with `ModelContextProtocol.Extensions.Tasks`; the negotiated protocol version selects which implementation handles a request. + +```csharp +builder + .WithLegacyTasks(new InMemoryLegacyMcpTaskStore()) + .WithTasks(new InMemoryMcpTaskStore()); + +var clientOptions = new McpClientOptions().EnableTasksMigration(); +await using var client = await McpClient.CreateAsync(transport, clientOptions); +var tasks = client.CreateTaskMigrationClient(logger); + +CallToolResult result = await tasks.CallToolWithPollingAsync( + new() { Name = "long-running-tool" }); +``` + +`EnableTasksMigration` advertises the legacy top-level `tasks` capability but does not set `McpClientOptions.ProtocolVersion`. The SDK therefore uses its standard protocol negotiation and fallback behavior. Create `McpTaskMigrationClient` only after connection: it delegates to modern Tasks for `2026-07-28` or later and to legacy Tasks for exactly `2025-11-30`. Its optional logger emits an Information-level record naming every server for which legacy Tasks is selected. + +For a single execution pipeline, call `McpTaskMigrationClient.ExecuteToolAsync` with the `McpClientTool` returned by `ListToolsAsync` and its request parameters. On a legacy connection it enters the task/polling path only when the tool metadata contains: + +```json +"execution": { + "taskSupport": "optional" +} +``` + +Tools without that property use the ordinary `tools/call` path. On a modern connection, the server-level `io.modelcontextprotocol/tasks` extension capability indicates task support. + +`EnableLegacyTasks` remains available for clients that intentionally pin themselves to `2025-11-30`. It pins the protocol version and is not the migration path. + +## Deprecated 1.x source compatibility + +The package also supplies deprecated source-compatibility models in the original +`ModelContextProtocol.Protocol` namespace and extension methods in +`ModelContextProtocol.Client`. They retain the 1.x names, including `McpTask`, +`McpTaskMetadata`, `CallToolAsTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, +`GetTaskResultAsync`, `CancelTaskAsync`, and `PollTaskUntilCompleteAsync`. + +Every compatibility type reports obsolete diagnostic `MCP9007`. The facade supports only +legacy task-augmented tool calls and their lifecycle. It does not restore binary compatibility, +the removed `McpClientOptions.TaskStore` or `McpServerOptions.TaskStore` properties, or legacy +reverse-direction sampling and elicitation tasks. Use `WithLegacyTasks` for server configuration. + +Some restored names, such as `McpTaskStatus` and `GetTaskResult`, also exist in +`ModelContextProtocol.Extensions.Tasks`. Code that imports both APIs should use a namespace alias +or the `McpTaskMigrationClient` facade rather than calling the overlapping extension methods +directly. + +The proof of concept covers task-augmented `tools/call` requests and the `tasks/get`, `tasks/list`, `tasks/result`, and `tasks/cancel` lifecycle. It does not recreate the draft's reverse-direction sampling or elicitation task augmentation. diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/ILegacyMcpTaskStore.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/ILegacyMcpTaskStore.cs new file mode 100644 index 000000000..446a2bbb1 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/ILegacyMcpTaskStore.cs @@ -0,0 +1,33 @@ +using System.Text.Json; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Defines durable storage operations required by the legacy Tasks protocol. +public interface ILegacyMcpTaskStore +{ + /// Creates a task for an augmented request. + Task CreateTaskAsync( + McpLegacyTaskMetadata metadata, + CancellationToken cancellationToken = default); + + /// Gets the state of a legacy task. + Task GetTaskAsync(string taskId, CancellationToken cancellationToken = default); + + /// Stores the terminal result of a task. + Task StoreTaskResultAsync( + string taskId, + McpLegacyTaskStatus status, + JsonElement result, + CancellationToken cancellationToken = default); + + /// Gets the result payload of a terminal task. + Task GetTaskResultAsync(string taskId, CancellationToken cancellationToken = default); + + /// Lists the tasks that are visible to the caller. + Task ListTasksAsync( + string? cursor = null, + CancellationToken cancellationToken = default); + + /// Cancels a task if it has not reached a terminal state. + Task CancelTaskAsync(string taskId, CancellationToken cancellationToken = default); +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/InMemoryLegacyMcpTaskStore.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/InMemoryLegacyMcpTaskStore.cs new file mode 100644 index 000000000..fe67bbd81 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/InMemoryLegacyMcpTaskStore.cs @@ -0,0 +1,190 @@ +using System.Collections.Concurrent; +using System.Text.Json; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Provides an in-memory task store for development, tests, and single-process compatibility scenarios. +public sealed class InMemoryLegacyMcpTaskStore : ILegacyMcpTaskStore +{ + private readonly ConcurrentDictionary _tasks = new(StringComparer.Ordinal); + + /// Gets or sets the poll interval assigned to newly created tasks. + public TimeSpan DefaultPollInterval { get; set; } = TimeSpan.FromSeconds(1); + + /// Creates a task in the working state. + public Task CreateTaskAsync(McpLegacyTaskMetadata metadata, CancellationToken cancellationToken = default) + { + if (metadata is null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + var now = DateTimeOffset.UtcNow; + var task = new McpLegacyTask + { + TaskId = Guid.NewGuid().ToString("N"), + Status = McpLegacyTaskStatus.Working, + CreatedAt = now, + LastUpdatedAt = now, + TimeToLive = metadata.TimeToLive, + PollInterval = DefaultPollInterval, + }; + + _tasks[task.TaskId] = new Entry(task); + return Task.FromResult(Clone(task)); + } + + /// Gets a task by identifier. + public Task GetTaskAsync(string taskId, CancellationToken cancellationToken = default) + { + if (taskId is null) + { + throw new ArgumentNullException(nameof(taskId)); + } + + if (!_tasks.TryGetValue(taskId, out var entry)) + { + return Task.FromResult(null); + } + + lock (entry.Gate) + { + return Task.FromResult(Clone(entry.Task)); + } + } + + /// Stores a completed or failed result. + public Task StoreTaskResultAsync( + string taskId, + McpLegacyTaskStatus status, + JsonElement result, + CancellationToken cancellationToken = default) + { + if (status is not (McpLegacyTaskStatus.Completed or McpLegacyTaskStatus.Failed)) + { + throw new ArgumentOutOfRangeException(nameof(status), "Only completed or failed task results can be stored."); + } + + var entry = GetRequiredEntry(taskId); + lock (entry.Gate) + { + if (!IsTerminal(entry.Task.Status)) + { + entry.Task.Status = status; + entry.Task.LastUpdatedAt = DateTimeOffset.UtcNow; + entry.Result = result.Clone(); + entry.Completion.TrySetResult(null); + } + + return Task.FromResult(Clone(entry.Task)); + } + } + + /// Waits for and returns a task's terminal result. + public async Task GetTaskResultAsync(string taskId, CancellationToken cancellationToken = default) + { + var entry = GetRequiredEntry(taskId); + await WaitWithCancellationAsync(entry.Completion.Task, cancellationToken).ConfigureAwait(false); + + lock (entry.Gate) + { + return entry.Result?.Clone() + ?? throw new InvalidOperationException($"Task '{taskId}' completed without a result payload."); + } + } + + /// Lists all in-memory tasks. + public Task ListTasksAsync(string? cursor = null, CancellationToken cancellationToken = default) + { + if (cursor is not null) + { + throw new ArgumentException("InMemoryLegacyMcpTaskStore does not support pagination cursors.", nameof(cursor)); + } + + var tasks = _tasks.Values + .Select(entry => + { + lock (entry.Gate) + { + return Clone(entry.Task); + } + }) + .OrderBy(task => task.CreatedAt) + .ToList(); + + return Task.FromResult(new ListLegacyTasksResult { Tasks = tasks }); + } + + /// Cancels a non-terminal task. + public Task CancelTaskAsync(string taskId, CancellationToken cancellationToken = default) + { + var entry = GetRequiredEntry(taskId); + lock (entry.Gate) + { + if (!IsTerminal(entry.Task.Status)) + { + entry.Task.Status = McpLegacyTaskStatus.Cancelled; + entry.Task.LastUpdatedAt = DateTimeOffset.UtcNow; + entry.Completion.TrySetResult(null); + } + + return Task.FromResult(Clone(entry.Task)); + } + } + + private Entry GetRequiredEntry(string taskId) + { + if (taskId is null) + { + throw new ArgumentNullException(nameof(taskId)); + } + + return _tasks.TryGetValue(taskId, out var entry) + ? entry + : throw new InvalidOperationException($"Task '{taskId}' was not found."); + } + + private static bool IsTerminal(McpLegacyTaskStatus status) => + status is McpLegacyTaskStatus.Completed or McpLegacyTaskStatus.Failed or McpLegacyTaskStatus.Cancelled; + + private static McpLegacyTask Clone(McpLegacyTask task) => + new() + { + TaskId = task.TaskId, + Status = task.Status, + StatusMessage = task.StatusMessage, + CreatedAt = task.CreatedAt, + LastUpdatedAt = task.LastUpdatedAt, + TimeToLive = task.TimeToLive, + PollInterval = task.PollInterval, + }; + + private static async Task WaitWithCancellationAsync(Task task, CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled || task.IsCompleted) + { + await task.ConfigureAwait(false); + return; + } + + var cancellation = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = cancellationToken.Register( + static state => ((TaskCompletionSource)state!).TrySetResult(null), + cancellation); + + if (await Task.WhenAny(task, cancellation.Task).ConfigureAwait(false) != task) + { + throw new OperationCanceledException(cancellationToken); + } + + await task.ConfigureAwait(false); + } + + private sealed class Entry(McpLegacyTask task) + { + public object Gate { get; } = new(); + public McpLegacyTask Task { get; } = task; + public JsonElement? Result { get; set; } + public TaskCompletionSource Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/LegacyTasksBuilderExtensions.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/LegacyTasksBuilderExtensions.cs new file mode 100644 index 000000000..9077ac902 --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/LegacyTasksBuilderExtensions.cs @@ -0,0 +1,278 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Provides server-builder extensions for the 2025-11-30 Tasks draft. +public static class LegacyTasksBuilderExtensions +{ + /// + /// Enables the legacy Tasks protocol for task-augmented tools/call requests. + /// + /// + /// The registered server can also use the 2026 Tasks extension. Each implementation only + /// handles requests for its own negotiated protocol revision. + /// + public static IMcpServerBuilder WithLegacyTasks(this IMcpServerBuilder builder, ILegacyMcpTaskStore store) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (store is null) + { + throw new ArgumentNullException(nameof(store)); + } + + builder.Services.AddSingleton>( + new LegacyTasksPostConfigureOptions(store)); + return builder; + } + + private sealed class LegacyTasksPostConfigureOptions(ILegacyMcpTaskStore store) : IPostConfigureOptions + { + private readonly ILegacyMcpTaskStore _store = store; + private readonly ConcurrentDictionary _cancellationSources = new(StringComparer.Ordinal); + + public void PostConfigure(string? name, McpServerOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Capabilities ??= new ServerCapabilities(); + options.Capabilities.AdditionalProperties ??= new Dictionary(); + options.Capabilities.AdditionalProperties[LegacyTasksProtocol.CapabilityName] = CreateCapabilityElement(); + + if (options.ToolCollection is not null) + { + foreach (var tool in options.ToolCollection) + { + tool.ProtocolTool.AdditionalProperties ??= new Dictionary(); + tool.ProtocolTool.AdditionalProperties["execution"] = CreateToolExecutionElement(); + } + } + + options.RequestHandlers ??= new List(); + options.RequestHandlers.Add(new McpServerRequestHandler + { + Method = LegacyTasksProtocol.GetTaskMethod, + IsApplicable = IsLegacyTasksProtocolRequest, + Handler = HandleGetTaskAsync, + }); + options.RequestHandlers.Add(new McpServerRequestHandler + { + Method = LegacyTasksProtocol.ListTasksMethod, + IsApplicable = IsLegacyTasksProtocolRequest, + Handler = HandleListTasksAsync, + }); + options.RequestHandlers.Add(new McpServerRequestHandler + { + Method = LegacyTasksProtocol.GetTaskResultMethod, + IsApplicable = IsLegacyTasksProtocolRequest, + Handler = HandleGetTaskResultAsync, + }); + options.RequestHandlers.Add(new McpServerRequestHandler + { + Method = LegacyTasksProtocol.CancelTaskMethod, + IsApplicable = IsLegacyTasksProtocolRequest, + Handler = HandleCancelTaskAsync, + }); + + options.Filters.Request.CallToolWithAlternateFilters.Add(next => async (request, cancellationToken) => + { + if (!IsLegacyTasksRequest(request)) + { + return await next(request, cancellationToken).ConfigureAwait(false); + } + + var metadata = request.JsonRpcRequest.Params?[LegacyTasksProtocol.TaskPropertyName]? + .Deserialize(LegacyTasksJsonContext.Default.McpLegacyTaskMetadata) + ?? new McpLegacyTaskMetadata(); + var task = await _store.CreateTaskAsync(metadata, cancellationToken).ConfigureAwait(false); + var taskCancellation = new CancellationTokenSource(); + _cancellationSources[task.TaskId] = taskCancellation; + + _ = Task.Run( + () => ExecuteTaskAsync(task.TaskId, request, next, taskCancellation), + CancellationToken.None); + + return ResultOrAlternate.FromAlternate( + new CreateLegacyTaskResult { Task = task }, + LegacyTasksJsonContext.Default.CreateLegacyTaskResult); + }); + } + + private async Task ExecuteTaskAsync( + string taskId, + RequestContext request, + McpRequestHandler> next, + CancellationTokenSource taskCancellation) + { + try + { + var execution = await next(request, taskCancellation.Token).ConfigureAwait(false); + if (execution.IsAlternate is true) + { + throw new InvalidOperationException( + $"The legacy Tasks compatibility package cannot compose with another alternate result for task '{taskId}'."); + } + + var result = execution.Result!; + await _store.StoreTaskResultAsync( + taskId, + result.IsError is true ? McpLegacyTaskStatus.Failed : McpLegacyTaskStatus.Completed, + JsonSerializer.SerializeToElement(result, McpJsonUtilities.DefaultOptions.GetTypeInfo()), + CancellationToken.None).ConfigureAwait(false); + } + catch (OperationCanceledException) when (taskCancellation.IsCancellationRequested) + { + await _store.CancelTaskAsync(taskId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception) + { + var error = new CallToolResult + { + IsError = true, + Content = [new TextContentBlock { Text = "An error occurred while executing the legacy task." }], + }; + + await _store.StoreTaskResultAsync( + taskId, + McpLegacyTaskStatus.Failed, + JsonSerializer.SerializeToElement(error, McpJsonUtilities.DefaultOptions.GetTypeInfo()), + CancellationToken.None).ConfigureAwait(false); + } + finally + { + if (_cancellationSources.TryRemove(taskId, out var registered)) + { + registered.Dispose(); + } + } + } + + private async ValueTask HandleGetTaskAsync(JsonRpcRequest request, CancellationToken cancellationToken) + { + GateToLegacyProtocol(request, LegacyTasksProtocol.GetTaskMethod); + var parameters = request.Params?.Deserialize(LegacyTasksJsonContext.Default.GetLegacyTaskRequestParams) + ?? throw new McpProtocolException("Missing params for tasks/get.", McpErrorCode.InvalidParams); + var task = await _store.GetTaskAsync(parameters.TaskId, cancellationToken).ConfigureAwait(false) + ?? throw new McpProtocolException($"Unknown task '{parameters.TaskId}'.", McpErrorCode.InvalidParams); + + return JsonSerializer.SerializeToNode(ToGetTaskResult(task), LegacyTasksJsonContext.Default.GetLegacyTaskResult); + } + + private async ValueTask HandleListTasksAsync(JsonRpcRequest request, CancellationToken cancellationToken) + { + GateToLegacyProtocol(request, LegacyTasksProtocol.ListTasksMethod); + var parameters = request.Params?.Deserialize(LegacyTasksJsonContext.Default.ListLegacyTasksRequestParams) + ?? new ListLegacyTasksRequestParams(); + var result = await _store.ListTasksAsync(parameters.Cursor, cancellationToken).ConfigureAwait(false); + + return JsonSerializer.SerializeToNode(result, LegacyTasksJsonContext.Default.ListLegacyTasksResult); + } + + private async ValueTask HandleGetTaskResultAsync(JsonRpcRequest request, CancellationToken cancellationToken) + { + GateToLegacyProtocol(request, LegacyTasksProtocol.GetTaskResultMethod); + var parameters = request.Params?.Deserialize(LegacyTasksJsonContext.Default.GetLegacyTaskPayloadRequestParams) + ?? throw new McpProtocolException("Missing params for tasks/result.", McpErrorCode.InvalidParams); + + try + { + var result = await _store.GetTaskResultAsync(parameters.TaskId, cancellationToken).ConfigureAwait(false); + return JsonNode.Parse(result.GetRawText()); + } + catch (InvalidOperationException exception) + { + throw new McpProtocolException(exception.Message, McpErrorCode.InvalidParams); + } + } + + private async ValueTask HandleCancelTaskAsync(JsonRpcRequest request, CancellationToken cancellationToken) + { + GateToLegacyProtocol(request, LegacyTasksProtocol.CancelTaskMethod); + var parameters = request.Params?.Deserialize(LegacyTasksJsonContext.Default.CancelLegacyTaskRequestParams) + ?? throw new McpProtocolException("Missing params for tasks/cancel.", McpErrorCode.InvalidParams); + var task = await _store.CancelTaskAsync(parameters.TaskId, cancellationToken).ConfigureAwait(false); + + if (_cancellationSources.TryRemove(task.TaskId, out var taskCancellation)) + { + taskCancellation.Cancel(); + taskCancellation.Dispose(); + } + + return JsonSerializer.SerializeToNode(ToCancelTaskResult(task), LegacyTasksJsonContext.Default.CancelLegacyTaskResult); + } + + private static bool IsLegacyTasksRequest(RequestContext request) => + string.Equals(request.Server.NegotiatedProtocolVersion, LegacyTasksProtocol.ProtocolVersion, StringComparison.Ordinal) && + request.JsonRpcRequest.Params?[LegacyTasksProtocol.TaskPropertyName] is not null; + + private static bool IsLegacyTasksProtocolRequest(JsonRpcRequest request) => + string.Equals(request.Context?.ProtocolVersion, LegacyTasksProtocol.ProtocolVersion, StringComparison.Ordinal); + + private static void GateToLegacyProtocol(JsonRpcRequest request, string method) + { + if (!IsLegacyTasksProtocolRequest(request)) + { + throw new McpProtocolException( + $"The method '{method}' requires protocol version '{LegacyTasksProtocol.ProtocolVersion}'.", + McpErrorCode.MethodNotFound); + } + } + + private static JsonElement CreateCapabilityElement() => + JsonSerializer.SerializeToElement( + new JsonObject + { + ["list"] = new JsonObject(), + ["cancel"] = new JsonObject(), + ["requests"] = new JsonObject + { + ["tools"] = new JsonObject + { + ["call"] = new JsonObject(), + }, + }, + }, + McpJsonUtilities.DefaultOptions.GetTypeInfo()); + + private static JsonElement CreateToolExecutionElement() => + JsonSerializer.SerializeToElement( + new JsonObject { ["taskSupport"] = "optional" }, + McpJsonUtilities.DefaultOptions.GetTypeInfo()); + + private static GetLegacyTaskResult ToGetTaskResult(McpLegacyTask task) => + new() + { + TaskId = task.TaskId, + Status = task.Status, + StatusMessage = task.StatusMessage, + CreatedAt = task.CreatedAt, + LastUpdatedAt = task.LastUpdatedAt, + TimeToLive = task.TimeToLive, + PollInterval = task.PollInterval, + }; + + private static CancelLegacyTaskResult ToCancelTaskResult(McpLegacyTask task) => + new() + { + TaskId = task.TaskId, + Status = task.Status, + StatusMessage = task.StatusMessage, + CreatedAt = task.CreatedAt, + LastUpdatedAt = task.LastUpdatedAt, + TimeToLive = task.TimeToLive, + PollInterval = task.PollInterval, + }; + } +} diff --git a/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/LegacyTasksServerExtensions.cs b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/LegacyTasksServerExtensions.cs new file mode 100644 index 000000000..7f60a3f9e --- /dev/null +++ b/src/ModelContextProtocol.Legacy.Tasks-2025-11-30/Server/LegacyTasksServerExtensions.cs @@ -0,0 +1,31 @@ +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol.Legacy.Tasks; + +/// Provides server APIs for the 2025-11-30 Tasks draft. +public static class LegacyTasksServerExtensions +{ + /// Sends an optional legacy task-status notification. + public static Task SendLegacyTaskStatusNotificationAsync( + this McpServer server, + LegacyTaskStatusNotificationParams notificationParams, + CancellationToken cancellationToken = default) + { + if (server is null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (notificationParams is null) + { + throw new ArgumentNullException(nameof(notificationParams)); + } + + return server.SendNotificationAsync( + LegacyTasksProtocol.TaskStatusNotificationMethod, + notificationParams, + LegacyTasksJsonContext.Default.Options, + cancellationToken); + } +} diff --git a/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/GlobalUsings.cs b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/GlobalUsings.cs new file mode 100644 index 000000000..c802f4480 --- /dev/null +++ b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksCompatibilityTests.cs b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksCompatibilityTests.cs new file mode 100644 index 000000000..264f0f443 --- /dev/null +++ b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksCompatibilityTests.cs @@ -0,0 +1,149 @@ +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Extensions.Tasks; +using ModelContextProtocol.Legacy.Tasks; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using ModelContextProtocol.Tests; +using System.ComponentModel; +using System.Text.Json; + +namespace ModelContextProtocol.Legacy.Tasks.Tests; + +public class LegacyTasksCompatibilityTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper) +{ + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) + { + mcpServerBuilder + .WithLegacyTasks(new InMemoryLegacyMcpTaskStore()) + .WithTasks(new InMemoryMcpTaskStore()) + .WithTools(); + } + + [Fact] + public async Task LegacyProtocol_ExposesTaskLifecycleAlongsideModernSdk() + { + var clientOptions = new McpClientOptions().EnableLegacyTasks(); + await using var client = await CreateMcpClientForServer(clientOptions); + var cancellationToken = TestContext.Current.CancellationToken; + + Assert.Equal(LegacyTasksProtocol.ProtocolVersion, client.NegotiatedProtocolVersion); + Assert.True(client.ServerCapabilities.AdditionalProperties?.ContainsKey(LegacyTasksProtocol.CapabilityName)); + + var tools = await client.ListToolsAsync(cancellationToken: cancellationToken); + var tool = Assert.Single(tools, tool => tool.Name == "legacy-echo"); + Assert.Equal( + "optional", + tool.ProtocolTool.AdditionalProperties!["execution"].GetProperty("taskSupport").GetString()); + + var started = await client.CallToolAsLegacyTaskAsync( + new CallToolRequestParams { Name = "legacy-echo" }, + cancellationToken: cancellationToken); + + Assert.True(started.IsTask); + Assert.NotNull(started.Task); + + var listed = await client.ListLegacyTasksAsync(cancellationToken: cancellationToken); + Assert.Contains(listed.Tasks, task => task.TaskId == started.Task.TaskId); + + var payload = await client.GetLegacyTaskPayloadAsync(started.Task.TaskId, cancellationToken); + var result = JsonSerializer.Deserialize(payload, McpJsonUtilities.DefaultOptions.GetTypeInfo()); + Assert.NotNull(result); + Assert.Equal("legacy result", Assert.IsType(result.Content[0]).Text); + + var completed = await client.GetLegacyTaskAsync(started.Task.TaskId, cancellationToken); + Assert.Equal(McpLegacyTaskStatus.Completed, completed.Status); + } + + [Fact] + public async Task LegacyProtocol_CancelTaskRoutesToLegacyTaskStore() + { + var clientOptions = new McpClientOptions().EnableLegacyTasks(); + await using var client = await CreateMcpClientForServer(clientOptions); + var cancellationToken = TestContext.Current.CancellationToken; + + var started = await client.CallToolAsLegacyTaskAsync( + new CallToolRequestParams { Name = "legacy-wait" }, + cancellationToken: cancellationToken); + + var cancelled = await client.CancelLegacyTaskAsync(started.Task!.TaskId, cancellationToken); + Assert.Equal(McpLegacyTaskStatus.Cancelled, cancelled.Status); + + var task = await client.GetLegacyTaskAsync(started.Task.TaskId, cancellationToken); + Assert.Equal(McpLegacyTaskStatus.Cancelled, task.Status); + } + + [Fact] + public void EnableLegacyTasks_RejectsAnotherPinnedProtocolVersion() + { + var options = new McpClientOptions { ProtocolVersion = "2025-11-25" }; + + var exception = Assert.Throws(() => options.EnableLegacyTasks()); + + Assert.Contains(LegacyTasksProtocol.ProtocolVersion, exception.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task ModernProtocol_UsesModernTasksWhenLegacyTasksAreAlsoRegistered() + { + await using var client = await CreateMcpClientForServer(); + var cancellationToken = TestContext.Current.CancellationToken; + + Assert.True(client.ServerCapabilities.Extensions?.ContainsKey(TasksProtocol.ExtensionId) is true); + + var started = await client.CallToolAsTaskAsync( + new CallToolRequestParams { Name = "legacy-echo" }, + cancellationToken); + + Assert.True(started.IsTask); + Assert.NotNull(started.TaskCreated); + + var task = await client.GetTaskAsync(started.TaskCreated.TaskId, cancellationToken); + Assert.NotNull(task); + } + + [Fact] + public async Task TaskMigration_UsesModernTasksWithoutInfluencingProtocolNegotiation() + { + var clientOptions = new McpClientOptions().EnableTasksMigration(); + Assert.Null(clientOptions.ProtocolVersion); + Assert.True(clientOptions.Capabilities!.AdditionalProperties!.ContainsKey(LegacyTasksProtocol.CapabilityName)); + + await using var client = await CreateMcpClientForServer(clientOptions); + var migrationClient = client.CreateTaskMigrationClient(); + var cancellationToken = TestContext.Current.CancellationToken; + + Assert.Equal(McpTaskMigrationMode.Modern, migrationClient.Mode); + var taskTool = Assert.Single( + await client.ListToolsAsync(cancellationToken: cancellationToken), + tool => tool.Name == "legacy-echo"); + Assert.True(migrationClient.SupportsTaskExecution(taskTool)); + + var started = await migrationClient.CallToolAsTaskAsync( + new CallToolRequestParams { Name = "legacy-echo" }, + cancellationToken); + + Assert.True(started.IsTask); + Assert.NotNull(started.ModernTask); + Assert.Null(started.LegacyTask); + + var result = await migrationClient.CallToolWithPollingAsync( + new CallToolRequestParams { Name = "legacy-echo" }, + cancellationToken: cancellationToken); + Assert.Equal("legacy result", Assert.IsType(result.Content[0]).Text); + } + + [McpServerToolType] + private sealed class LegacyTaskTools + { + [McpServerTool(Name = "legacy-echo"), Description("Returns a value through a legacy task.")] + public static string LegacyEcho() => "legacy result"; + + [McpServerTool(Name = "legacy-wait"), Description("Waits until a legacy task is cancelled.")] + public static async Task LegacyWait(CancellationToken cancellationToken) + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + } +} diff --git a/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksMigrationFallbackTests.cs b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksMigrationFallbackTests.cs new file mode 100644 index 000000000..0238bf57f --- /dev/null +++ b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksMigrationFallbackTests.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Legacy.Tasks; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using ModelContextProtocol.Tests; +using ModelContextProtocol.Tests.Utils; +using System.ComponentModel; +using System.Text.Json; + +namespace ModelContextProtocol.Legacy.Tasks.Tests; + +public class LegacyTasksMigrationFallbackTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper) +{ + private readonly InMemoryLegacyMcpTaskStore _taskStore = new(); + + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) + { + services.Configure(options => + { + options.ProtocolVersion = LegacyTasksProtocol.ProtocolVersion; + options.ServerInfo = new Implementation { Name = "legacy-tasks-test-server", Version = "1.0" }; + }); + + mcpServerBuilder + .WithLegacyTasks(_taskStore) + .WithTools(); + } + + [Fact] + public async Task TaskMigration_FallsBackToLegacyTasksAndLogsTheServer() + { + var clientOptions = new McpClientOptions().EnableTasksMigration(); + Assert.Null(clientOptions.ProtocolVersion); + + await using var client = await CreateMcpClientForServer(clientOptions); + var migrationClient = client.CreateTaskMigrationClient(MockLoggerProvider.CreateLogger("TasksMigration")); + var cancellationToken = TestContext.Current.CancellationToken; + + Assert.Equal(LegacyTasksProtocol.ProtocolVersion, client.NegotiatedProtocolVersion); + Assert.Equal(McpTaskMigrationMode.Legacy, migrationClient.Mode); + + var started = await migrationClient.CallToolAsTaskAsync( + new CallToolRequestParams { Name = "legacy-echo" }, + cancellationToken); + + Assert.True(started.IsTask); + Assert.NotNull(started.LegacyTask); + Assert.Null(started.ModernTask); + + var result = await migrationClient.CallToolWithPollingAsync( + new CallToolRequestParams { Name = "legacy-echo" }, + cancellationToken: cancellationToken); + Assert.Equal("legacy result", Assert.IsType(result.Content[0]).Text); + + Assert.Contains( + MockLoggerProvider.LogMessages, + message => message.LogLevel == Microsoft.Extensions.Logging.LogLevel.Information && + message.Message.Contains("legacy-tasks-test-server", StringComparison.Ordinal) && + message.Message.Contains(LegacyTasksProtocol.ProtocolVersion, StringComparison.Ordinal)); + } + + [Fact] + public async Task TaskMigration_LegacyToolMetadataSelectsTheTaskPipeline() + { + await using var client = await CreateMcpClientForServer(new McpClientOptions().EnableTasksMigration()); + var migrationClient = client.CreateTaskMigrationClient(); + var cancellationToken = TestContext.Current.CancellationToken; + var taskTool = Assert.Single(await client.ListToolsAsync(cancellationToken: cancellationToken)); + + Assert.Equal( + "optional", + taskTool.ProtocolTool.AdditionalProperties!["execution"].GetProperty("taskSupport").GetString()); + Assert.True(migrationClient.SupportsTaskExecution(taskTool)); + + var result = await migrationClient.ExecuteToolAsync( + taskTool, + new CallToolRequestParams { Name = taskTool.ProtocolTool.Name }, + cancellationToken: cancellationToken); + Assert.Equal("legacy result", Assert.IsType(result.Content[0]).Text); + Assert.Single((await client.ListLegacyTasksAsync(cancellationToken: cancellationToken)).Tasks); + + var directTool = new McpClientTool(client, new Tool { Name = taskTool.ProtocolTool.Name }); + Assert.False(migrationClient.SupportsTaskExecution(directTool)); + + result = await migrationClient.ExecuteToolAsync( + directTool, + new CallToolRequestParams { Name = directTool.ProtocolTool.Name }, + cancellationToken: cancellationToken); + Assert.Equal("legacy result", Assert.IsType(result.Content[0]).Text); + Assert.Single((await client.ListLegacyTasksAsync(cancellationToken: cancellationToken)).Tasks); + } + + [Fact] + public async Task LegacyToolDescriptor_ExecutionMetadataIsPreservedWithoutForcingTaskExecution() + { + await using var client = await CreateMcpClientForServer(); + var cancellationToken = TestContext.Current.CancellationToken; + var tool = Assert.Single(await client.ListToolsAsync(cancellationToken: cancellationToken)); + + JsonElement execution = tool.ProtocolTool.AdditionalProperties!["execution"]; + Assert.Equal("""{"taskSupport":"optional"}""", execution.GetRawText()); + Assert.Equal("optional", execution.GetProperty("taskSupport").GetString()); + + var result = await tool.CallAsync(cancellationToken: cancellationToken); + + Assert.Equal("legacy result", Assert.IsType(result.Content[0]).Text); + Assert.Empty((await _taskStore.ListTasksAsync(cancellationToken: cancellationToken)).Tasks); + } + + [McpServerToolType] + private sealed class LegacyTaskTools + { + [McpServerTool(Name = "legacy-echo"), Description("Returns a value through a legacy task.")] + public static string LegacyEcho() => "legacy result"; + } +} diff --git a/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksSourceCompatibilityTests.cs b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksSourceCompatibilityTests.cs new file mode 100644 index 000000000..e4cae6677 --- /dev/null +++ b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/LegacyTasksSourceCompatibilityTests.cs @@ -0,0 +1,95 @@ +#pragma warning disable MCP9007 + +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Legacy.Tasks; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using ModelContextProtocol.Tests; +using System.ComponentModel; +using System.Text.Json; + +namespace ModelContextProtocol.Legacy.Tasks.Tests; + +public class LegacyTasksSourceCompatibilityTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper) +{ + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) + { + services.Configure(options => options.ProtocolVersion = LegacyTasksProtocol.ProtocolVersion); + mcpServerBuilder + .WithLegacyTasks(new InMemoryLegacyMcpTaskStore()) + .WithTools(); + } + + [Fact] + public async Task LegacyTaskClientApi_SourceCompatibleMethodsUseLegacyTaskLifecycle() + { + await using var client = await CreateMcpClientForServer(new McpClientOptions().EnableLegacyTasks()); + var cancellationToken = TestContext.Current.CancellationToken; + + McpTask created = await client.CallToolAsTaskAsync( + "legacy-echo", + taskMetadata: new McpTaskMetadata { TimeToLive = TimeSpan.FromMinutes(5) }, + cancellationToken: cancellationToken); + Assert.Equal(McpTaskStatus.Working, created.Status); + + McpTask fetched = await client.GetTaskAsync(created.TaskId, cancellationToken: cancellationToken); + Assert.Equal(created.TaskId, fetched.TaskId); + + IList allTasks = await client.ListTasksAsync(cancellationToken: cancellationToken); + Assert.Contains(allTasks, task => task.TaskId == created.TaskId); + + ListTasksResult page = await client.ListTasksAsync( + new ListTasksRequestParams(), + cancellationToken); + Assert.Contains(page.Tasks, task => task.TaskId == created.TaskId); + + McpTask terminal = await client.PollTaskUntilCompleteAsync(created.TaskId, cancellationToken: cancellationToken); + Assert.Equal(McpTaskStatus.Completed, terminal.Status); + + JsonElement payload = await client.GetTaskResultAsync(created.TaskId, cancellationToken: cancellationToken); + var result = JsonSerializer.Deserialize(payload, McpJsonUtilities.DefaultOptions.GetTypeInfo()); + Assert.Equal("legacy result", Assert.IsType(result!.Content[0]).Text); + } + + [Fact] + public async Task LegacyTaskClientApi_CancelTaskUsesOriginalMethodName() + { + await using var client = await CreateMcpClientForServer(new McpClientOptions().EnableLegacyTasks()); + var cancellationToken = TestContext.Current.CancellationToken; + + McpTask created = await client.CallToolAsTaskAsync("legacy-wait", cancellationToken: cancellationToken); + McpTask cancelled = await client.CancelTaskAsync(created.TaskId, cancellationToken: cancellationToken); + + Assert.Equal(McpTaskStatus.Cancelled, cancelled.Status); + } + + [Fact] + public void LegacyTaskCompatibilityTypes_ReportCustomObsolescenceDiagnostic() + { + Assert.Equal("MCP9007", GetObsolescenceDiagnosticId(typeof(McpTask))); + Assert.Equal("MCP9007", GetObsolescenceDiagnosticId(typeof(LegacyMcpClientTaskExtensions))); + } + + private static string? GetObsolescenceDiagnosticId(Type type) => + type.CustomAttributes + .Single(attribute => attribute.AttributeType.FullName == "System.ObsoleteAttribute") + .NamedArguments + .Single(argument => argument.MemberName == "DiagnosticId") + .TypedValue + .Value as string; + + [McpServerToolType] + private sealed class LegacyTaskTools + { + [McpServerTool(Name = "legacy-echo"), Description("Returns a legacy task result.")] + public static string LegacyEcho() => "legacy result"; + + [McpServerTool(Name = "legacy-wait"), Description("Waits until its legacy task is cancelled.")] + public static async Task LegacyWait(CancellationToken cancellationToken) + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + } +} diff --git a/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests.csproj b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests.csproj new file mode 100644 index 000000000..373ce75d9 --- /dev/null +++ b/tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests/ModelContextProtocol.Legacy.Tasks-2025-11-30.Tests.csproj @@ -0,0 +1,50 @@ + + + + Exe + $(DefaultTestTargetFrameworks) + enable + enable + false + true + ModelContextProtocol.Legacy.Tasks.Tests + $(NoWarn);MCP9007 + + true + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs index af1334fab..edc700921 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs @@ -5,6 +5,8 @@ using ModelContextProtocol.Server; using System.Runtime.InteropServices; using System.Text.Json; +using GetTaskResult = ModelContextProtocol.Extensions.Tasks.GetTaskResult; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; #pragma warning disable MCPEXP001 diff --git a/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs index da4afb62f..c7eb7c063 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/TaskSerializationTests.cs @@ -1,7 +1,10 @@ using ModelContextProtocol.Extensions.Tasks; using ModelContextProtocol.Protocol; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; using System.Text.Json; using System.Text.Json.Nodes; +using CreateTaskResult = ModelContextProtocol.Extensions.Tasks.CreateTaskResult; +using GetTaskResult = ModelContextProtocol.Extensions.Tasks.GetTaskResult; namespace ModelContextProtocol.Tests.Protocol; diff --git a/tests/ModelContextProtocol.Tests/Server/CustomRequestHandlerCollisionTests.cs b/tests/ModelContextProtocol.Tests/Server/CustomRequestHandlerCollisionTests.cs index 424537461..fa16c84f3 100644 --- a/tests/ModelContextProtocol.Tests/Server/CustomRequestHandlerCollisionTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/CustomRequestHandlerCollisionTests.cs @@ -13,9 +13,10 @@ namespace ModelContextProtocol.Tests.Server; public class CustomRequestHandlerCollisionTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) { #pragma warning disable MCPEXP002 - private static McpServerRequestHandler CreateHandler(string method) => new() + private static McpServerRequestHandler CreateHandler(string method, Func? isApplicable = null) => new() { Method = method, + IsApplicable = isApplicable, Handler = (request, cancellationToken) => new ValueTask((JsonNode?)null), }; @@ -75,5 +76,22 @@ public async Task CustomHandler_UniqueMethod_Succeeds() await using var server = McpServer.Create(transport, options, LoggerFactory); Assert.NotNull(server); } + + [Fact] + public async Task CustomHandler_DuplicateCustomMethodWithPredicates_Succeeds() + { + await using var transport = new StreamServerTransport(Stream.Null, Stream.Null); + var options = new McpServerOptions + { + RequestHandlers = + [ + CreateHandler("custom/method", _ => true), + CreateHandler("custom/method", _ => false), + ], + }; + + await using var server = McpServer.Create(transport, options, LoggerFactory); + Assert.NotNull(server); + } #pragma warning restore MCPEXP002 } diff --git a/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs b/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs index 236740f61..77ebc914a 100644 --- a/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/InMemoryMcpTaskStoreTests.cs @@ -2,6 +2,7 @@ using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.Text.Json; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; #pragma warning disable MCPEXP001 diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTaskTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTaskTests.cs index 669499c12..50c473181 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTaskTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTaskTests.cs @@ -1,5 +1,9 @@ using ModelContextProtocol.Extensions.Tasks; using ModelContextProtocol.Protocol; +using GetTaskResult = ModelContextProtocol.Extensions.Tasks.GetTaskResult; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; +using CreateTaskResult = ModelContextProtocol.Extensions.Tasks.CreateTaskResult; +using GetTaskRequestParams = ModelContextProtocol.Extensions.Tasks.GetTaskRequestParams; using ModelContextProtocol.Server; using Microsoft.Extensions.DependencyInjection; using System.Runtime.InteropServices; diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index c0f85a94a..01a8bc788 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -428,6 +428,12 @@ public async Task Initialize_CopiesAllCapabilityProperties() Tools = new ToolsCapability(), Completions = new CompletionsCapability(), Extensions = new Dictionary { ["io.test"] = new JsonObject() }, + AdditionalProperties = new Dictionary + { + ["legacyTest"] = JsonSerializer.SerializeToElement( + new JsonObject(), + McpJsonUtilities.DefaultOptions.GetTypeInfo()), + }, }; await Can_Handle_Requests( diff --git a/tests/ModelContextProtocol.Tests/Server/McpTaskStoreTests.cs b/tests/ModelContextProtocol.Tests/Server/McpTaskStoreTests.cs index a904c87ed..aea6259bd 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpTaskStoreTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpTaskStoreTests.cs @@ -7,6 +7,8 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Threading.Channels; +using GetTaskResult = ModelContextProtocol.Extensions.Tasks.GetTaskResult; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; #pragma warning disable MCPEXP001 diff --git a/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs index e751bdcdc..91f5cd0f7 100644 --- a/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/TaskCancellationIntegrationTests.cs @@ -6,6 +6,7 @@ using ModelContextProtocol.Tests.Utils; using System.Runtime.InteropServices; using System.Text.Json; +using GetTaskResult = ModelContextProtocol.Extensions.Tasks.GetTaskResult; #pragma warning disable MCPEXP001 diff --git a/tests/ModelContextProtocol.Tests/Server/TaskHandlerConfigurationValidationTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskHandlerConfigurationValidationTests.cs index 621acfddb..06920a6b9 100644 --- a/tests/ModelContextProtocol.Tests/Server/TaskHandlerConfigurationValidationTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/TaskHandlerConfigurationValidationTests.cs @@ -5,6 +5,8 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using CreateTaskResult = ModelContextProtocol.Extensions.Tasks.CreateTaskResult; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; namespace ModelContextProtocol.Tests.Server; diff --git a/tests/ModelContextProtocol.Tests/Server/TaskPollStuckDetectorTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskPollStuckDetectorTests.cs index c2552abd3..ae7d5300b 100644 --- a/tests/ModelContextProtocol.Tests/Server/TaskPollStuckDetectorTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/TaskPollStuckDetectorTests.cs @@ -6,6 +6,9 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; +using CreateTaskResult = ModelContextProtocol.Extensions.Tasks.CreateTaskResult; +using GetTaskRequestParams = ModelContextProtocol.Extensions.Tasks.GetTaskRequestParams; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; #pragma warning disable MCPEXP001, MCPEXP002 diff --git a/tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs index 6fa9fa215..fc1bcd73d 100644 --- a/tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/TaskProtocolGatingTests.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; +using GetTaskRequestParams = ModelContextProtocol.Extensions.Tasks.GetTaskRequestParams; namespace ModelContextProtocol.Tests.Server; diff --git a/tests/ModelContextProtocol.Tests/Server/TaskStoreOrphanedTaskTests.cs b/tests/ModelContextProtocol.Tests/Server/TaskStoreOrphanedTaskTests.cs index 144b08476..47b4cbc62 100644 --- a/tests/ModelContextProtocol.Tests/Server/TaskStoreOrphanedTaskTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/TaskStoreOrphanedTaskTests.cs @@ -6,6 +6,9 @@ using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using CreateTaskResult = ModelContextProtocol.Extensions.Tasks.CreateTaskResult; +using GetTaskResult = ModelContextProtocol.Extensions.Tasks.GetTaskResult; +using McpTaskStatus = ModelContextProtocol.Extensions.Tasks.McpTaskStatus; namespace ModelContextProtocol.Tests.Server;