PrefectHQ/fastmcp · error · ValueError
'{name}' uses a sync function but has task execution enabled
Error message
'{name}' uses a sync function but has task execution enabled. Background tasks require async functions. What it means
FastMCP raises this ValueError when a component (tool, resource, prompt) is configured with task execution enabled but the underlying callable is a synchronous function. Background task execution is implemented with asyncio, so only coroutine functions can be scheduled to run in the background. The check runs during component construction via from_function, failing fast rather than at task submission time.
Source
Thrown at fastmcp_slim/fastmcp/utilities/tasks.py:80
return self.mode != "forbidden"
def validate_function(self, fn: Callable[..., Any], name: str) -> None:
"""Validate that a function is compatible with this task config."""
if not self.supports_tasks():
return
fn_to_check = fn
if (
not inspect.isroutine(fn)
and not isinstance(fn, functools.partial)
and callable(fn)
):
fn_to_check = fn.__call__
if isinstance(fn_to_check, staticmethod):
fn_to_check = fn_to_check.__func__
if not is_coroutine_function(fn_to_check):
raise ValueError(
f"'{name}' uses a sync function but has task execution enabled. "
"Background tasks require async functions."
)
View on GitHub (pinned to 1f02114297)
Solutions
- Convert the function to an async def function (awaiting async I/O or wrapping blocking work with asyncio.to_thread)
- Keep the function sync but disable task execution for the component (remove the task=True / task execution config)
- If blocking work must stay sync but be non-blocking, create an async wrapper that calls asyncio.to_thread(sync_fn, ...) and register the wrapper with task execution enabled
- If using a callable object or staticmethod, ensure the __call__/__func__ actually is a coroutine function, since the check unwraps those
Example fix
// before
@mcp.tool(task=True)
def fetch_data(url: str) -> str:
return requests.get(url).text
// after
@mcp.tool(task=True)
async def fetch_data(url: str) -> str:
return await asyncio.to_thread(requests.get, url).text Defensive patterns
Strategy: validation
Validate before calling
import asyncio
if not asyncio.iscoroutinefunction(fn):
raise TypeError(f"{fn.__name__} must be async def when task execution is enabled") Type guard
def is_async_fn(fn) -> bool:
import asyncio
target = fn.__func__ if isinstance(fn, staticmethod) else getattr(fn, "__call__", fn)
return asyncio.iscoroutinefunction(target) Prevention
- Make background-task components async def by convention
- Wrap blocking I/O with asyncio.to_thread instead of registering sync functions
- Re-check task flags after refactoring functions between sync and async
When it happens
Trigger: Calling Tool.from_function / Resource.from_function / Prompt.from_function (or decorators like @mcp.tool) on a plain `def` function while passing task execution enabled (e.g. task=True or an execution/task config). The check inspects fn (or fn.__call__ / staticmethod __func__) with is_coroutine_function and raises when it returns False.
Common situations: Developers write a sync helper (doing blocking I/O or CPU work) and enable background task mode so it runs 'async', not realizing async scheduling requires a coroutine function. Also common after refactoring an async function to sync while leaving task config in place, or wrapping functions where the wrapper is sync.
Related errors
- Client is not connected. Use the 'async with client:' contex
- ClientGroup requires at least one client
- Protocol mode for server {name!r} must be a string
- ClientGroup is already connected
- MultiAuth requires at least a server or one verifier
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/95ec4376bbabb1a7.
Report an issue: GitHub.