From e5ca0269cbf2ca846acd07fc772ece1bcd185c6d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:24:56 -0400 Subject: [PATCH] docs: rewrite background tasks pages for SEP-2663 Server (servers/tasks.mdx) and client (clients/tasks.mdx) docs rewritten for the extension model: add_extension(TasksExtension()), the guard pattern for in-task input (no imperative ctx.elicit()), tools-only, and the modern-protocol requirement (the inverse of the old SEP-1686 legacy-only note). Mechanical fixes elsewhere for the same reason: telemetry.mdx's tasks/{operation} method list (get/update/cancel, not result/list), client.mdx's legacy-only feature list (tasks moved to modern-only) and extension-composition paragraph (describes the tasks ClientExtension, not the removed notification binding), and stale SEP-1686 references in the FastMCP 2 upgrade guide. v4-notes status lines updated to Shipped (#4602, #4603). --- docs/clients/client.mdx | 5 +- docs/clients/tasks.mdx | 169 ++++++------------ .../development/v4-notes/background-tasks.mdx | 2 +- docs/development/v4-notes/feature-program.mdx | 4 +- docs/development/v4-notes/index.mdx | 2 +- docs/development/v4-notes/protocol-2026.mdx | 7 +- .../upgrading/from-fastmcp-2.mdx | 4 +- docs/servers/tasks.mdx | 135 +++++++++----- docs/servers/telemetry.mdx | 8 +- 9 files changed, 168 insertions(+), 168 deletions(-) diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx index 1024984c7..320a240df 100644 --- a/docs/clients/client.mdx +++ b/docs/clients/client.mdx @@ -188,9 +188,10 @@ Legacy mode is also what you need for the capabilities that depend on a live ses - **[Sampling](/clients/sampling)** — server-initiated LLM completion requests - **[Roots](/clients/roots)** — server-initiated requests for the client's roots - **[Elicitation](/clients/elicitation)** — server-initiated requests for user input, which modern connections replace with [input-required rounds](/clients/elicitation#input-required-rounds) -- **[Background tasks](/clients/tasks)** — submitting an operation with `task=True` - `client.ping()` and `transport.get_session_id()` +Conversely, [background tasks](/clients/tasks) are **modern-only**: the tasks capability is negotiated over `2026-07-28` connections, so `mode="legacy"` never triggers one and a task-enabled tool just runs synchronously. + A FastMCP server serves both eras, so a default client negotiates the modern one and these raise an era-specific error. Pinning the handshake restores them. You can also pin a specific modern protocol version to adopt it directly, without a discovery probe: @@ -282,7 +283,7 @@ from myproject.extensions import AppsExtension client = Client("https://example.com/mcp", extensions=[AppsExtension()]) ``` -Each extension's contributions are threaded into the underlying session. Notification bindings compose with FastMCP's own internal task-status binding rather than replacing it, so an extension that observes a custom notification and FastMCP's task tracking both work on the same connection. When a tool returns a shape an extension claims, `client.call_tool()` resolves it transparently through the owning claim's resolver and hands you back an ordinary result. Result claims and their advertisements are honored only on modern-era connections, so they are inert on a legacy handshake. +Each extension's contributions are threaded into the underlying session. FastMCP folds in its own internal extension for [background tasks](/clients/tasks) automatically, and your own extensions *compose* with it rather than replacing it — pass your own tasks extension with the same identifier if you need to override it. When a tool returns a shape an extension claims, `client.call_tool()` resolves it transparently through the owning claim's resolver and hands you back an ordinary result. Result claims and their advertisements are honored only on modern-era connections, so they are inert on a legacy handshake. For the rare case where you need to register additional result claims against an extension that is already advertised, pass them through `result_claims=`, keyed by the extension's identifier. Prefer declaring claims on the extension itself; this parameter merges extra claims with an extension's own. diff --git a/docs/clients/tasks.mdx b/docs/clients/tasks.mdx index 0a434e457..8182ba415 100644 --- a/docs/clients/tasks.mdx +++ b/docs/clients/tasks.mdx @@ -1,184 +1,133 @@ --- title: Background Tasks sidebarTitle: Tasks -description: Execute operations asynchronously and track their progress. +description: Call long-running tools without blocking, and answer questions they ask mid-run. icon: clock tag: "NEW" --- import { VersionBadge } from "/snippets/version-badge.mdx" - + -Use this when you need to run long operations asynchronously while doing other work. - -The MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results. +Some tool calls take a while. The MCP background tasks extension lets a server run one in the background instead of holding the request open, and FastMCP's client drives the whole thing for you — most of the time you don't need to know a call was tasked at all. -**Background tasks require the older MCP protocol.** FastMCP submits a task over the session that the `initialize` handshake opens, and protocol version `2026-07-28` has no equivalent. Clients default to `mode="auto"`, which negotiates the newest version both sides support, so the examples on this page pass `mode="legacy"`. See [protocol negotiation](/clients/client#protocol-negotiation). +**Background tasks require the modern protocol.** The tasks capability is negotiated over `2026-07-28` connections. `mode="auto"` (the client default) negotiates it automatically; `mode="legacy"` never does, so a tasked tool just runs synchronously for a legacy-pinned client. See [protocol negotiation](/clients/client#protocol-negotiation). -## Requesting Background Execution +## Transparent Calls -Pass `task=True` to run an operation as a background task: +Just call the tool. If the server runs it as a background task, `call_tool` polls it to completion under the hood and returns the same result you'd get from a synchronous call — the task is invisible. ```python from fastmcp import Client -async with Client(server, mode="legacy") as client: - # Start a background task - task = await client.call_tool("slow_computation", {"duration": 10}, task=True) +async with Client(server, mode="auto") as client: + result = await client.call_tool("slow_computation", {"duration": 10}) + print(result.data) +``` +This is the right default for most code: it works whether or not the server actually tasks the call, so you can write ordinary tool-calling code without checking server capabilities. + +## Driving a Task Explicitly + +When you want to do other work while a task runs — or check on it, or cancel it — use `call_tool_task` instead. It returns a `ToolTask` handle immediately rather than waiting for completion. + +```python +from fastmcp import Client +from fastmcp_tasks import call_tool_task + +async with Client(server, mode="auto") as client: + task = await call_tool_task(client, "slow_computation", {"duration": 10}) print(f"Task started: {task.task_id}") # Do other work while it runs... - # Get the result when ready result = await task.result() ``` -This works with tools, resources, and prompts: - -```python -tool_task = await client.call_tool("my_tool", args, task=True) -resource_task = await client.read_resource("file://large.txt", task=True) -prompt_task = await client.get_prompt("my_prompt", args, task=True) -``` - -## Task API - -All task types share a common interface. - -### Getting Results - -Call `await task.result()` or simply `await task` to block until the task completes: - -```python -task = await client.call_tool("analyze", {"text": "hello"}, task=True) - -# Wait for result (blocking) -result = await task.result() -# or: result = await task -``` +`call_tool_task` requires the server to actually run the call as a task — if the tool isn't `task=True`, or the server doesn't have the tasks extension registered, it raises `ToolError`. Use it when you specifically need the handle; use `call_tool` when you just want the result. ### Checking Status -Check the current status without blocking: - ```python status = await task.status() -print(f"{status.status}: {status.statusMessage}") +print(f"{status.status}: {status.status_message}") # status.status is "working", "input_required", "completed", "failed", or "cancelled" ``` ### Waiting with Control -Use `task.wait()` for more control over waiting. With no `state`, it returns when the task leaves `working`; pass a specific state when you need to wait for a particular transition: +`task.wait()` polls until a terminal state (or a specific one you name), without answering any input the task asks for — use it when you want to observe an `input_required` pause yourself rather than have it answered automatically. ```python # Wait up to 30 seconds for completion status = await task.wait(timeout=30.0) # Wait for a specific state -status = await task.wait(state="completed", timeout=30.0) +status = await task.wait(state="input_required", timeout=30.0) ``` -### Cancellation +### Getting the Result -Cancel a running task: +`task.result()` drives the task the rest of the way — including answering any input it asks for — and returns the finished result, same as `client.call_tool` would. Awaiting the task directly is shorthand for this. + +```python +result = await task.result() +# or: result = await task +``` + +By default a failed or cancelled task raises `ToolError`. Pass `raise_on_error=False` to `call_tool_task` to get an error result back instead. + +### Cancellation ```python await task.cancel() ``` -## Status Updates +Cancellation is cooperative — the task may still finish before the server notices the request. -Register callbacks to receive real-time status updates as the server reports progress: +## Answering Questions Mid-Task -```python -def on_status_change(status): - print(f"Task {status.taskId}: {status.status} - {status.statusMessage}") - -task.on_status_change(on_status_change) - -# Async callbacks work too -async def on_status_async(status): - await log_status(status) - -task.on_status_change(on_status_async) -``` - -### Handler Template +A task can pause partway through to ask a question, the same way a foreground [multi-round-trip](/clients/elicitation#input-required-rounds) tool does. Pass an `elicitation_handler` and both `call_tool` and `task.result()` answer it automatically as part of driving the task to completion: ```python from fastmcp import Client -def status_handler(status): - """ - Handle task status updates. +async def handle_elicitation(message, response_type, params, context): + return {"cuisine": "Thai", "vegetarian": True} - Args: - status: Task status object with: - - taskId: Unique task identifier - - status: "working", "input_required", "completed", "failed", or "cancelled" - - statusMessage: Optional progress message from server - """ - if status.status == "working": - print(f"Progress: {status.statusMessage}") - elif status.status == "completed": - print("Task completed") - elif status.status == "failed": - print(f"Task failed: {status.statusMessage}") - -task.on_status_change(status_handler) +async with Client(server, mode="auto", elicitation_handler=handle_elicitation) as client: + result = await client.call_tool("plan_dinner", {}) + print(result.data) ``` -## Graceful Degradation - -You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline. - -```python -task = await client.call_tool("my_tool", args, task=True) - -if task.returned_immediately: - print("Server executed immediately (no background support)") -else: - print("Running in background") - -# Either way, this works -result = await task.result() -``` - -This lets you write task-aware client code without worrying about server capabilities. +Without an `elicitation_handler`, a task that asks for input raises `ToolError` rather than hanging. See [server-side background tasks](/servers/tasks#gathering-input-mid-task) for how a tool asks a question in the first place. ## Example ```python import asyncio from fastmcp import Client +from fastmcp_tasks import call_tool_task async def main(): - async with Client(server, mode="legacy") as client: - # Start background task - task = await client.call_tool( - "slow_computation", - {"duration": 10}, - task=True, - ) + async with Client(server, mode="auto") as client: + # Return immediately and drive the task yourself + task = await call_tool_task(client, "slow_computation", {"duration": 10}) + print(f"Task started: {task.task_id}") - # Subscribe to updates - def on_update(status): - print(f"Progress: {status.statusMessage}") + # Do other work while the task runs + while True: + status = await task.status() + if status.status in ("completed", "failed", "cancelled"): + break + print(f"Still working... ({status.status})") + await asyncio.sleep(1) - task.on_status_change(on_update) - - # Do other work while task runs - print("Doing other work...") - await asyncio.sleep(2) - - # Wait for completion and get result result = await task.result() - print(f"Result: {result.content}") + print(f"Result: {result.data}") asyncio.run(main()) ``` diff --git a/docs/development/v4-notes/background-tasks.mdx b/docs/development/v4-notes/background-tasks.mdx index 8c0df9156..c8ac0a93f 100644 --- a/docs/development/v4-notes/background-tasks.mdx +++ b/docs/development/v4-notes/background-tasks.mdx @@ -2,7 +2,7 @@ title: Background Tasks (SEP-2663) --- -**Status: Designed — approved for implementation.** This page is the approved design for rebuilding FastMCP's background-task support on the `io.modelcontextprotocol/tasks` extension. It supersedes the earlier "delete the task machinery" direction recorded during the SDK v2 migration. Implementation is sequenced behind the [extension API](#the-extension-api); the [Feature Program](/development/v4-notes/feature-program#background-tasks-sep-2663) carries the one-line status. +**Status: Shipped (#4602, #4603).** This page is the approved design for rebuilding FastMCP's background-task support on the `io.modelcontextprotocol/tasks` extension. It supersedes the earlier "delete the task machinery" direction recorded during the SDK v2 migration. The [Feature Program](/development/v4-notes/feature-program#background-tasks-sep-2663) carries the one-line status; user-facing usage is documented at [Background Tasks](/servers/tasks) and [Background Tasks (client)](/clients/tasks). ## TL;DR diff --git a/docs/development/v4-notes/feature-program.mdx b/docs/development/v4-notes/feature-program.mdx index a0b0c7f3d..349bdf5a1 100644 --- a/docs/development/v4-notes/feature-program.mdx +++ b/docs/development/v4-notes/feature-program.mdx @@ -115,7 +115,7 @@ A cluster of protocol features tracked for v4. Their statuses have diverged: ## FastMCP-native extension API -**Status: Designed.** +**Status: Shipped (#4602).** MCP extensions (SEP-2133) are optional, capability-negotiated protocol features identified by a reverse-DNS string — `io.modelcontextprotocol/ui` (MCP Apps), `io.modelcontextprotocol/tasks` (SEP-2663). They are a genuinely new abstraction in SDK v2; they did not exist in v1. The SDK exposes them through an `Extension` server class that contributes a capability, additive request methods, and a `tools/call` interceptor, plus a symmetric `ClientExtension` with result claims and notification bindings. @@ -125,7 +125,7 @@ The Designed work is a FastMCP-native server extension API — a single registra ## Background tasks (SEP-2663) -**Status: Designed — approved for implementation.** +**Status: Shipped (#4603).** Background tasks return to the modern era as `fastmcp-tasks`, an in-repo optional package rebuilt on the `io.modelcontextprotocol/tasks` extension (SEP-2663, Final, merged upstream 2026-05-15). SEP-2663 supersedes SEP-1686 but keeps its polling core: a client that advertises the tasks capability issues an augmented `tools/call`; the server decides whether to run it as a task and returns a `CreateTaskResult` carrying a server-generated task id; the client polls `tasks/get` until terminal and reads the result inlined there. FastMCP's existing SEP-1686 wire layer is removed while the Docket/Redis execution engine underneath moves into `fastmcp-tasks` intact — the spec moved toward what FastMCP already built, so the rebuild is mostly deletion plus a thin wire adapter. `task=True` stays the authoring surface (gated by the `fastmcp[tasks]` extra and an explicit `mcp.add_extension(TasksExtension(...))`, the first consumer of the [extension API](#fastmcp-native-extension-api) above), so a server that already uses tasks needs no code change. Scope for v1 is polling-only and `tools/call`-only. diff --git a/docs/development/v4-notes/index.mdx b/docs/development/v4-notes/index.mdx index cd84bab34..b7bec4ead 100644 --- a/docs/development/v4-notes/index.mdx +++ b/docs/development/v4-notes/index.mdx @@ -5,7 +5,7 @@ title: v4.0 Development Notes This directory is the working map of FastMCP v4.0: the complete register of user-facing changes from the MCP Python SDK v2 migration ([PR #4437](https://github.com/PrefectHQ/fastmcp/pull/4437)), plus the forward v4 feature program. It plays three roles at once. 1. **A change register.** Every user-visible change from the migration, organized by subsystem, with a note on how FastMCP handles it (absorbed, bridged, breaking, or deprecated) and where to find it in the diff. This is the [Change Register](/development/v4-notes/change-register). -2. **A feature program.** The forward v4 work — sampling removal, multi-round-trip elicitation, the first-class 2026 client, a FastMCP-native extension API, the SEP-2663 background-tasks rebuild, and the SDK-delegation round-two convergence — now a mix of shipped, designed, and pending. Multi-round-trip guard tools (#4544) and the client's `mode="auto"` default with a partial SDK-composition (#4572/#4574, full composition blocked upstream) have shipped; sampling removal, the extension API, tasks, and SDK delegation remain ahead. Each carries an explicit status in the [Feature Program](/development/v4-notes/feature-program). The shipped side — what a v4 deployment provides on the modern protocol today, including the complete server-side SEP-990 identity assertion implementation — is cataloged in [2026-07-28 Protocol Support](/development/v4-notes/protocol-2026). +2. **A feature program.** The forward v4 work — sampling removal, multi-round-trip elicitation, the first-class 2026 client, a FastMCP-native extension API, the SEP-2663 background-tasks rebuild, and the SDK-delegation round-two convergence — now a mix of shipped, designed, and pending. Multi-round-trip guard tools (#4544), the client's `mode="auto"` default with a partial SDK-composition (#4572/#4574, full composition blocked upstream), the extension API (#4602), and background tasks on SEP-2663 (#4603) have shipped; sampling removal and SDK delegation remain ahead. Each carries an explicit status in the [Feature Program](/development/v4-notes/feature-program). The shipped side — what a v4 deployment provides on the modern protocol today, including the complete server-side SEP-990 identity assertion implementation — is cataloged in [2026-07-28 Protocol Support](/development/v4-notes/protocol-2026). 3. **A review lens.** Because the migration PR is too large to review line by line, the change register is organized so a reviewer can take one subsystem, read its claimed changes, and verify each against the diff. The [Known Gaps](/development/v4-notes/known-gaps) page collects the deliberate xfails and the upstream dependencies that gate the follow-up work. ## Why v4 exists diff --git a/docs/development/v4-notes/protocol-2026.mdx b/docs/development/v4-notes/protocol-2026.mdx index 7b2a34676..dd8abc299 100644 --- a/docs/development/v4-notes/protocol-2026.mdx +++ b/docs/development/v4-notes/protocol-2026.mdx @@ -46,11 +46,8 @@ The complete picture of what a FastMCP v4 server and client provide on the `2026 | **Composition** | `mount()`, providers, proxying, and tool transforms compose servers dynamically at runtime, with lifespans and middleware driven through the SDK session manager. | | **Pagination** | Declarative `FastMCP(list_page_size=...)` paginates all list operations in the high-level server; the client auto-paginates with cycle detection. | | **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_ENABLE_TELEMETRY=false` disables cleanly. | - -**Background tasks are not yet in the table because their modern-era support is being rebuilt.** The current `@mcp.tool(task=True)` runtime implements the 2025 task wire protocol (SEP-1686), which left the core MCP spec. Tasks did not disappear — they were reworked into the `io.modelcontextprotocol/tasks` extension (SEP-2663, Final, merged upstream 2026-05-15), a capability-negotiated feature layered on the extensions mechanism. So on `2026-07-28` the current SEP-1686 wire layer does not apply, and `task=True` completes only on handshake-era connections today. - -The plan is to rebuild task support on SEP-2663 as an in-repo optional package, `fastmcp-tasks`, gated by `task=True` exactly as `app=True` gates `prefab-ui`. The SEP-1686 wire layer is removed, but the Docket/Redis execution engine underneath it is extracted and re-adapted to the SEP-2663 wire shape — a polling protocol (augmented `tools/call` → `CreateTaskResult` → poll `tasks/get`, resolve in-task input via `tasks/update`) that the durable engine already fits. `task=True` stays the authoring surface, so a server that opts into tasks needs no code change when the wire underneath modernizes. This is a Designed feature — see [Background Tasks (SEP-2663)](/development/v4-notes/background-tasks) for the full design, and [Known Gaps](/development/v4-notes/known-gaps#the-xfail-register) for the SEP-1686-layer removal tracking. +| **Background tasks (SEP-2663)** | `fastmcp-tasks` implements the `io.modelcontextprotocol/tasks` extension end to end: `mcp.add_extension(TasksExtension())` plus `task=True` runs a tool as a background task, driven by the same Docket engine FastMCP 3 used. A client transparently completes a tasked call; gathering input mid-task uses the same guard pattern as foreground multi-round-trip tools, so a tool is written once and works either way. Modern-protocol only — the `task=True` runtime this replaced (SEP-1686) is gone entirely, not bridged. See [Background Tasks (SEP-2663)](/development/v4-notes/background-tasks) for the design and [servers/tasks](/servers/tasks) for usage. | ## Still in the program -Elicitation on the modern protocol is now shipped in its **guard form** — a tool returns an `InputRequiredResult` and re-runs per round to gather user input via multi-round trips (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). The declarative `Resolve(...)` layer over that primitive remains staged, tracked in the [Feature Program](/development/v4-notes/feature-program), along with the unified `subscriptions/listen` stream and the SEP-2663 tasks extension. The [Known Gaps](/development/v4-notes/known-gaps) page tracks the upstream dependencies that gate them. +Elicitation on the modern protocol is now shipped in its **guard form** — a tool returns an `InputRequiredResult` and re-runs per round to gather user input via multi-round trips (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). The declarative `Resolve(...)` layer over that primitive remains staged, tracked in the [Feature Program](/development/v4-notes/feature-program), along with the unified `subscriptions/listen` stream. The [Known Gaps](/development/v4-notes/known-gaps) page tracks the upstream dependencies that gate them. diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx index d8d6d3476..8550413bc 100644 --- a/docs/getting-started/upgrading/from-fastmcp-2.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx @@ -81,7 +81,7 @@ BREAKING CHANGES (will crash at import or runtime): 12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location. -13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]". +13. BACKGROUND TASKS: FastMCP's background task system is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]". DEPRECATIONS (still work but emit warnings): @@ -321,7 +321,7 @@ If you have code that treats the decorated result as a `FunctionTool` (e.g., acc **Background tasks require optional dependency** -FastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with: +FastMCP's background task system is now behind an optional extra. If your server uses background tasks, install with: ```bash pip install "fastmcp[tasks]" diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx index bd167c2fc..798ef83b9 100644 --- a/docs/servers/tasks.mdx +++ b/docs/servers/tasks.mdx @@ -1,58 +1,59 @@ --- title: Background Tasks sidebarTitle: Background Tasks -description: Run long-running operations asynchronously with progress tracking +description: Run long-running tools asynchronously with progress tracking icon: clock tag: "NEW" --- import { VersionBadge } from "/snippets/version-badge.mdx" - + -Background tasks require the `tasks` optional extra. See [installation instructions](#enabling-background-tasks) below. +Background tasks require the `fastmcp-tasks` package. See [enabling background tasks](#enabling-background-tasks) below. -FastMCP implements the MCP background task protocol ([SEP-1686](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)), giving your servers a production-ready distributed task scheduler with a single decorator change. +FastMCP implements the MCP background tasks extension ([`io.modelcontextprotocol/tasks`](https://modelcontextprotocol.io/extensions/tasks/overview), SEP-2663), giving your servers a production-ready distributed task scheduler with one extension registration and a decorator change. **What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service, where it processes millions of concurrent tasks every day. Docket is now open-sourced for the community. - ## What Are MCP Background Tasks? -In MCP, all component interactions are blocking by default. When a client calls a tool, reads a resource, or fetches a prompt, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience. +In MCP, a tool call is blocking by default. When a client calls a tool, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience. -The MCP background task protocol solves this by letting clients: -1. **Start** an operation and receive a task ID immediately -2. **Track** progress as the operation runs -3. **Retrieve** the result when ready +Background tasks solve this by letting a server tell a supporting client: +1. **Start** the tool and return a task ID immediately +2. **Poll** for status as the tool runs +3. **Retrieve** the result when ready — or answer a question the tool asks mid-run -FastMCP handles all of this for you. Add `task=True` to your decorator, and your function gains full background execution with progress reporting, distributed processing, and horizontal scaling. +FastMCP handles all of this for you. Add `task=True` to a tool decorator and register the tasks extension, and your function gains background execution with progress reporting, distributed processing, and horizontal scaling. ### MCP Background Tasks vs Python Concurrency You can always use Python's concurrency primitives (asyncio, threads, multiprocessing) or external task queues in your FastMCP servers. FastMCP is just Python—run code however you like. -MCP background tasks are different: they're **protocol-native**. This means MCP clients that support the task protocol can start operations, receive progress updates, and retrieve results through the standard MCP interface. The coordination happens at the protocol level, not inside your application code. +MCP background tasks are different: they're **protocol-native**. This means MCP clients that support the tasks extension can start a call, poll it, and retrieve its result through the standard MCP interface. The coordination happens at the protocol level, not inside your application code. ## Enabling Background Tasks - Background tasks require the `tasks` extra: +Background tasks require the `fastmcp-tasks` package: ```bash pip install "fastmcp[tasks]" ``` -Add `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution. +Register `TasksExtension` on your server, then add `task=True` to a tool decorator. `task=True` marks the tool as *capable* of background execution; the extension is what actually runs it — a `task=True` tool on a server with no tasks extension registered raises at server startup. -```python {6} +```python {5,8} import asyncio from fastmcp import FastMCP +from fastmcp_tasks import TasksExtension mcp = FastMCP("MyServer") +mcp.add_extension(TasksExtension()) @mcp.tool(task=True) async def slow_computation(duration: int) -> str: @@ -62,34 +63,38 @@ async def slow_computation(duration: int) -> str: return f"Completed in {duration} seconds" ``` -When a client requests background execution, the call returns immediately with a task ID. The work executes in a background worker, and the client can poll for status or wait for the result. +Whether a given call actually runs as a task depends on the client: it opts in per request, and the *server* decides based on the tool's execution mode (below). When it does run as a task, the call returns immediately with a task ID; the work executes in a background worker, and the client polls for the result. A [FastMCP client](/clients/tasks) does all of this transparently — `client.call_tool(...)` looks the same either way. + +Background tasks are a modern-protocol feature: the tasks capability is negotiated over `2026-07-28` connections, so a client pinned to `mode="legacy"` never triggers one — the tool always runs synchronously for it. -Background tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time. +Background tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time. Only tools can be task-enabled; resources, resource templates, and prompts do not carry `task=`. ## Execution Modes -For fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The MCP task protocol defines three execution modes: +For fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The tasks extension defines three execution modes: -| Mode | Client calls without task | Client calls with task | +| Mode | Client calls without the tasks capability | Client calls with the tasks capability | |------|--------------------------|------------------------| -| `"forbidden"` | Executes synchronously | Error: task not supported | -| `"optional"` | Executes synchronously | Executes as background task | -| `"required"` | Error: task required | Executes as background task | +| `"forbidden"` | Executes synchronously | Executes synchronously (never tasked) | +| `"optional"` | Executes synchronously | Executes as a background task | +| `"required"` | Error: task required | Executes as a background task | ```python from fastmcp import FastMCP from fastmcp.utilities.tasks import TaskConfig +from fastmcp_tasks import TasksExtension mcp = FastMCP("MyServer") +mcp.add_extension(TasksExtension()) # Supports both sync and background execution (default when task=True) @mcp.tool(task=TaskConfig(mode="optional")) async def flexible_task() -> str: return "Works either way" -# Requires background execution - errors if client doesn't request task +# Requires background execution - errors if the client didn't opt in @mcp.tool(task=TaskConfig(mode="required")) async def must_be_background() -> str: return "Only runs as a background task" @@ -104,18 +109,20 @@ The boolean shortcuts map to these modes: - `task=True` → `TaskConfig(mode="optional")` - `task=False` → `TaskConfig(mode="forbidden")` +When a `mode="required"` tool is called by a client that didn't opt in, FastMCP returns a "missing required capability" error rather than running it synchronously. + ### Poll Interval - - -When clients poll for task status, the server tells them how frequently to check back. By default, FastMCP suggests a 5-second interval, but you can customize this per component: +When a client polls for task status, the server can suggest how frequently to check back: ```python from datetime import timedelta from fastmcp import FastMCP from fastmcp.utilities.tasks import TaskConfig +from fastmcp_tasks import TasksExtension mcp = FastMCP("MyServer") +mcp.add_extension(TasksExtension()) # Poll every 2 seconds for a fast-completing task @mcp.tool(task=TaskConfig(mode="optional", poll_interval=timedelta(seconds=2))) @@ -128,31 +135,33 @@ async def slow_task() -> str: return "Eventually done" ``` -Shorter intervals give clients faster feedback but increase server load. Longer intervals reduce load but delay status updates. FastMCP clients honor the advertised interval exactly, so this is a real load control — but note that status notifications still wake a waiting client immediately, so the interval only governs how quickly a *missed* notification is noticed. +Shorter intervals give clients faster feedback but increase server load. The interval is a ceiling, not an exact cadence — the FastMCP client starts polling quickly and backs off toward it, so a fast task is still observed as done almost immediately. ### Server-Wide Default -To enable background task support for all components by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`. +To enable background task support for all tools by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`. ```python mcp = FastMCP("MyServer", tasks=True) ``` -If your server defines any synchronous tools, resources, or prompts, you will need to explicitly set `task=False` on their decorators to avoid an error. +If your server defines any synchronous tools, you will need to explicitly set `task=False` on their decorators to avoid an error. -### Graceful Degradation - -When a client requests background execution but the component has `mode="forbidden"`, FastMCP rejects the task-augmented request with a `METHOD_NOT_FOUND` error saying the component does not support task execution. The high-level FastMCP tool client can surface this as an immediate errored `ToolTask` when you call `client.call_tool(..., task=True, raise_on_error=False)`, but the server does not run the forbidden component synchronously for that task request. - -Conversely, when a component has `mode="required"` but the client doesn't request background execution, FastMCP returns an error indicating that task execution is required. - ### Configuration +`TasksExtension` takes the backend configuration directly, with `FASTMCP_DOCKET_*` environment variables as defaults — so `TasksExtension()` works out of the box against an env-configured deployment: + +```python +mcp.add_extension(TasksExtension(url="redis://localhost:6379/0", concurrency=20)) +``` + | Environment Variable | Default | Description | |---------------------|---------|-------------| | `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) | +| `FASTMCP_DOCKET_NAME` | `fastmcp` | Queue name. Servers and workers sharing a name and URL share a queue. | +| `FASTMCP_DOCKET_CONCURRENCY` | `10` | Maximum concurrent tasks per worker. | ## Backends @@ -173,7 +182,11 @@ The in-memory backend (`memory://`) requires zero configuration and works out of ### Redis Backend -For production deployments, use Redis (or Valkey) as your backend by setting `FASTMCP_DOCKET_URL=redis://localhost:6379`. +For production deployments, use Redis (or Valkey) as your backend: + +```python +mcp.add_extension(TasksExtension(url="redis://localhost:6379/0")) +``` **Advantages:** - **Persistent**: Tasks survive server restarts @@ -182,19 +195,19 @@ For production deployments, use Redis (or Valkey) as your backend by setting `FA ## Workers -Every FastMCP server with task-enabled components automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute. +Every FastMCP server with task-enabled tools automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute. -To scale horizontally, add more workers using the CLI: +To scale horizontally, add more workers: ```bash -fastmcp tasks worker server.py +python -m fastmcp_tasks.worker_cli worker server.py ``` Each additional worker pulls tasks from the same queue, distributing load across processes. Configure worker concurrency via environment: ```bash export FASTMCP_DOCKET_CONCURRENCY=20 -fastmcp tasks worker server.py +python -m fastmcp_tasks.worker_cli worker server.py ``` @@ -202,7 +215,47 @@ Additional workers only work with Redis/Valkey backends. The in-memory backend i -Task-enabled components must be defined at server startup to be registered with all workers. Components added dynamically after the server starts will not be available for background execution. +Task-enabled tools must be defined at server startup to be registered with all workers. Tools added dynamically after the server starts will not be available for background execution. + + +## Gathering Input Mid-Task + +A tool can ask the client a question partway through — the same [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol) used for multi-round-trip input on foreground calls: instead of awaiting a response, the tool *returns* one, and FastMCP re-runs it once the client answers. + +```python +from fastmcp import Context, FastMCP +from fastmcp_tasks import TasksExtension +import mcp_types + +mcp = FastMCP("MyServer") +mcp.add_extension(TasksExtension()) + +@mcp.tool(task=True) +async def plan_dinner(ctx: Context) -> str | mcp_types.InputRequiredResult: + responses = ctx.input_responses + if responses is None: + # First leg: ask a question and end here. + request = mcp_types.ElicitRequest( + params=mcp_types.ElicitRequestFormParams( + message="What are you in the mood for?", + requested_schema={"type": "object", "properties": {"cuisine": {"type": "string"}}}, + ) + ) + return mcp_types.InputRequiredResult( + result_type="input_required", + input_requests={"prefs": request}, + ) + + # Re-entered leg: the client's answer is on ctx.input_responses. + answer = responses["prefs"] + assert isinstance(answer, mcp_types.ElicitResult) + return f"Tonight: {answer.content['cuisine']}!" +``` + +Run as a task, this "ends" the tool's first leg entirely rather than blocking a worker on the client's answer: the task reports `input_required`, the client answers, and FastMCP re-invokes the tool with the answer attached. No worker ever sits idle waiting on a round-trip — the same tool works identically whether it's called synchronously or as a background task, and a [FastMCP client](/clients/tasks) answers the question automatically through its elicitation handler. + + +Imperative `await ctx.elicit(...)` is not supported inside a background task — it would require blocking a worker for the length of a client round-trip. Use the guard pattern (return `InputRequiredResult`) instead; calling `ctx.elicit()` from a task-enabled tool raises with guidance toward the guard pattern. ## Progress Reporting diff --git a/docs/servers/telemetry.mdx b/docs/servers/telemetry.mdx index 5e26a58f1..7e7356f5e 100644 --- a/docs/servers/telemetry.mdx +++ b/docs/servers/telemetry.mdx @@ -69,7 +69,7 @@ The server creates spans for each operation using [MCP semantic conventions](htt | `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) | | `resources/read` | Resource read (URI in `mcp.resource.uri` attribute, not span name) | | `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) | -| `tasks/{operation}` | Task management (`tasks/get`, `tasks/result`, `tasks/list`, or `tasks/cancel`) | +| `tasks/{operation}` | Task management (`tasks/get`, `tasks/update`, or `tasks/cancel`) | For mounted servers, an additional `delegate {name}` span shows the delegation to the child server. @@ -100,12 +100,12 @@ tools/call remote_search (CLIENT) Background task traces have two parts: -- Task submission and management requests use normal client-to-server context propagation. `tasks/get`, `tasks/result`, `tasks/list`, and `tasks/cancel` server spans are descendants of the corresponding FastMCP client spans. +- Task submission and management requests use normal client-to-server context propagation. `tasks/get`, `tasks/update`, and `tasks/cancel` server spans are descendants of the corresponding FastMCP client spans. - Deferred execution runs in a Docket worker. Docket records its `CONSUMER` span as a new trace root with a span link to the submission context, rather than making it a child of the submission span. Custom spans created inside the task are children of that worker span. Span links preserve the causal relationship without forcing worker sampling to inherit the submit trace's sampling decision. Some tracing backends do not display links prominently, so the worker trace may look disconnected even though the link is present. -Frequent status and list polling can produce more detail than you need. You can drop those client and server spans with a sampler that checks the span name before delegating to `ParentBased`: +Frequent status polling can produce more detail than you need. You can drop those client and server spans with a sampler that checks the span name before delegating to `ParentBased`: ```python from opentelemetry import trace @@ -124,7 +124,7 @@ class DropTaskPolls(Sampler): self._delegate = ParentBased(ALWAYS_ON) def should_sample(self, parent_context, trace_id, name, *args, **kwargs): - if name in {"tasks/get", "tasks/list"}: + if name in {"tasks/get"}: return SamplingResult(Decision.DROP) return self._delegate.should_sample( parent_context,