Skip to content

Extract Tasks into ModelContextProtocol.Extensions.Tasks (raw seams)#27

Open
jeffhandley wants to merge 3 commits into
mainfrom
jeffhandley/tasks-ext-raw-seams
Open

Extract Tasks into ModelContextProtocol.Extensions.Tasks (raw seams)#27
jeffhandley wants to merge 3 commits into
mainfrom
jeffhandley/tasks-ext-raw-seams

Conversation

@jeffhandley

Copy link
Copy Markdown
Owner

Summary

Extracts the SEP-2663 Tasks feature out of ModelContextProtocol.Core into a new
bolt-on package, ModelContextProtocol.Extensions.Tasks, mirroring the existing
ModelContextProtocol.Extensions.Apps. After this change Core has no compile-time
knowledge of Tasks
; the extension references the main package and adds Tasks behavior
entirely from the side.

Unlike Apps -- which was pure additive metadata over existing public APIs -- Tasks was
woven into Core's request-dispatch pipeline (task-augmented tools/call, AsyncLocal
redirection of sampling/elicitation/roots, and transparent client polling). To make
a clean side bolt-on possible, Core gains a small set of minimal "raw" extensibility
hooks
(delegate/registry seams, no Tasks-specific surface, no InternalsVisibleTo).

New public Core API surface (the seams)

namespace ModelContextProtocol.Server;

// Raw (untyped JSON-RPC) request handler delegate an extension can register.
public delegate ValueTask<JsonRpcResponse> McpRawRequestHandler(
    JsonRpcRequest request, CancellationToken cancellationToken);

// Registry an extension uses to install raw handlers and wrap existing ones
// (e.g. wrap tools/call to offer task-augmented dispatch).
public interface IMcpServerRawHandlerRegistry
{
    McpServer Server { get; }
    bool ContainsHandler(string method);
    void SetHandler(string method, McpRawRequestHandler handler);
    bool TryWrapHandler(string method, Func<McpRawRequestHandler, McpRawRequestHandler> wrap);
    bool IsDraftProtocolRequest(JsonRpcRequest request);
}

public sealed partial class McpServerOptions
{
    // Extension callbacks invoked to configure the raw handler registry at startup.
    public IList<Action<IMcpServerRawHandlerRegistry>> RawRequestHandlerConfigurators { get; }
}

// Interceptor for outgoing server->client requests (sampling/elicitation/roots),
// installed via a public AsyncLocal so the extension can redirect them to a task store.
public delegate ValueTask<JsonRpcResponse?> McpOutgoingRequestInterceptor(
    JsonRpcRequest request, CancellationToken cancellationToken);

public abstract partial class McpServer
{
    public static AsyncLocal<McpOutgoingRequestInterceptor?> CurrentOutgoingRequestInterceptor { get; }
}
namespace ModelContextProtocol;

public abstract partial class McpSession
{
    // Public draft-protocol predicate used by the extension's gating.
    public bool IsDraftProtocol();
}
namespace ModelContextProtocol.Client;

public abstract partial class McpClient
{
    // Public hook letting an extension resolve server input requests during polling.
    public ValueTask ResolveInputRequestsAsync(...);
}

What moves to the extension

All Tasks-specific types and machinery move to ModelContextProtocol.Extensions.Tasks:

  • Protocol DTOs: CreateTaskResult, GetTaskRequestParams/Result,
    UpdateTaskRequestParams/Result, CancelTaskRequestParams/Result,
    TaskStatusNotificationParams, McpTaskStatus, ResultOrCreatedTask.
  • Server: IMcpTaskStore, InMemoryMcpTaskStore, McpTaskExecutionContext,
    McpTaskInfo, InputResponseReceivedEventArgs.
  • The task-augmented tools/call wrapper, the sampling/elicitation/roots
    redirection, and the client transparent-polling loop -- re-expressed on top of the
    raw Core seams above.
  • tasks/* request methods, notifications/tasks/status, the task result
    discriminator, the RelatedTask meta key, and the tasks extension capability
    string -- all owned by the extension.
  • Tasks [JsonSerializable] registrations move to the extension's JSON context.

Public entry points become extension methods, mirroring Apps:
builder.WithTasks(...) for servers and client.GetTaskAsync(...) /
CancelTaskAsync / UpdateTaskAsync for clients.

Design rationale (raw seams)

  • Smallest possible Core surface. Core exposes untyped delegate/registry hooks and
    lets the extension own all typing and shape, so Core carries the least Tasks-shaped
    baggage.
  • Wrap-existing semantics. TryWrapHandler lets the extension layer task
    augmentation over Core's existing tools/call handler without Core knowing why.
  • No InternalsVisibleTo. The extension depends only on public Core API, like Apps.

Trade-offs vs the typed-seams alternative

  • Uses a public mutable static AsyncLocal
    (McpServer.CurrentOutgoingRequestInterceptor) for redirection rather than a scoped
    IDisposable; simpler wiring but ambient lifetime.
  • Hooks are untyped (JsonRpcRequest/JsonRpcResponse) rather than generic typed
    primitives, so less reusable outside Tasks and less AOT-shaped.
  • This branch removes two Tasks tests
    (TaskStoreOrphanedTaskTests, TaskHandlerConfigurationValidationTests) whose
    scenarios no longer map onto the raw-seam wiring.

Breaking changes

This is a preview SDK and the change is intentionally breaking:

Validation

  • dotnet build clean (0/0) across net10.0/net9.0/net8.0/netstandard2.0 with
    TreatWarningsAsErrors=true.
  • Tasks test suite green.

jeffhandley and others added 3 commits June 25, 2026 22:26
- Add IMcpServerRawHandlerRegistry seam (McpRawRequestHandler delegate, registry over request handlers, IsDraftProtocolRequest) and McpServerOptions.RawRequestHandlerConfigurators
- Add McpServer.CurrentOutgoingRequestInterceptor AsyncLocal seam and McpOutgoingRequestInterceptor delegate; route SampleAsync/ElicitAsync/RequestRootsAsync through it
- Make McpSession.IsDraftProtocol public and McpClient.ResolveInputRequestsAsync public abstract (MCPEXP002)
- Remove Tasks coupling from Core: ConfigureTasks, task-store tools/call wrapper, SetTaskAugmented, CallToolWithTaskHandler/Filters, task lifecycle handlers, MaxConsecutiveStuckPolls, task method/notification/meta consts, task JsonSerializable entries
- Rewrite client CallToolAsync to a plain tools/call returning CallToolResult
- Relocate Tasks DTOs and store types into ModelContextProtocol.Extensions.Tasks; drop McpTaskExecutionContext
- Add ModelContextProtocol.Extensions.Tasks project and TasksJsonContext

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add TaskMethods/TaskMetaKeys consts (tasks/get|update|cancel, notification, related-task)
- Add McpServerTasksExtensions: WithTasks(McpServerOptions/IMcpServerBuilder), SendTaskStatusNotificationAsync, store-backed tasks/* raw handlers gated to draft, and a tools/call wrapper that runs tasks in the background via the outgoing-request interceptor seam
- Add McpClientTasksExtensions: CallToolAsTaskAsync, CallToolRawAsync, GetTaskAsync/UpdateTaskAsync/CancelTaskAsync, PollTaskToCompletionAsync
- Add JsonNode to TasksJsonContext; replace internal Throw with inline null checks in ResultOrCreatedTask
- Include only the attribute polyfills the Tasks DTOs need (required/init) on netstandard2.0
- Relax Result/RequestParams/NotificationParams base ctors to protected so the moved DTOs can derive across the assembly boundary
- Register the new project in ModelContextProtocol.slnx

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ProjectReference from tests and sample to ModelContextProtocol.Extensions.Tasks
- Migrate task tests to public extension API (WithTasks, CallToolAsTaskAsync, GetTaskAsync, UpdateTaskAsync, CancelTaskAsync, CallToolRawAsync)
- Rename const references to TaskMethods.Get/Update/Cancel/StatusNotification
- Rewrite McpServerTaskTests and TaskPollStuckDetectorTests to use the store-based public API with custom IMcpTaskStore doubles
- Drop manual CallToolWithTaskHandler tests (TaskStoreOrphanedTaskTests, TaskHandlerConfigurationValidationTests) that exercised configuration paths removed from Core
- Add public McpTasksJsonUtilities.DefaultOptions so the public task DTOs can be serialized under source generation when reflection is disabled
- Point task DTO serialization in tests at McpTasksJsonUtilities.DefaultOptions instead of the Core McpJsonUtilities.DefaultOptions
- Send and read tasks/update inputResponses explicitly in the extension because RequestParams serializes them through an internal backing property the extension context cannot access
- Update samples/TasksExtension to the WithTasks plus CallToolAsTaskAsync API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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