tasks

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

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), see docs/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.execute span on the GoAsync path — a NEW root trace (not a child of the create span — the work outlives the tools/call dispatch span) carrying a Link back to the originating tools/call create span. Attributes: mcp.task.id (at start), mcp.task.status (stamped at End from the final stored status — completed / failed / cancelled / input_required). RecordError fires on protocol-level failures (mwErr, resp.Error, unexpected result shape, panic recover); handler-returned errors map to completed with IsError=true per SEP-2663 semantics.
  • AddLink on each tasks/get / tasks/update / tasks/cancel dispatch 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 in server/. Rationale: v1 is frozen and decommed-bound; once it retires, deletion in server/ 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. See docs/TASKS_V2_MIGRATION.md for 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.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Register

func Register(cfg Config)

Register hooks up v2 tasks support on the given server:

  • Advertises the io.modelcontextprotocol/tasks extension in initialize (replacing the v1 ServerCapabilities.Tasks declaration).
  • Installs middleware that intercepts tools/call for task-eligible tools (server-directed, no client task param needed). Task creation is gated on the client supporting the extension — either at session level (initialize handshake) or per-request (SEP-2575 _meta).
  • Registers tasks/get, tasks/update, and tasks/cancel handlers, gated on session-level extension support; otherwise the handlers return -32021 (Missing Required Client Capability, SEP-2575) with a machine-readable `requiredCapabilities` payload so unsupported clients can self-describe what to add.
  • Does NOT register tasks/result or tasks/list (removed in v2).
  • Does NOT call SetTasksCap — v2 tasks live under capabilities.extensions, not the v1 ServerCapabilities.Tasks slot.

Must be called before accepting connections.

func WithTaskContext

func WithTaskContext(ctx context.Context, tc *TaskContext) context.Context

WithTaskContext returns a context carrying the TaskContext.

Types

type Config

type Config struct {
	// Store is the task state backend. If nil, an InMemoryTaskStore is used.
	Store server.TaskStore

	// Server is the MCP server to register tasks on.
	Server *server.Server

	// DefaultTTLMs is the default task TTL in integer milliseconds. Per
	// SEP-2663 the wire surfaces ttlMs and the store also uses ms, so this
	// value flows through unchanged. Default: 300000 (5 minutes).
	DefaultTTLMs int

	// DefaultPollMs is the suggested poll interval in milliseconds,
	// returned to clients in CreateTaskResult as pollIntervalMs. Default: 1000 (1 second).
	DefaultPollMs int

	// TracerProvider opts the runtime into SEP-414 P6 instrumentation of
	// the async task lifecycle (issue 659). When set, spawnGoAsyncTask
	// emits a `task.execute` span as a NEW root trace (so the long-running
	// task work doesn't render as a multi-hour child under a few-ms create
	// span) carrying a Link back to the originating tools/call create
	// span. Each tasks/get / tasks/update / tasks/cancel dispatch span
	// additionally Adds a Link to the originating create span so a backend
	// can pivot from any poll into the whole task lifecycle.
	//
	// Span attributes:
	//   - `mcp.task.id`     — task identifier
	//   - `mcp.task.status` — final status stamped at End (completed /
	//                         failed / cancelled / input_required)
	//
	// On the failure paths (handler error, protocol error, panic recover)
	// the span also gets RecordError so observability backends count it
	// as an error trace and surface the underlying message.
	//
	// Nil or core.NoopTracerProvider{} (the default) skips the install —
	// zero allocation, no spans emitted. ext/tasks depends on the core
	// abstraction only; no compile-time dep on ext/otel.
	TracerProvider core.TracerProvider
}

Config holds the options for registering v2 tasks support on an MCP server.

type TaskContext

type TaskContext struct {
	core.ToolContext
	// contains filtered or unexported fields
}

TaskContext is the typed context for tool handlers running as a v2 background task. It embeds core.ToolContext and adds task-specific methods (TaskID, ProgressToken, TaskElicit, TaskSample, SetStatus).

Tool handlers retrieve this via GetTaskContext(ctx). It is nil for synchronous (non-task) tool invocations.

Usage:

func myToolHandler(ctx core.ToolContext, input MyInput) (string, error) {
    tc := tasks.GetTaskContext(ctx)
    if tc != nil {
        result, err := tc.TaskElicit(core.ElicitationRequest{...})
    }
    return "done", nil
}

Distinct from server.TaskContext (which serves the v1 surface). A tool registered for both v1 and v2 (via RegisterHybrid) sees the appropriate type when invoked under each runtime.

func GetTaskContext

func GetTaskContext(ctx core.ToolContext) *TaskContext

GetTaskContext retrieves the TaskContext from a tool handler's context, binding it to the provided ToolContext for elicitation/sampling. Returns nil if the tool was not invoked as a v2 task.

func (*TaskContext) ProgressToken

func (tc *TaskContext) ProgressToken() any

ProgressToken returns the client's original _meta.progressToken from the tools/call request. Use this instead of TaskID() for EmitProgress so progress notifications correlate with what the client expects. Returns nil if the client didn't send a progressToken.

func (*TaskContext) SetStatus

func (tc *TaskContext) SetStatus(status core.TaskStatus) error

SetStatus transitions the task to a new status and emits a notifications/tasks status event so polling/streaming clients pick up the new state.

func (*TaskContext) TaskElicit

TaskElicit asks the client for elicitation input from inside a running v2 task. The request is stashed on the task's inputState under a monotonic key, the task transitions to input_required, and the goroutine blocks on a per-key waiter channel. The client observes the pending request via tasks/get's DetailedTask.InputRequests and resumes the task by sending the matching response via tasks/update. ctx cancellation unblocks the wait with the corresponding context error.

Status transitions: working → input_required → working.

func (*TaskContext) TaskID

func (tc *TaskContext) TaskID() string

TaskID returns the task's unique identifier.

func (*TaskContext) TaskSample

TaskSample asks the client for a sampling/createMessage response from inside a running v2 task. See TaskElicit for the input-state flow.

Status transitions: working → input_required → working.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL