""" Tests for server `tasks` parameter default inheritance. Verifies that the server's `tasks` parameter correctly sets defaults for all components (tools, prompts, resources), and that explicit component-level settings properly override the server default. """ import pytest from fastmcp import FastMCP from fastmcp.client import Client @pytest.mark.timeout(10) @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_server_tasks_true_defaults_all_components(): """Server with tasks=True makes all components default to supporting tasks.""" mcp = FastMCP("test", tasks=True) @mcp.tool() async def my_tool() -> str: return "tool result" @mcp.prompt() async def my_prompt() -> str: return "prompt result" @mcp.resource("test://resource") async def my_resource() -> str: return "resource result" async with Client(mcp) as client: # Verify all task-enabled components are registered with docket # Components use prefixed keys: tool:name, prompt:name, resource:uri docket = mcp.docket assert docket is not None assert "tool:my_tool@" in docket.tasks assert "prompt:my_prompt@" in docket.tasks assert "resource:test://resource@" in docket.tasks # Tool should support background execution tool_task = await client.call_tool("my_tool", task=True) assert not tool_task.returned_immediately # Prompt should support background execution prompt_task = await client.get_prompt("my_prompt", task=True) assert not prompt_task.returned_immediately # Resource should support background execution resource_task = await client.read_resource("test://resource", task=True) assert not resource_task.returned_immediately @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_server_tasks_false_defaults_all_components(): """Server with tasks=False makes all components default to mode=forbidden.""" import pytest from mcp.shared.exceptions import MCPError mcp = FastMCP("test", tasks=False) @mcp.tool() async def my_tool() -> str: return "tool result" @mcp.prompt() async def my_prompt() -> str: return "prompt result" @mcp.resource("test://resource") async def my_resource() -> str: return "resource result" async with Client(mcp) as client: # Tool with mode="forbidden" returns error when called with task=True tool_task = await client.call_tool("my_tool", task=True, raise_on_error=False) assert tool_task.returned_immediately result = await tool_task.result() assert result.is_error assert "does not support task-augmented execution" in str(result) # Prompt with mode="forbidden" raises MCPError when called with task=True with pytest.raises(MCPError): await client.get_prompt("my_prompt", task=True) # Resource with mode="forbidden" raises MCPError when called with task=True with pytest.raises(MCPError): await client.read_resource("test://resource", task=True) async def test_server_tasks_none_defaults_to_false(): """Server with tasks=None (or omitted) defaults to False.""" mcp = FastMCP("test") # tasks=None, defaults to False @mcp.tool() async def my_tool() -> str: return "tool result" async with Client(mcp) as client: # Tool should NOT support background execution (mode="forbidden" from default) tool_task = await client.call_tool("my_tool", task=True, raise_on_error=False) assert tool_task.returned_immediately result = await tool_task.result() assert result.is_error assert "does not support task-augmented execution" in str(result) async def test_component_explicit_false_overrides_server_true(): """Component with task=False overrides server default of tasks=True.""" mcp = FastMCP("test", tasks=True) @mcp.tool(task=False) async def no_task_tool() -> str: return "immediate result" @mcp.tool() async def default_tool() -> str: return "background result" async with Client(mcp) as client: # Verify docket registration matches task settings (prefixed keys) docket = mcp.docket assert docket is not None assert ( "tool:no_task_tool@" not in docket.tasks ) # task=False means not registered assert "tool:default_tool@" in docket.tasks # Inherits tasks=True # Explicit False (mode="forbidden") returns error when called with task=True no_task = await client.call_tool( "no_task_tool", task=True, raise_on_error=False ) assert no_task.returned_immediately result = await no_task.result() assert result.is_error assert "does not support task-augmented execution" in str(result) # Default should support background execution default_task = await client.call_tool("default_tool", task=True) assert not default_task.returned_immediately async def test_component_explicit_true_overrides_server_false(): """Component with task=True overrides server default of tasks=False.""" mcp = FastMCP("test", tasks=False) @mcp.tool(task=True) async def task_tool() -> str: return "background result" @mcp.tool() async def default_tool() -> str: return "immediate result" async with Client(mcp) as client: # Verify docket registration matches task settings (prefixed keys) docket = mcp.docket assert docket is not None assert "tool:task_tool@" in docket.tasks # task=True means registered assert "tool:default_tool@" not in docket.tasks # Inherits tasks=False # Explicit True should support background execution despite server default task = await client.call_tool("task_tool", task=True) assert not task.returned_immediately # Default (mode="forbidden") returns error when called with task=True default = await client.call_tool( "default_tool", task=True, raise_on_error=False ) assert default.returned_immediately result = await default.result() assert result.is_error @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_mixed_explicit_and_inherited(): """Mix of explicit True/False/None on different components.""" import pytest from mcp.shared.exceptions import MCPError mcp = FastMCP("test", tasks=True) # Server default is True @mcp.tool() async def inherited_tool() -> str: return "inherits True" @mcp.tool(task=True) async def explicit_true_tool() -> str: return "explicit True" @mcp.tool(task=False) async def explicit_false_tool() -> str: return "explicit False" @mcp.prompt() async def inherited_prompt() -> str: return "inherits True" @mcp.prompt(task=False) async def explicit_false_prompt() -> str: return "explicit False" @mcp.resource("test://inherited") async def inherited_resource() -> str: return "inherits True" @mcp.resource("test://explicit_false", task=False) async def explicit_false_resource() -> str: return "explicit False" async with Client(mcp) as client: # Verify docket registration matches task settings # Components use prefixed keys: tool:name, prompt:name, resource:uri docket = mcp.docket assert docket is not None # task=True (explicit or inherited) means registered (with prefixed keys) assert "tool:inherited_tool@" in docket.tasks assert "tool:explicit_true_tool@" in docket.tasks assert "prompt:inherited_prompt@" in docket.tasks assert "resource:test://inherited@" in docket.tasks # task=False means NOT registered assert "tool:explicit_false_tool@" not in docket.tasks assert "prompt:explicit_false_prompt@" not in docket.tasks assert "resource:test://explicit_false@" not in docket.tasks # Tools inherited = await client.call_tool("inherited_tool", task=True) assert not inherited.returned_immediately explicit_true = await client.call_tool("explicit_true_tool", task=True) assert not explicit_true.returned_immediately # Explicit False (mode="forbidden") returns error explicit_false = await client.call_tool( "explicit_false_tool", task=True, raise_on_error=False ) assert explicit_false.returned_immediately result = await explicit_false.result() assert result.is_error # Prompts inherited_prompt_task = await client.get_prompt("inherited_prompt", task=True) assert not inherited_prompt_task.returned_immediately # Explicit False prompt (mode="forbidden") raises MCPError with pytest.raises(MCPError): await client.get_prompt("explicit_false_prompt", task=True) # Resources inherited_resource_task = await client.read_resource( "test://inherited", task=True ) assert not inherited_resource_task.returned_immediately # Explicit False resource (mode="forbidden") raises MCPError with pytest.raises(MCPError): await client.read_resource("test://explicit_false", task=True) async def test_server_tasks_parameter_sets_component_defaults(): """Server tasks parameter sets component defaults.""" # Server tasks=True sets component defaults mcp = FastMCP("test", tasks=True) @mcp.tool() async def tool_inherits_true() -> str: return "tool result" async with Client(mcp) as client: # Tool inherits tasks=True from server tool_task = await client.call_tool("tool_inherits_true", task=True) assert not tool_task.returned_immediately # Server tasks=False sets component defaults mcp2 = FastMCP("test2", tasks=False) @mcp2.tool() async def tool_inherits_false() -> str: return "tool result" async with Client(mcp2) as client: # Tool inherits tasks=False (mode="forbidden") - returns error tool_task = await client.call_tool( "tool_inherits_false", task=True, raise_on_error=False ) assert tool_task.returned_immediately result = await tool_task.result() assert result.is_error @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_resource_template_inherits_server_tasks_default(): """Resource templates inherit server tasks default.""" mcp = FastMCP("test", tasks=True) @mcp.resource("test://{item_id}") async def templated_resource(item_id: str) -> str: return f"resource {item_id}" async with Client(mcp) as client: # Template should support background execution resource_task = await client.read_resource("test://123", task=True) assert not resource_task.returned_immediately @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_multiple_components_same_name_different_tasks(): """Different component types with same name can have different task settings.""" import pytest from mcp.shared.exceptions import MCPError mcp = FastMCP("test", tasks=False) @mcp.tool(task=True) async def shared_name() -> str: return "tool result" @mcp.prompt() async def shared_name_prompt() -> str: return "prompt result" async with Client(mcp) as client: # Tool with explicit True should support background execution tool_task = await client.call_tool("shared_name", task=True) assert not tool_task.returned_immediately # Prompt inheriting False (mode="forbidden") raises MCPError with pytest.raises(MCPError): await client.get_prompt("shared_name_prompt", task=True) async def test_task_with_custom_tool_name(): """Tools with custom names work correctly as tasks (issue #2642). When a tool is registered with a custom name different from the function name, task execution should use the custom name for Docket lookup. """ mcp = FastMCP("test", tasks=True) async def my_function() -> str: return "result from custom-named tool" mcp.tool(my_function, name="custom-tool-name") async with Client(mcp) as client: # Verify the tool is registered with its custom name in Docket (prefixed key) docket = mcp.docket assert docket is not None assert "tool:custom-tool-name@" in docket.tasks # Call the tool as a task using its custom name task = await client.call_tool("custom-tool-name", task=True) assert not task.returned_immediately result = await task assert result.data == "result from custom-named tool" @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_task_with_custom_resource_name(): """Resources with custom names work correctly as tasks. Resources are registered/looked up by their .key (URI), not their name. """ mcp = FastMCP("test", tasks=True) @mcp.resource("test://resource", name="custom-resource-name") async def my_resource_func() -> str: return "result from custom-named resource" async with Client(mcp) as client: # Verify the resource is registered with its key (prefixed URI) in Docket docket = mcp.docket assert docket is not None assert "resource:test://resource@" in docket.tasks # Call the resource as a task task = await client.read_resource("test://resource", task=True) assert not task.returned_immediately result = await task.result() assert result[0].text == "result from custom-named resource" @pytest.mark.xfail( reason="SDK v2 has no `task` field on GetPromptRequestParams / " "ReadResourceRequestParams; prompt/resource task submission is not " "wire-expressible and always graceful-degrades (sdk-feedback #3).", strict=True, ) async def test_task_with_custom_template_name(): """Resource templates with custom names work correctly as tasks. Templates are registered/looked up by their .key (uri_template), not their name. """ mcp = FastMCP("test", tasks=True) @mcp.resource("test://{item_id}", name="custom-template-name") async def my_template_func(item_id: str) -> str: return f"result for {item_id}" async with Client(mcp) as client: # Verify the template is registered with its key (prefixed uri_template) in Docket docket = mcp.docket assert docket is not None assert "template:test://{item_id}@" in docket.tasks # Call the template as a task task = await client.read_resource("test://123", task=True) assert not task.returned_immediately result = await task.result() assert result[0].text == "result for 123"