Skip to content

Draft: Prototype legacy MCP Tasks 2025-11-30 compatibility#28

Draft
jeffhandley wants to merge 4 commits into
mainfrom
jeffhandley/legacy-tasks-compatibility
Draft

Draft: Prototype legacy MCP Tasks 2025-11-30 compatibility#28
jeffhandley wants to merge 4 commits into
mainfrom
jeffhandley/legacy-tasks-compatibility

Conversation

@jeffhandley

@jeffhandley jeffhandley commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Overview

This exploratory POC adds ModelContextProtocol.Legacy.Tasks-2025-11-30, a separately named compatibility package for the experimental MCP Tasks draft used by SDK 1.x. It runs beside ModelContextProtocol.Extensions.Tasks: after normal protocol negotiation, 2025-11-30 activates the legacy wire behavior and 2026-07-28+ activates modern extension-based Tasks.

The package preserves legacy top-level tasks capabilities, tools/call task augmentation, lifecycle methods, status notifications, and tool descriptor metadata. In particular, execution: { "taskSupport": "optional" } remains available through McpClientTool.ProtocolTool.AdditionalProperties, so an upgraded client can retain separate direct and task-augmented execution pipelines.

Protocol selection and execution

flowchart TD
    Options["McpClientOptions"]
    Options -->|"EnableLegacyTasks()"| Pinned["Pin 2025-11-30"]
    Options -->|"EnableTasksMigration()"| Auto["Do not pin a version"]
    Auto --> Discover["Prefer server/discover"]
    Auto -->|"initialize fallback"| Legacy["Negotiate 2025-11-30"]
    Pinned --> Legacy
    Discover --> Modern["Negotiate 2026-07-28+"]
    Legacy --> LegacyTasks["Legacy Tasks wire protocol"]
    Modern --> ModernTasks["Modern Tasks extension"]
    LegacyTasks --> Migration["McpTaskMigrationClient"]
    ModernTasks --> Migration
Loading
flowchart TD
    List["client.ListToolsAsync()"] --> Descriptor["McpClientTool.ProtocolTool"]
    Descriptor --> LegacyCheck{"Legacy execution.taskSupport?"}
    LegacyCheck -->|"optional / required"| LegacyTask["CallToolAsLegacyTaskAsync\npoll tasks/result"]
    LegacyCheck -->|"otherwise"| Direct["Existing CallAsync / ExecuteTool"]
    Descriptor --> ModernCheck{"Modern Tasks extension?"}
    ModernCheck -->|"yes"| ModernTask["CallToolWithPollingAsync"]
    ModernCheck -->|"no"| Direct
Loading

Public API

Legacy package

Area Surface
Protocol LegacyTasksProtocol, McpLegacyTask, McpLegacyTaskStatus, McpLegacyTaskMetadata, and legacy lifecycle/status DTOs
Client EnableLegacyTasks, EnableTasksMigration, CallToolAsLegacyTaskAsync, GetLegacyTaskAsync, ListLegacyTasksAsync, GetLegacyTaskPayloadAsync, CancelLegacyTaskAsync, McpTaskMigrationClient
Server WithLegacyTasks, ILegacyMcpTaskStore, InMemoryLegacyMcpTaskStore, source-compatible InMemoryMcpTaskStore, tool task-support helpers, and status notifications
Deprecated source facade 1.x ModelContextProtocol.Protocol task models plus legacy McpClient and McpServer extension methods, marked [Obsolete] with diagnostic MCP9007

There are no protected APIs in the package.

Required Core surface

Surface Purpose
Tool.AdditionalProperties, client/server capability additional properties Preserve legacy descriptor and top-level capability data.
McpServerRequestHandler.IsApplicable Dispatch shared lifecycle method names by negotiated protocol version.
McpServerRequestHandler.HandlerWithServer Expose the session-bound server to raw lifecycle handlers for HTTP ownership checks.
McpClientHandlers.RequestHandlers, McpClientRequestHandler Handle client-bound legacy task-augmented sampling and elicitation requests.

Client examples

A. Legacy only

var options = new McpClientOptions().EnableLegacyTasks();
await using var client = await McpClient.CreateAsync(transport, options);

var tool = (await client.ListToolsAsync()).Single(t => t.Name == "long-running-tool");
var taskSupport = tool.ProtocolTool.AdditionalProperties?["execution"]
    .GetProperty("taskSupport").GetString();

CallToolResult result = taskSupport is "optional" or "required"
    ? (await client.CallToolAsLegacyTaskAsync(new() { Name = tool.Name })).Result!
    : await tool.CallAsync();

B. Modern only

await using var client = await McpClient.CreateAsync(transport);
CallToolResult result = await client.CallToolWithPollingAsync(
    new CallToolRequestParams { Name = "long-running-tool" });

C. Both, with post-negotiation selection

var options = new McpClientOptions().EnableTasksMigration();
await using var client = await McpClient.CreateAsync(transport, options);

var tasks = client.CreateTaskMigrationClient(logger);
var tool = (await client.ListToolsAsync()).Single(t => t.Name == "long-running-tool");
CallToolResult result = await tasks.ExecuteToolAsync(
    tool, new CallToolRequestParams { Name = tool.Name });

CreateTaskMigrationClient(logger) logs legacy selection at Information level so customers can identify servers that still need compatibility support.

Server examples

A. Legacy only

services.Configure<McpServerOptions>(options =>
    options.ProtocolVersion = LegacyTasksProtocol.ProtocolVersion);

builder.WithTools<MyTools>()
    .WithLegacyTasks(new InMemoryLegacyMcpTaskStore());

For Streamable HTTP legacy Tasks, use a stateful server:

builder.WithHttpTransport(options => options.Stateless = false);

B. Modern only

builder.WithTools<MyTools>()
    .WithTasks(new InMemoryMcpTaskStore());

C. Both revisions

builder.WithTools<MyTools>()
    .WithLegacyTasks(new InMemoryLegacyMcpTaskStore())
    .WithTasks(new InMemoryMcpTaskStore());

Leave ProtocolVersion unset for the SDK's normal version negotiation. Legacy requests route to this package; modern requests route to ModelContextProtocol.Extensions.Tasks.

Challenges and risks

  1. The two wire protocols differ materially; the POC preserves both rather than translating one into the other.
  2. Lifecycle method collisions require protocol-version routing.
  3. Legacy HTTP task visibility requires Stateless = false; the HTTP tests prove session isolation.
  4. Reverse sampling/elicitation uses new raw client request-handler extensibility and remains legacy-only.
  5. The restored facade is source-compatible after recompilation, not binary-compatible. Removed TaskStore option properties are not restored.
  6. In-memory stores are not a production durability or authorization solution.
  7. The added Core surface requires API review before production adoption.
  8. Restored 1.x type names can collide with modern Tasks names; dual-version callers should use aliases or the migration facade.
  9. Object-based 1.x argument overloads are not Native AOT safe.

Validation

  • dotnet build ModelContextProtocol.slnx --no-restore succeeds with zero warnings/errors.
  • Package-owned legacy tests: 202 pass on net10.0, net9.0, and net8.0; on net472, 180 pass and 22 reverse-task tests are skipped by the repository-wide Windows issue Running ModelContextProtocol.Tests in net472 is causing VS test platform to hang modelcontextprotocol/csharp-sdk#587 guard.
  • Package-owned HTTP tests: all 8 pass on net10.0, net9.0, and net8.0.
  • Focused coverage proves legacy execution.taskSupport survives tools/list and ordinary tool calls remain direct rather than being forced into Tasks.

Assessment

Feasible as a separately versioned migration bridge, but not ready to merge as a production feature without API and design review. It lets existing customers upgrade without consolidating direct and task-augmented pipelines immediately, while providing telemetry to retire legacy support. Production adoption should first settle response shaping by protocol version, durable/scoped store guidance, notification ergonomics, and the Core public API review.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c2b4e8e7-6c94-4b5c-81ff-c3de5ea600ed
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c2b4e8e7-6c94-4b5c-81ff-c3de5ea600ed
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c2b4e8e7-6c94-4b5c-81ff-c3de5ea600ed
Move legacy Tasks compatibility coverage into its package-owned test project and remove the core test project dependency.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c2b4e8e7-6c94-4b5c-81ff-c3de5ea600ed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant