Skip to content

Commit e47abb4

Browse files
authored
[SEP-1686] Raise ValueError when sync functions have task=True (#2554)
* Raise ValueError when sync functions have task=True Move validation from server.py decorators to the from_function() class methods on FunctionTool, FunctionPrompt, FunctionResource, and FunctionResourceTemplate. This ensures the check runs regardless of how the objects are created. * Fix async check for callable classes and staticmethods Move the task=True async validation to run AFTER callable classes and staticmethods are unwrapped, preventing false positives for async callable classes with sync-looking signatures.
1 parent 66aaf42 commit e47abb4

File tree

8 files changed

+155
-206
lines changed

8 files changed

+155
-206
lines changed

src/fastmcp/prompts/prompt.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ def from_function(
208208
if isinstance(fn, staticmethod):
209209
fn = fn.__func__
210210

211+
# Validate that task=True requires async functions (after unwrapping)
212+
if task and not inspect.iscoroutinefunction(fn):
213+
raise ValueError(
214+
f"Prompt '{func_name}' uses a sync function but has task=True. "
215+
"Background tasks require async functions. Set task=False to disable."
216+
)
217+
211218
# Wrap fn to handle dependency resolution internally
212219
wrapped_fn = without_injected_parameters(fn)
213220
type_adapter = get_cached_typeadapter(wrapped_fn)

src/fastmcp/resources/resource.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ def from_function(
197197
if isinstance(uri, str):
198198
uri = AnyUrl(uri)
199199

200+
# Validate that task=True requires async functions
201+
# Handle callable classes and staticmethods before checking
202+
fn_to_check = fn
203+
if not inspect.isroutine(fn) and callable(fn):
204+
fn_to_check = fn.__call__
205+
if isinstance(fn_to_check, staticmethod):
206+
fn_to_check = fn_to_check.__func__
207+
if task and not inspect.iscoroutinefunction(fn_to_check):
208+
raise ValueError(
209+
f"Resource '{name or get_fn_name(fn)}' uses a sync function but has task=True. "
210+
"Background tasks require async functions. Set task=False to disable."
211+
)
212+
200213
# Wrap fn to handle dependency resolution internally
201214
wrapped_fn = without_injected_parameters(fn)
202215

src/fastmcp/resources/template.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,13 @@ def from_function(
369369
if isinstance(fn, staticmethod):
370370
fn = fn.__func__
371371

372+
# Validate that task=True requires async functions (after unwrapping)
373+
if task and not inspect.iscoroutinefunction(fn):
374+
raise ValueError(
375+
f"Resource template '{func_name}' uses a sync function but has task=True. "
376+
"Background tasks require async functions. Set task=False to disable."
377+
)
378+
372379
wrapper_fn = without_injected_parameters(fn)
373380
type_adapter = get_cached_typeadapter(wrapper_fn)
374381
parameters = type_adapter.json_schema()

src/fastmcp/server/server.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import asyncio
65
import inspect
76
import re
87
import secrets
@@ -1787,16 +1786,6 @@ def my_tool(x: int) -> str:
17871786
task if task is not None else self._support_tasks_by_default
17881787
)
17891788

1790-
# Disable task support for sync functions (Docket requires async)
1791-
if supports_task and not asyncio.iscoroutinefunction(fn):
1792-
if task is True:
1793-
# User explicitly requested task=True for sync function
1794-
logger.warning(
1795-
f"Tool '{tool_name or fn.__name__}' has task=True but is synchronous. "
1796-
"Background task support requires async functions. Disabling task support."
1797-
)
1798-
supports_task = False
1799-
18001789
# Register the tool immediately and return the tool object
18011790
# Note: Deprecation warning for exclude_args is handled in Tool.from_function
18021791
tool = Tool.from_function(
@@ -1988,16 +1977,6 @@ def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
19881977
task if task is not None else self._support_tasks_by_default
19891978
)
19901979

1991-
# Disable task support for sync functions (Docket requires async)
1992-
if supports_task and not asyncio.iscoroutinefunction(fn):
1993-
if task is True:
1994-
# User explicitly requested task=True for sync function
1995-
logger.warning(
1996-
f"Resource '{uri}' has task=True but is synchronous. "
1997-
"Background task support requires async functions. Disabling task support."
1998-
)
1999-
supports_task = False
2000-
20011980
# Check if this should be a template
20021981
has_uri_params = "{" in uri and "}" in uri
20031982
# Use wrapper to check for user-facing parameters
@@ -2207,16 +2186,6 @@ def another_prompt(data: str) -> list[Message]:
22072186
task if task is not None else self._support_tasks_by_default
22082187
)
22092188

2210-
# Disable task support for sync functions (Docket requires async)
2211-
if supports_task and not asyncio.iscoroutinefunction(fn):
2212-
if task is True:
2213-
# User explicitly requested task=True for sync function
2214-
logger.warning(
2215-
f"Prompt '{prompt_name or fn.__name__}' has task=True but is synchronous. "
2216-
"Background task support requires async functions. Disabling task support."
2217-
)
2218-
supports_task = False
2219-
22202189
# Register the prompt immediately
22212190
prompt = Prompt.from_function(
22222191
fn=fn,

src/fastmcp/tools/tool.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,20 @@ def from_function(
314314
stacklevel=2,
315315
)
316316

317+
# Validate that task=True requires async functions
318+
# Handle callable classes and staticmethods before checking
319+
fn_to_check = fn
320+
if not inspect.isroutine(fn) and callable(fn):
321+
fn_to_check = fn.__call__
322+
if isinstance(fn_to_check, staticmethod):
323+
fn_to_check = fn_to_check.__func__
324+
if task and not inspect.iscoroutinefunction(fn_to_check):
325+
fn_name = name or getattr(fn, "__name__", repr(fn))
326+
raise ValueError(
327+
f"Tool '{fn_name}' uses a sync function but has task=True. "
328+
"Background tasks require async functions. Set task=False to disable."
329+
)
330+
317331
parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
318332

319333
if name is None and parsed_fn.name == "<lambda>":

0 commit comments

Comments
 (0)