Back to Fastmcp

Background Tasks

docs/clients/tasks.mdx

4.0.0a25.5 KB
Original Source

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> **Client task support is opt-in.** Install the `fastmcp-tasks` package (`pip install "fastmcp[tasks]"`) and import it — importing `fastmcp_tasks` anywhere (which you do to use `call_tool_task`) enables task support for every `Client` in the process. Without it, a `Client` never advertises the tasks capability, so the server runs its calls synchronously and background tasks simply don't happen.

Tasks also require the modern protocol. The capability is negotiated over 2026-07-28 connections. mode="auto" (the client default) negotiates it automatically; mode="legacy" never does. See protocol negotiation. </Note>

Transparent Calls

With task support enabled, 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
import fastmcp_tasks  # enables client task support
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 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 for how a tool asks a question in the first place.

Example

Putting it together, here is a client that submits a background task with call_tool_task and awaits its result:

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 for how to enable background task support on the server side.