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