fastmcp/tests/server/tasks/test_sync_function_task_disabled.py
Jeremiah Lowin e47abb4bd7
[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.
2025-12-05 11:31:28 -05:00

206 lines
6.4 KiB
Python

"""
Tests that synchronous functions cannot be used as background tasks.
Docket requires async functions for background execution. FastMCP raises
ValueError when task=True is used with a sync function.
"""
import pytest
from fastmcp import FastMCP
async def test_sync_tool_with_explicit_task_true_raises():
"""Sync tool with task=True raises ValueError."""
mcp = FastMCP("test")
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
@mcp.tool(task=True)
def sync_tool(x: int) -> int:
"""A synchronous tool."""
return x * 2
async def test_sync_tool_with_inherited_task_true_raises():
"""Sync tool inheriting task=True from server raises ValueError."""
mcp = FastMCP("test", tasks=True)
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
@mcp.tool() # Inherits task=True from server
def sync_tool(x: int) -> int:
"""A synchronous tool."""
return x * 2
async def test_sync_prompt_with_explicit_task_true_raises():
"""Sync prompt with task=True raises ValueError."""
mcp = FastMCP("test")
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
@mcp.prompt(task=True)
def sync_prompt() -> str:
"""A synchronous prompt."""
return "Hello"
async def test_sync_prompt_with_inherited_task_true_raises():
"""Sync prompt inheriting task=True from server raises ValueError."""
mcp = FastMCP("test", tasks=True)
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
@mcp.prompt() # Inherits task=True from server
def sync_prompt() -> str:
"""A synchronous prompt."""
return "Hello"
async def test_sync_resource_with_explicit_task_true_raises():
"""Sync resource with task=True raises ValueError."""
mcp = FastMCP("test")
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
@mcp.resource("test://sync", task=True)
def sync_resource() -> str:
"""A synchronous resource."""
return "data"
async def test_sync_resource_with_inherited_task_true_raises():
"""Sync resource inheriting task=True from server raises ValueError."""
mcp = FastMCP("test", tasks=True)
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
@mcp.resource("test://sync") # Inherits task=True from server
def sync_resource() -> str:
"""A synchronous resource."""
return "data"
async def test_async_tool_with_task_true_remains_enabled():
"""Async tools with task=True keep task support enabled."""
mcp = FastMCP("test")
@mcp.tool(task=True)
async def async_tool(x: int) -> int:
"""An async tool."""
return x * 2
# Tool should have task=True
tool = await mcp.get_tool("async_tool")
assert tool.task is True
async def test_async_prompt_with_task_true_remains_enabled():
"""Async prompts with task=True keep task support enabled."""
mcp = FastMCP("test")
@mcp.prompt(task=True)
async def async_prompt() -> str:
"""An async prompt."""
return "Hello"
# Prompt should have task=True
prompt = await mcp.get_prompt("async_prompt")
assert prompt.task is True
async def test_async_resource_with_task_true_remains_enabled():
"""Async resources with task=True keep task support enabled."""
mcp = FastMCP("test")
@mcp.resource("test://async", task=True)
async def async_resource() -> str:
"""An async resource."""
return "data"
# Resource should have task=True
resource = await mcp._resource_manager.get_resource("test://async")
assert resource.task is True
async def test_sync_tool_with_task_false_works():
"""Sync tool with explicit task=False works (no error)."""
mcp = FastMCP("test", tasks=True)
@mcp.tool(task=False) # Explicitly disable
def sync_tool(x: int) -> int:
"""A synchronous tool."""
return x * 2
tool = await mcp.get_tool("sync_tool")
assert tool.task is False
async def test_sync_prompt_with_task_false_works():
"""Sync prompt with explicit task=False works (no error)."""
mcp = FastMCP("test", tasks=True)
@mcp.prompt(task=False) # Explicitly disable
def sync_prompt() -> str:
"""A synchronous prompt."""
return "Hello"
prompt = await mcp.get_prompt("sync_prompt")
assert prompt.task is False
async def test_sync_resource_with_task_false_works():
"""Sync resource with explicit task=False works (no error)."""
mcp = FastMCP("test", tasks=True)
@mcp.resource("test://sync", task=False) # Explicitly disable
def sync_resource() -> str:
"""A synchronous resource."""
return "data"
resource = await mcp._resource_manager.get_resource("test://sync")
assert resource.task is False
# =============================================================================
# Callable classes and staticmethods with async __call__
# =============================================================================
async def test_async_callable_class_tool_with_task_true_works():
"""Callable class with async __call__ and task=True should work."""
from fastmcp.tools import Tool
class AsyncCallableTool:
async def __call__(self, x: int) -> int:
return x * 2
# Callable classes use Tool.from_function() directly
tool = Tool.from_function(AsyncCallableTool(), task=True)
assert tool.task is True
async def test_async_callable_class_prompt_with_task_true_works():
"""Callable class with async __call__ and task=True should work."""
from fastmcp.prompts import Prompt
class AsyncCallablePrompt:
async def __call__(self) -> str:
return "Hello"
# Callable classes use Prompt.from_function() directly
prompt = Prompt.from_function(AsyncCallablePrompt(), task=True)
assert prompt.task is True
async def test_sync_callable_class_tool_with_task_true_raises():
"""Callable class with sync __call__ and task=True should raise."""
from fastmcp.tools import Tool
class SyncCallableTool:
def __call__(self, x: int) -> int:
return x * 2
with pytest.raises(ValueError, match="uses a sync function but has task=True"):
Tool.from_function(SyncCallableTool(), task=True)