--- 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" 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. **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](/clients/client#protocol-negotiation). ## 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](/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 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](/servers/tasks) for how to enable background task support on the server side.