mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
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).
135 lines
5.2 KiB
Text
135 lines
5.2 KiB
Text
---
|
|
title: Background Tasks
|
|
sidebarTitle: Tasks
|
|
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"
|
|
|
|
<VersionBadge version="4.0.0" />
|
|
|
|
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.
|
|
|
|
<Note>
|
|
**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).
|
|
</Note>
|
|
|
|
## Transparent Calls
|
|
|
|
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="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...
|
|
|
|
result = await task.result()
|
|
```
|
|
|
|
`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
|
|
|
|
```python
|
|
status = await task.status()
|
|
print(f"{status.status}: {status.status_message}")
|
|
# status.status is "working", "input_required", "completed", "failed", or "cancelled"
|
|
```
|
|
|
|
### Waiting with Control
|
|
|
|
`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="input_required", timeout=30.0)
|
|
```
|
|
|
|
### Getting the Result
|
|
|
|
`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()
|
|
```
|
|
|
|
Cancellation is cooperative — the task may still finish before the server notices the request.
|
|
|
|
## Answering Questions Mid-Task
|
|
|
|
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
|
|
|
|
async def handle_elicitation(message, response_type, params, context):
|
|
return {"cuisine": "Thai", "vegetarian": True}
|
|
|
|
async with Client(server, mode="auto", elicitation_handler=handle_elicitation) as client:
|
|
result = await client.call_tool("plan_dinner", {})
|
|
print(result.data)
|
|
```
|
|
|
|
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="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}")
|
|
|
|
# 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)
|
|
|
|
result = await task.result()
|
|
print(f"Result: {result.data}")
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
See [Server Background Tasks](/servers/tasks) for how to enable background task support on the server side.
|