ext/tasks
SEP-2663 v2 tasks extension — separate go.mod sub-module.
Source: ext/tasks/README.md
ext/tasks — SEP-2663 Tasks Extension ¶
Go module for the v2 task surface defined by SEP-2663 (merged Final 2026-05-15). Provides server-directed async task execution registered as a protocol extension under capabilities.extensions["io.modelcontextprotocol/tasks"].
Looking for a guided walk-through? Read
docs/TASKS_TUTORIAL.md— covers when to use tasks vs sync vs MRTR, the GoAsync sentinel + middleware peek, lifecycle, the in-task input flow (TaskElicit/TaskSample), notifications + the G6 filter, cancellation semantics, and the multi-tenancy caveat on stateless. For the sibling MRTR surface (SEP-2322 multi-round-trip requests), seedocs/MRTR_TUTORIAL.md. This README is the API reference; the tutorials are the conceptual walk-throughs.
Why a sub-module ¶
SEP-2663 defines tasks as an extension, not a top-level capability. ext/tasks mirrors the same pattern ext/auth and ext/ui use: a separate go.mod consumed by servers that opt into the extension. v1 (the legacy capabilities.tasks surface) stays in server/ as RegisterTasksV1 and continues to ship until decommissioned.
Quickstart ¶
import (
"github.com/panyam/mcpkit/server"
"github.com/panyam/mcpkit/ext/tasks"
)
srv := server.NewServer(info)
srv.RegisterTool(
core.ToolDef{
Name: "slow_compute",
Description: "...",
Execution: &core.ToolExecution{TaskSupport: core.TaskSupportOptional},
},
func(ctx core.ToolContext, req core.ToolRequest) (core.ToolResponse, error) {
// Handlers whose work is genuinely async opt into the continuation
// goroutine by returning core.GoAsyncResult{} — see "Handler pattern" below.
if tasks.GetTaskContext(ctx) == nil {
return core.GoAsyncResult{}, nil
}
// Real work runs here, with TaskContext available for TaskElicit /
// TaskSample / SetStatus / progress emission under the G6 filter.
return core.TextResult("done"), nil
},
)
tasks.Register(tasks.Config{Server: srv})
tasks.Register installs the v2 middleware that intercepts tools/call for task-eligible tools, registers the tasks/get / tasks/update / tasks/cancel method handlers (all gated on the client declaring the extension), and advertises the extension in the initialize response.
ToolHandler returns the sealed core.ToolResponse interface; the middleware dispatches on the concrete variant — ToolResult, InputRequiredResult, CreateTaskResult, or GoAsyncResult.
Handler pattern ¶
mcpkit’s v2 middleware runs the handler synchronously first and then dispatches on the concrete ToolResponse variant it returned. The handler chooses one of three shapes:
| Handler returns | Middleware does | When to use |
|---|---|---|
core.InputRequiredResult via ctx.RequestInput(...) |
Passes through unchanged; no task created | SEP-2322 MRTR round — gather input before deciding whether to escalate |
core.GoAsyncResult{} |
Mints a task, spawns continuation goroutine that re-invokes the handler with TaskContext attached, returns CreateTaskResult |
Slow / blocking work, TaskElicit / TaskSample calls, progress emission that should be filtered |
core.ToolResult{...} (a regular sync result) |
Wraps as a born-terminal task (status: completed, result stored, one notifications/tasks event fired), returns CreateTaskResult |
Sync work that finishes immediately on a TaskSupport=optional/required tool |
The continuation goroutine re-invokes the same handler with a TaskContext accessible via tasks.GetTaskContext(ctx). The handler typically branches on that:
func myHandler(ctx core.ToolContext, req core.ToolRequest) (core.ToolResponse, error) {
if tasks.GetTaskContext(ctx) != nil {
// Continuation: do the real work. TaskElicit / TaskSample / SetStatus
// are all available; emissions are filtered per SEP-2663 G6.
return doRealWork(ctx, req)
}
// Optional MRTR phase: gather input via ctx.RequestInput first.
// ctx.RequestInput returns (core.InputRequiredResult, error) directly;
// InputRequiredResult satisfies ToolResponse.
if ctx.InputResponse("user_name") == nil {
return ctx.RequestInput(core.InputRequests{
"user_name": core.InputRequest{Method: "elicitation/create", Params: ...},
})
}
// MRTR complete; defer the rest to the continuation goroutine.
return core.GoAsyncResult{}, nil
}
The examples/mrtr reference fixture’s test_tool_with_task walks this exact pattern end-to-end (drives the matching mrtr-tasks-composition conformance scenario). For the full conceptual walk-through — MRTR as a stateless continuation primitive, capabilities across wires, progressToken, the G6 filter replacement table, MRTR vs push vs task-input-flow — see docs/MRTR_TUTORIAL.md.
G6 filter scope: the SEP-2663 G6 session-notify filter (notifications/progress and notifications/message MUST NOT be sent on tasks) is installed only on the continuation goroutine’s bgCtx. A sync-returning handler runs on the unfiltered POST ctx — it is responsible for not leaking those notifications itself.
Surface ¶
| Symbol | Purpose |
|---|---|
tasks.Register(tasks.Config) |
Canonical entry point. |
tasks.Config |
Holds Server, Store, DefaultTTLMs, DefaultPollMs, TracerProvider. |
tasks.TaskContext |
Typed-context for tool handlers running as v2 tasks. Exposes TaskID(), ProgressToken(), SetStatus(status), TaskElicit(req), TaskSample(req). |
tasks.WithTaskContext / tasks.GetTaskContext |
Context wiring (mirrors core’s typed-context pattern). |
Tracing (SEP-414 P6 — issue 659) ¶
Optional. Set Config.TracerProvider to opt the runtime into span-link instrumentation of the async task lifecycle:
tasks.Register(tasks.Config{
Server: srv,
TracerProvider: mcpotel.NewProvider(otelTP), // or any core.TracerProvider
})
What gets emitted:
task.executespan on the GoAsync path — a NEW root trace (not a child of the create span — the work outlives thetools/calldispatch span) carrying aLinkback to the originatingtools/callcreate span. Attributes:mcp.task.id(at start),mcp.task.status(stamped at End from the final stored status —completed/failed/cancelled/input_required).RecordErrorfires on protocol-level failures (mwErr, resp.Error, unexpected result shape, panic recover); handler-returned errors map tocompletedwithIsError=trueper SEP-2663 semantics.AddLinkon eachtasks/get/tasks/update/tasks/canceldispatch span — points back to the originating create span so a backend can pivot from any poll into the whole lifecycle.
Nil or core.NoopTracerProvider{} (the default) skips the install — zero overhead, zero allocation. ext/tasks depends on core only; no compile-time dep on ext/otel. The contract details (core.WithNewRootSpan, core.LinkedTracerProvider, core.Link) live in docs/SEP_414_OTEL.md § Span links and § New-root-span marker.
The wire types (CreateTaskResult, DetailedTask, UpdateTaskRequest, TaskInfoV2, etc.) live in core/task_v2.go since they’re consumed by both server and client.
Relationship to server/ ¶
- ext/tasks uses
server.Server,server.Middleware,server.MethodHandler,server.Registry,server.TaskStore,server.NewInMemoryStore. These are the generic server primitives any extension consumes. - ext/tasks duplicates v2-flavoured pieces (
TaskContext,activeTask,generateTaskID) rather than sharing with v1’s versions inserver/. Rationale: v1 is frozen and decommed-bound; once it retires, deletion inserver/is total without unwinding cross-package shared types. - The previous
RegisterTasksHybrid(a single helper installing both v1 and v2 on one server) was dropped during the move. Seedocs/TASKS_V2_MIGRATION.mdfor the recommended two-call pattern.
Where the conformance suite lives ¶
panyam/mcpconformance on the feat/tasks-mrtr-extension branch. Run via make testconf-tasks-v2 in the root repo — it spawns examples/tasks-v2/tasks-v2 --serve as the reference implementation fixture.
V1-RETIREMENT markers ¶
Code that should be reconsidered when v1 retires is tagged V1-RETIREMENT: in comments. git grep V1-RETIREMENT enumerates the cleanup list at retirement time.