chore: import upstream snapshot with attribution
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
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
|
||||
from fastmcp.prompts.function_prompt import FunctionPrompt
|
||||
from fastmcp.resources.function_resource import FunctionResource
|
||||
from fastmcp.tools.function_tool import FunctionTool
|
||||
|
||||
|
||||
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 execution enabled"
|
||||
):
|
||||
|
||||
@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 execution enabled"
|
||||
):
|
||||
|
||||
@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 execution enabled"
|
||||
):
|
||||
|
||||
@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 execution enabled"
|
||||
):
|
||||
|
||||
@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 execution enabled"
|
||||
):
|
||||
|
||||
@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 execution enabled"
|
||||
):
|
||||
|
||||
@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 mode="optional" and be a FunctionTool
|
||||
tool = await mcp.get_tool("async_tool")
|
||||
assert isinstance(tool, FunctionTool)
|
||||
assert tool.task_config.mode == "optional"
|
||||
|
||||
|
||||
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 mode="optional" and be a FunctionPrompt
|
||||
prompt = await mcp.get_prompt("async_prompt")
|
||||
assert isinstance(prompt, FunctionPrompt)
|
||||
assert prompt.task_config.mode == "optional"
|
||||
|
||||
|
||||
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 mode="optional" and be a FunctionResource
|
||||
resource = await mcp.get_resource("test://async")
|
||||
assert isinstance(resource, FunctionResource)
|
||||
assert resource.task_config.mode == "optional"
|
||||
|
||||
|
||||
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 isinstance(tool, FunctionTool)
|
||||
assert tool.task_config.mode == "forbidden"
|
||||
|
||||
|
||||
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 isinstance(prompt, FunctionPrompt)
|
||||
assert prompt.task_config.mode == "forbidden"
|
||||
|
||||
|
||||
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.get_resource("test://sync")
|
||||
assert isinstance(resource, FunctionResource)
|
||||
assert resource.task_config.mode == "forbidden"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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_config.mode == "optional"
|
||||
|
||||
|
||||
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_config.mode == "optional"
|
||||
|
||||
|
||||
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 execution enabled"
|
||||
):
|
||||
Tool.from_function(SyncCallableTool(), task=True)
|
||||
Reference in New Issue
Block a user