diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx index fcf1f75fb..7e73a4860 100644 --- a/docs/servers/tasks.mdx +++ b/docs/servers/tasks.mdx @@ -16,9 +16,6 @@ FastMCP implements the MCP background task protocol ([SEP-1686](https://modelcon **What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service. Docket is the beating heart of Prefect's enterprise task infrastructure, processing millions of tasks daily across their multi-tenant SaaS platform. It's now open-sourced for the community. - -Background tasks are disabled by default in v2.14.0. Enable them with `FASTMCP_ENABLE_TASKS=true` or by passing `tasks=True` to the FastMCP constructor. This default will change in a future release. - ## What Are MCP Background Tasks? @@ -116,11 +113,8 @@ Conversely, when a component has `mode="required"` but the client doesn't reques ### Configuration -Background tasks require explicit opt-in via environment variable: - | Environment Variable | Default | Description | |---------------------|---------|-------------| -| `FASTMCP_ENABLE_TASKS` | `false` | Enable the MCP task protocol | | `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) | ## Backends diff --git a/examples/tasks/.envrc b/examples/tasks/.envrc index c9afbca5b..87a7dfef9 100644 --- a/examples/tasks/.envrc +++ b/examples/tasks/.envrc @@ -2,9 +2,6 @@ # This file is loaded by direnv (https://direnv.net/) when you cd into this directory # Run `direnv allow` to enable automatic environment loading -# Enable MCP SEP-1686 task protocol support -export FASTMCP_ENABLE_TASKS=true - # Configure Docket backend URL # Use Redis backend (requires docker-compose up) export FASTMCP_DOCKET_URL=redis://localhost:24242/0 diff --git a/examples/tasks/README.md b/examples/tasks/README.md index f6275009b..8013968d5 100644 --- a/examples/tasks/README.md +++ b/examples/tasks/README.md @@ -51,7 +51,6 @@ fastmcp tasks worker server.py | Variable | Default | Description | |----------|---------|-------------| -| `FASTMCP_ENABLE_TASKS` | `false` | Enable MCP task protocol (SEP-1686) | | `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL | ## Learn More diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index f4804f478..4651edadb 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -458,14 +458,9 @@ class Client(Generic[ClientTransportT]): try: with anyio.fail_after(timeout): - if fastmcp.settings.enable_tasks: - self._session_state.initialize_result = ( - await _task_capable_initialize(self.session) - ) - else: - self._session_state.initialize_result = ( - await self.session.initialize() - ) + self._session_state.initialize_result = await _task_capable_initialize( + self.session + ) return self._session_state.initialize_result except TimeoutError as e: diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index c9962144e..439507179 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -36,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url from fastmcp.server.dependencies import get_http_headers from fastmcp.server.server import FastMCP +from fastmcp.server.tasks.capabilities import get_task_capabilities from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment @@ -856,16 +857,7 @@ class FastMCPTransport(ClientTransport): _enter_server_lifespan(server=self.server), ): # Build experimental capabilities - import fastmcp - - experimental_capabilities = {} - if fastmcp.settings.enable_tasks: - # Declare SEP-1686 task support - experimental_capabilities["tasks"] = { - "tools": True, - "prompts": True, - "resources": True, - } + experimental_capabilities = get_task_capabilities() tg.start_soon( lambda: self.server._mcp_server.run( diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index d0da9cefa..825fcd0aa 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -19,9 +19,9 @@ from starlette.responses import Response from starlette.routing import BaseRoute, Mount, Route from starlette.types import Lifespan, Receive, Scope, Send -import fastmcp from fastmcp.server.auth import AuthProvider from fastmcp.server.auth.middleware import RequireAuthMiddleware +from fastmcp.server.tasks.capabilities import get_task_capabilities from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: @@ -161,19 +161,7 @@ def create_sse_app( async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response: async with sse.connect_sse(scope, receive, send) as streams: # Build experimental capabilities - experimental_capabilities = {} - if fastmcp.settings.enable_tasks: - # Declare SEP-1686 task support per final spec (lines 49-63) - # Nested structure: {list: {}, cancel: {}, requests: {tools: {call: {}}}} - experimental_capabilities["tasks"] = { - "list": {}, - "cancel": {}, - "requests": { - "tools": {"call": {}}, - "prompts": {"get": {}}, - "resources": {"read": {}}, - }, - } + experimental_capabilities = get_task_capabilities() await server._mcp_server.run( streams[0], diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 34f706975..50a29ba7c 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -75,6 +75,7 @@ from fastmcp.server.http import ( ) from fastmcp.server.low_level import LowLevelServer from fastmcp.server.middleware import Middleware, MiddlewareContext +from fastmcp.server.tasks.capabilities import get_task_capabilities from fastmcp.server.tasks.config import TaskConfig from fastmcp.server.tasks.handlers import ( handle_prompt_as_task, @@ -211,9 +212,7 @@ class FastMCP(Generic[LifespanResultT]): sampling_handler_behavior: Literal["always", "fallback"] | None = None, ): # Resolve server default for background task support - self._support_tasks_by_default: bool = ( - tasks if tasks is not None else fastmcp.settings.enable_tasks - ) + self._support_tasks_by_default: bool = tasks if tasks is not None else False # Docket instance (set during lifespan for cross-task access) self._docket = None @@ -690,43 +689,46 @@ class FastMCP(Generic[LifespanResultT]): pass # Check for task metadata and route appropriately - if fastmcp.settings.enable_tasks: - async with fastmcp.server.context.Context(fastmcp=self): - # Get resource including from mounted servers - resource = await self._get_resource_with_task_config(str(uri)) - if resource and hasattr(resource, "task_config"): - task_mode = resource.task_config.mode # type: ignore[union-attr] + async with fastmcp.server.context.Context(fastmcp=self): + # Get resource including from mounted servers + resource = await self._get_resource_with_task_config(str(uri)) + if ( + resource + and self._should_enable_component(resource) + and hasattr(resource, "task_config") + ): + task_mode = resource.task_config.mode # type: ignore[union-attr] - # Enforce mode="required" - must have task metadata - if task_mode == "required" and not task_meta: - raise McpError( - ErrorData( - code=METHOD_NOT_FOUND, - message=f"Resource '{uri}' requires task-augmented execution", - ) + # Enforce mode="required" - must have task metadata + if task_mode == "required" and not task_meta: + raise McpError( + ErrorData( + code=METHOD_NOT_FOUND, + message=f"Resource '{uri}' requires task-augmented execution", + ) + ) + + # Route to background if task metadata present and mode allows + if task_meta and task_mode != "forbidden": + # For FunctionResource/FunctionResourceTemplate, use Docket + if isinstance( + resource, + FunctionResource | FunctionResourceTemplate, + ): + task_meta_dict = task_meta.model_dump(exclude_none=True) + return await handle_resource_as_task( + self, str(uri), resource, task_meta_dict ) - # Route to background if task metadata present and mode allows - if task_meta and task_mode != "forbidden": - # For FunctionResource/FunctionResourceTemplate, use Docket - if isinstance( - resource, - FunctionResource | FunctionResourceTemplate, - ): - task_meta_dict = task_meta.model_dump(exclude_none=True) - return await handle_resource_as_task( - self, str(uri), resource, task_meta_dict - ) - - # Forbidden mode: task requested but mode="forbidden" - # Raise error since resources don't have isError field - if task_meta and task_mode == "forbidden": - raise McpError( - ErrorData( - code=METHOD_NOT_FOUND, - message=f"Resource '{uri}' does not support task-augmented execution", - ) + # Forbidden mode: task requested but mode="forbidden" + # Raise error since resources don't have isError field + if task_meta and task_mode == "forbidden": + raise McpError( + ErrorData( + code=METHOD_NOT_FOUND, + message=f"Resource '{uri}' does not support task-augmented execution", ) + ) # Synchronous execution result = await self._read_resource_mcp(uri) @@ -818,39 +820,43 @@ class FastMCP(Generic[LifespanResultT]): pass # Check for task metadata and route appropriately - if fastmcp.settings.enable_tasks: - async with fastmcp.server.context.Context(fastmcp=self): - prompts = await self.get_prompts() - prompt = prompts.get(name) - if prompt and hasattr(prompt, "task_config") and prompt.task_config: - task_mode = prompt.task_config.mode # type: ignore[union-attr] + async with fastmcp.server.context.Context(fastmcp=self): + prompts = await self.get_prompts() + prompt = prompts.get(name) + if ( + prompt + and self._should_enable_component(prompt) + and hasattr(prompt, "task_config") + and prompt.task_config + ): + task_mode = prompt.task_config.mode # type: ignore[union-attr] - # Enforce mode="required" - must have task metadata - if task_mode == "required" and not task_meta: - raise McpError( - ErrorData( - code=METHOD_NOT_FOUND, - message=f"Prompt '{name}' requires task-augmented execution", - ) + # Enforce mode="required" - must have task metadata + if task_mode == "required" and not task_meta: + raise McpError( + ErrorData( + code=METHOD_NOT_FOUND, + message=f"Prompt '{name}' requires task-augmented execution", ) + ) - # Route to background if task metadata present and mode allows - if task_meta and task_mode != "forbidden": - task_meta_dict = task_meta.model_dump(exclude_none=True) - result = await handle_prompt_as_task( - self, name, arguments, task_meta_dict - ) - return mcp.types.ServerResult(result) + # Route to background if task metadata present and mode allows + if task_meta and task_mode != "forbidden": + task_meta_dict = task_meta.model_dump(exclude_none=True) + result = await handle_prompt_as_task( + self, name, arguments, task_meta_dict + ) + return mcp.types.ServerResult(result) - # Forbidden mode: task requested but mode="forbidden" - # Raise error since prompts don't have isError field - if task_meta and task_mode == "forbidden": - raise McpError( - ErrorData( - code=METHOD_NOT_FOUND, - message=f"Prompt '{name}' does not support task-augmented execution", - ) + # Forbidden mode: task requested but mode="forbidden" + # Raise error since prompts don't have isError field + if task_meta and task_mode == "forbidden": + raise McpError( + ErrorData( + code=METHOD_NOT_FOUND, + message=f"Prompt '{name}' does not support task-augmented execution", ) + ) # Synchronous execution result = await self._get_prompt_mcp(name, arguments) @@ -860,9 +866,6 @@ class FastMCP(Generic[LifespanResultT]): def _setup_task_protocol_handlers(self) -> None: """Register SEP-1686 task protocol handlers with SDK.""" - if not fastmcp.settings.enable_tasks: - return - from mcp.types import ( CancelTaskRequest, GetTaskPayloadRequest, @@ -1548,49 +1551,52 @@ class FastMCP(Generic[LifespanResultT]): # No request context available - proceed without task metadata pass - if fastmcp.settings.enable_tasks: - # Get tool from local manager, mounted servers, or proxy - tool = await self._get_tool_with_task_config(key) - if tool and hasattr(tool, "task_config"): - task_mode = tool.task_config.mode # type: ignore[union-attr] + # Get tool from local manager, mounted servers, or proxy + tool = await self._get_tool_with_task_config(key) + if ( + tool + and self._should_enable_component(tool) + and hasattr(tool, "task_config") + ): + task_mode = tool.task_config.mode # type: ignore[union-attr] - # Enforce mode="required" - must have task metadata - if task_mode == "required" and not task_meta: - raise McpError( - ErrorData( - code=METHOD_NOT_FOUND, - message=f"Tool '{key}' requires task-augmented execution", - ) + # Enforce mode="required" - must have task metadata + if task_mode == "required" and not task_meta: + raise McpError( + ErrorData( + code=METHOD_NOT_FOUND, + message=f"Tool '{key}' requires task-augmented execution", ) + ) - # Route to background if task metadata present and mode allows - if task_meta and task_mode != "forbidden": - # For FunctionTool, use Docket for background execution - if isinstance(tool, FunctionTool): - task_meta_dict = task_meta.model_dump(exclude_none=True) - return await handle_tool_as_task( - self, key, arguments, task_meta_dict - ) - # For ProxyTool/mounted tools, proceed with normal execution - # They will forward task metadata to their backend - - # Forbidden mode: task requested but mode="forbidden" - # Return error result with returned_immediately=True - if task_meta and task_mode == "forbidden": - return mcp.types.CallToolResult( - content=[ - mcp.types.TextContent( - type="text", - text=f"Tool '{key}' does not support task-augmented execution", - ) - ], - isError=True, - _meta={ - "modelcontextprotocol.io/task": { - "returned_immediately": True - } - }, + # Route to background if task metadata present and mode allows + if task_meta and task_mode != "forbidden": + # For FunctionTool, use Docket for background execution + if isinstance(tool, FunctionTool): + task_meta_dict = task_meta.model_dump(exclude_none=True) + return await handle_tool_as_task( + self, key, arguments, task_meta_dict ) + # For ProxyTool/mounted tools, proceed with normal execution + # They will forward task metadata to their backend + + # Forbidden mode: task requested but mode="forbidden" + # Return error result with returned_immediately=True + if task_meta and task_mode == "forbidden": + return mcp.types.CallToolResult( + content=[ + mcp.types.TextContent( + type="text", + text=f"Tool '{key}' does not support task-augmented execution", + ) + ], + isError=True, + _meta={ + "modelcontextprotocol.io/task": { + "returned_immediately": True + } + }, + ) # Synchronous execution (normal path) result = await self._call_tool_middleware(key, arguments) @@ -2485,19 +2491,7 @@ class FastMCP(Generic[LifespanResultT]): ) # Build experimental capabilities - experimental_capabilities = {} - if fastmcp.settings.enable_tasks: - # Declare SEP-1686 task support per final spec (lines 49-63) - # Nested structure: {list: {}, cancel: {}, requests: {tools: {call: {}}}} - experimental_capabilities["tasks"] = { - "list": {}, - "cancel": {}, - "requests": { - "tools": {"call": {}}, - "prompts": {"get": {}}, - "resources": {"read": {}}, - }, - } + experimental_capabilities = get_task_capabilities() await self._mcp_server.run( read_stream, diff --git a/src/fastmcp/server/tasks/__init__.py b/src/fastmcp/server/tasks/__init__.py index 43eef5d80..10b59b66b 100644 --- a/src/fastmcp/server/tasks/__init__.py +++ b/src/fastmcp/server/tasks/__init__.py @@ -3,43 +3,19 @@ This module implements protocol-level background task execution for MCP servers. """ +from fastmcp.server.tasks.capabilities import get_task_capabilities from fastmcp.server.tasks.config import TaskConfig, TaskMode -from fastmcp.server.tasks.converters import ( - convert_prompt_result, - convert_resource_result, - convert_tool_result, -) -from fastmcp.server.tasks.handlers import ( - handle_prompt_as_task, - handle_resource_as_task, - handle_tool_as_task, -) from fastmcp.server.tasks.keys import ( build_task_key, get_client_task_id_from_key, parse_task_key, ) -from fastmcp.server.tasks.protocol import ( - tasks_cancel_handler, - tasks_get_handler, - tasks_list_handler, - tasks_result_handler, -) __all__ = [ "TaskConfig", "TaskMode", "build_task_key", - "convert_prompt_result", - "convert_resource_result", - "convert_tool_result", "get_client_task_id_from_key", - "handle_prompt_as_task", - "handle_resource_as_task", - "handle_tool_as_task", + "get_task_capabilities", "parse_task_key", - "tasks_cancel_handler", - "tasks_get_handler", - "tasks_list_handler", - "tasks_result_handler", ] diff --git a/src/fastmcp/server/tasks/capabilities.py b/src/fastmcp/server/tasks/capabilities.py new file mode 100644 index 000000000..c08371eca --- /dev/null +++ b/src/fastmcp/server/tasks/capabilities.py @@ -0,0 +1,22 @@ +"""SEP-1686 task capabilities declaration.""" + +from typing import Any + + +def get_task_capabilities() -> dict[str, Any]: + """Return the SEP-1686 task capabilities structure. + + This is the standard capabilities map advertised to clients, + declaring support for list, cancel, and request operations. + """ + return { + "tasks": { + "list": {}, + "cancel": {}, + "requests": { + "tools": {"call": {}}, + "prompts": {"get": {}}, + "resources": {"read": {}}, + }, + } + } diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 982f0f2f1..8a5a55310 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -209,24 +209,6 @@ class Settings(BaseSettings): experimental: ExperimentalSettings = ExperimentalSettings() - # Tasks settings - enable_tasks: Annotated[ - bool, - Field( - description=inspect.cleandoc( - """ - Enable MCP SEP-1686 task protocol support for background execution. - - Server-side: Advertises task capabilities and handles task/* protocol - methods. Tools, prompts, and resources marked with task=True will - execute in the background via Docket. - - Client-side: Advertises task capability to servers. - """ - ), - ), - ] = False - docket: DocketSettings = DocketSettings() enable_rich_tracebacks: Annotated[ diff --git a/tests/client/tasks/conftest.py b/tests/client/tasks/conftest.py index a08438659..a12e0199a 100644 --- a/tests/client/tasks/conftest.py +++ b/tests/client/tasks/conftest.py @@ -1,12 +1,3 @@ """Shared fixtures for client task tests.""" -import pytest - -from fastmcp.utilities.tests import temporary_settings - - -@pytest.fixture(autouse=True) -async def enable_tasks(): - """Enable task protocol support for all client task tests.""" - with temporary_settings(enable_tasks=True): - yield +# Task protocol is now always enabled - no fixture needed diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 3805a23de..7dbc1bd9d 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -1,9 +1,7 @@ import asyncio import sys from typing import Any, cast -from unittest.mock import AsyncMock -import mcp import pytest from mcp import McpError from mcp.client.auth import OAuthClientProvider @@ -415,12 +413,13 @@ async def test_client_connection(fastmcp_server): assert not client.is_connected() -async def test_initialize_called_once(fastmcp_server, monkeypatch): - mock_initialize = AsyncMock() - monkeypatch.setattr(mcp.ClientSession, "initialize", mock_initialize) +async def test_initialize_called_once(fastmcp_server): + """Test that initialization is called once and sets initialize_result.""" client = Client(transport=FastMCPTransport(fastmcp_server)) async with client: - assert mock_initialize.call_count == 1 + # Verify that initialization succeeded by checking initialize_result + assert client.initialize_result is not None + assert client.initialize_result.serverInfo is not None async def test_initialize_result_connected(fastmcp_server): diff --git a/tests/server/tasks/conftest.py b/tests/server/tasks/conftest.py index a527ae70f..56f525eba 100644 --- a/tests/server/tasks/conftest.py +++ b/tests/server/tasks/conftest.py @@ -1,17 +1,3 @@ """Shared fixtures for task tests.""" -import pytest - -from fastmcp.utilities.tests import temporary_settings - - -@pytest.fixture(autouse=True) -async def enable_tasks(): - """Enable task protocol support for all task tests.""" - with temporary_settings(enable_tasks=True): - # Verify enabled - import fastmcp - - assert fastmcp.settings.enable_tasks, "Tasks should be enabled after fixture" - - yield +# Task protocol is now always enabled - no fixture needed diff --git a/tests/server/tasks/test_server_tasks_parameter.py b/tests/server/tasks/test_server_tasks_parameter.py index 842cd02a2..c46e42ada 100644 --- a/tests/server/tasks/test_server_tasks_parameter.py +++ b/tests/server/tasks/test_server_tasks_parameter.py @@ -8,7 +8,6 @@ settings properly override the server default. from fastmcp import FastMCP from fastmcp.client import Client -from fastmcp.utilities.tests import temporary_settings async def test_server_tasks_true_defaults_all_components(): @@ -84,38 +83,21 @@ async def test_server_tasks_false_defaults_all_components(): await client.read_resource("test://resource", task=True) -async def test_server_tasks_none_uses_settings(): - """Server with tasks=None (or omitted) uses global settings.""" - # Test with enable_tasks=True in settings - with temporary_settings(enable_tasks=True): - mcp = FastMCP("test") # tasks=None, should use settings +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" + @mcp.tool() + async def my_tool() -> str: + return "tool result" - async with Client(mcp) as client: - # Tool should support background execution (from settings) - tool_task = await client.call_tool("my_tool", task=True) - assert not tool_task.returned_immediately - - # Test with enable_tasks=False in settings - with temporary_settings(enable_tasks=False): - mcp2 = FastMCP("test2") # tasks=None, should use settings - - @mcp2.tool() - async def my_tool2() -> str: - return "tool result" - - async with Client(mcp2) as client: - # When enable_tasks=False, server doesn't advertise task capabilities. - # Client's task=True is ignored because server doesn't support tasks. - # Tool executes synchronously and succeeds. - tool_task = await client.call_tool("my_tool2", task=True) - assert tool_task.returned_immediately - result = await tool_task.result() - # Tool should execute successfully (synchronously) - assert "tool result" in str(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) + 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(): @@ -261,34 +243,32 @@ async def test_mixed_explicit_and_inherited(): async def test_server_tasks_parameter_sets_component_defaults(): - """Server tasks parameter sets component defaults but global settings gate protocol.""" - # Server tasks=True sets component defaults, but enable_tasks must be True - with temporary_settings(enable_tasks=True): - mcp = FastMCP("test", tasks=True) + """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" + @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 + 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 - with temporary_settings(enable_tasks=True): - mcp2 = FastMCP("test2", tasks=False) + mcp2 = FastMCP("test2", tasks=False) - @mcp2.tool() - async def tool_inherits_false() -> str: - return "tool result" + @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) - assert tool_task.returned_immediately - result = await tool_task.result() - assert result.is_error + 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) + assert tool_task.returned_immediately + result = await tool_task.result() + assert result.is_error async def test_resource_template_inherits_server_tasks_default(): diff --git a/tests/server/tasks/test_task_capabilities.py b/tests/server/tasks/test_task_capabilities.py index 4ad861445..2bafcb707 100644 --- a/tests/server/tasks/test_task_capabilities.py +++ b/tests/server/tasks/test_task_capabilities.py @@ -1,81 +1,44 @@ """ Tests for SEP-1686 task capabilities declaration. -Verifies that the server correctly advertises task support based on settings. +Verifies that the server correctly advertises task support. +Task protocol is now always enabled. """ from fastmcp import FastMCP from fastmcp.client import Client -from fastmcp.utilities.tests import temporary_settings +from fastmcp.server.tasks import get_task_capabilities -async def test_capabilities_include_tasks_when_enabled(): - """Server capabilities include tasks when enable_tasks=True.""" - with temporary_settings(enable_tasks=True): - mcp = FastMCP("capability-test") +async def test_capabilities_include_tasks(): + """Server capabilities always include tasks.""" + mcp = FastMCP("capability-test") - @mcp.tool() - async def test_tool() -> str: - return "test" + @mcp.tool() + async def test_tool() -> str: + return "test" - async with Client(mcp) as client: - # Get server initialization result which includes capabilities - init_result = client.initialize_result + async with Client(mcp) as client: + # Get server initialization result which includes capabilities + init_result = client.initialize_result - # Verify tasks capability is present - assert init_result.capabilities.experimental is not None - assert "tasks" in init_result.capabilities.experimental - tasks_cap = init_result.capabilities.experimental["tasks"] - assert tasks_cap == { - "tools": True, - "prompts": True, - "resources": True, - } + # Verify tasks capability is present + assert init_result.capabilities.experimental is not None + assert "tasks" in init_result.capabilities.experimental + tasks_cap = init_result.capabilities.experimental["tasks"] + assert tasks_cap == get_task_capabilities()["tasks"] -async def test_capabilities_exclude_tasks_when_disabled(): - """Server capabilities do NOT include tasks when enable_tasks=False.""" - with temporary_settings(enable_tasks=False): - mcp = FastMCP("capability-test") +async def test_client_uses_task_capable_session(): + """Client uses task-capable initialization.""" + mcp = FastMCP("client-cap-test") - @mcp.tool() - def test_tool() -> str: - return "test" + @mcp.tool() + async def test_tool() -> str: + return "test" - async with Client(mcp) as client: - # Get server initialization result - init_result = client.initialize_result - - # Verify tasks capability is NOT present - if init_result.capabilities.experimental: - assert "tasks" not in init_result.capabilities.experimental - - -async def test_client_advertises_task_capability_when_enabled(): - """Client advertises experimental.tasks capability when enable_tasks=True.""" - with temporary_settings(enable_tasks=True): - mcp = FastMCP("client-cap-test") - - @mcp.tool() - async def test_tool() -> str: - return "test" - - async with Client(mcp) as client: - # Client should have connected successfully with task capabilities - assert client.initialize_result is not None - - -async def test_client_does_not_advertise_tasks_when_disabled(): - """Client does NOT use custom session when enable_tasks=False.""" - with temporary_settings(enable_tasks=False): - mcp = FastMCP("no-tasks-client-test") - - @mcp.tool() - def test_tool() -> str: - return "test" - - async with Client(mcp) as client: - # Session should be standard ClientSession, not our custom one - - # The session should be a standard ClientSession - assert type(client.session).__name__ == "ClientSession" + async with Client(mcp) as client: + # Client should have connected successfully with task capabilities + assert client.initialize_result is not None + # Session should be a ClientSession (task-capable init uses standard session) + assert type(client.session).__name__ == "ClientSession" diff --git a/tests/server/tasks/test_task_protocol.py b/tests/server/tasks/test_task_protocol.py index 09c0e9712..1d1daa02c 100644 --- a/tests/server/tasks/test_task_protocol.py +++ b/tests/server/tasks/test_task_protocol.py @@ -26,8 +26,6 @@ async def task_enabled_server(): """A tool that always fails.""" raise ValueError("This tool always fails") - assert mcp._support_tasks_by_default - return mcp