Skip to content

Extract Tasks into ModelContextProtocol.Extensions.Tasks (typed seams)#26

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

Extract Tasks into ModelContextProtocol.Extensions.Tasks (typed seams)#26
jeffhandley wants to merge 3 commits into
mainfrom
jeffhandley/tasks-ext-typed-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 generic, reusable public seams
(no Tasks-specific surface, no InternalsVisibleTo).

New public Core API surface (the seams)

namespace ModelContextProtocol.Protocol;

// Generic "either the result, or a server-chosen alternate result" envelope.
// Used by request handlers that may answer a request with a different shape
// (e.g. a Tasks handler answering tools/call with a CreateTaskResult).
public sealed class ResultOrAlternate<TResult>
{
    public ResultOrAlternate(TResult result);
    public ResultOrAlternate(object alternate);
    public bool HasAlternate { get; }
    public TResult Result { get; }
    public object Alternate { get; }
}
namespace ModelContextProtocol.Server;

// Generic, strongly-typed request handler an extension can register against Core.
public sealed class McpServerRequestHandler { /* params/result typed delegate */ }

public sealed partial class McpServerOptions
{
    // Extension-registered request handlers, keyed by method.
    public IDictionary<string, McpServerRequestHandler> RequestHandlers { get; }
}

public sealed partial class McpServerHandlers
{
    // Filter/handler pair that may return an alternate result for tools/call.
    public IList<Func<...>> CallToolWithAlternateFilters { get; }
    public Func<RequestContext<CallToolRequestParams>, CancellationToken,
        ValueTask<ResultOrAlternate<CallToolResult>>>? CallToolWithAlternateHandler { get; set; }
}

public static partial class McpServerBuilderExtensions
{
    // Public registration hook for extension-owned request handlers.
    public static IMcpServerBuilder WithRequestHandler<TParams, TResult>(
        this IMcpServerBuilder builder, string method,
        Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler);
}

public abstract partial class McpServer
{
    // Scoped redirection of outgoing server->client requests (sampling/elicitation/roots).
    // Returns a scope that restores the previous interceptor on dispose.
    public IDisposable InterceptOutgoingRequests(
        Func<JsonRpcRequest, CancellationToken, ValueTask<JsonRpcResponse?>> interceptor);
}
namespace ModelContextProtocol.Client;

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

Protocol-version gating reuses existing public surface rather than adding a new
draft-detection API: the extension links McpHttpHeaders.July2026ProtocolVersion /
IsJuly2026OrLaterProtocolVersion and floors on the July 2026 (or later) revision that
introduces tasks.

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
    AsyncLocal redirection, and the client transparent-polling loop -- re-expressed
    on top of the generic 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 (typed seams)

  • Strongly typed and AOT-friendly. ResultOrAlternate<TResult> and
    McpServerRequestHandler are generic and reusable beyond Tasks, so Core exposes a
    general extensibility primitive rather than a Tasks-shaped one.
  • Scoped, not ambient. Outgoing-request redirection uses
    InterceptOutgoingRequests(...) returning an IDisposable scope rather than a
    public mutable static AsyncLocal, keeping the redirection lifetime explicit.
  • No InternalsVisibleTo. The extension depends only on public Core API, exactly
    like Apps.

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:20
- Add ResultOrAlternate<TResult> replacing task-specific types in server pipeline
- Add McpServerRequestHandler for custom request handler registration (seam #1)
- Add McpServerOptions.RequestHandlers property with wiring in McpServerImpl
- Rename CallToolWithTaskHandler/Filters to CallToolWithAlternateHandler/Filters
- Rename SetTaskAugmented to SetWithAlternate (remove tasks/get guard)
- Rename InvokeToolAsTask to InvokeToolWithAlternate
- Rename BuildInitialTaskToolFilter to BuildInitialAlternateToolFilter
- Make McpClient.ResolveInputRequestsAsync public (seam #4)
- Update test references to use new names
- Adapt TaskHandlerConfigurationValidationTests for removed guard

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create src/ModelContextProtocol.Extensions.Tasks/ with csproj, JSON context,
  server builder extensions, client extension methods
- Move task protocol DTOs (CreateTaskResult, GetTaskResult, UpdateTask*,
  CancelTask*, McpTaskStatus, TaskStatusNotificationParams) to extension
- Move server types (IMcpTaskStore, InMemoryMcpTaskStore, McpTaskInfo,
  InputResponseReceivedEventArgs) to extension
- Move task constants (RequestMethods.Tasks*, NotificationMethods.TaskStatus*,
  MetaKeys.RelatedTask, McpExtensions.Tasks) into TasksProtocol static class
- Delete McpTaskExecutionContext, ResultOrCreatedTask from Core
- Remove ~18 task [JsonSerializable] entries from McpJsonUtilities
- Remove McpServerOptions.TaskStore and McpClientOptions.MaxConsecutiveStuckPolls
- Remove task client methods from McpClient (CallToolRawAsync, PollTaskToCompletion,
  GetTaskAsync, UpdateTaskAsync, CancelTaskAsync, GetMetaWithTaskCapability,
  ThrowIfTasksNotSupported)
- Remove task server methods/handlers (GetTaskHandler, UpdateTaskHandler,
  CancelTaskHandler, ConfigureTasks, InvokeToolAsTask, task cancellation sources)
- Add Tasks_DiagnosticId (MCPEXP001) to Experimentals.cs
- Extension WithTasks(store) registers request handlers via seam #1,
  alternate filter via seam #2, interceptor via seam #3
- Extension client methods: CallToolAsTaskAsync, CallToolWithPollingAsync,
  GetTaskAsync, UpdateTaskAsync, CancelTaskAsync
- Manually serialize/deserialize InputResponses in UpdateTaskAsync to handle
  internal Core property visibility across assembly boundary
- Update test project and TasksExtension sample to reference new package
- Update all task test files with new using and extension API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Restore the inline-result branch comment on the !raw.IsTask path
- Restore the CompletedTaskResult JsonElement deserialization comment
- Restore the RunReport sleep-vs-real-work comment

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