diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx index 2ca24948eb..f4fd8c5084 100644 --- a/docs/servers/tasks.mdx +++ b/docs/servers/tasks.mdx @@ -55,11 +55,8 @@ Background tasks require explicit opt-in: | Environment Variable | Default | Description | |---------------------|---------|-------------| | `FASTMCP_ENABLE_TASKS` | `false` | Enable the MCP task protocol | -| `FASTMCP_ENABLE_DOCKET` | `false` | Enable the Docket task system | | `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) | -Both `ENABLE_TASKS` and `ENABLE_DOCKET` must be `true` for background tasks to work. - You can also set a server-wide default in the constructor: ```python diff --git a/examples/tasks/.envrc b/examples/tasks/.envrc index 08830292b8..c9afbca5b3 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 Docket support for background task execution -export FASTMCP_ENABLE_DOCKET=true - # Enable MCP SEP-1686 task protocol support export FASTMCP_ENABLE_TASKS=true diff --git a/examples/tasks/README.md b/examples/tasks/README.md index ed0cd5be0d..f6275009b9 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_DOCKET` | `false` | Enable Docket task system | | `FASTMCP_ENABLE_TASKS` | `false` | Enable MCP task protocol (SEP-1686) | | `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL | diff --git a/src/fastmcp/cli/tasks.py b/src/fastmcp/cli/tasks.py index b5eceb4e90..931802d0fe 100644 --- a/src/fastmcp/cli/tasks.py +++ b/src/fastmcp/cli/tasks.py @@ -19,25 +19,17 @@ ) -def check_docket_enabled() -> None: - """Check if Docket is enabled with a distributed backend. +def check_distributed_backend() -> None: + """Check if Docket is configured with a distributed backend. + + The CLI worker runs as a separate process, so it needs Redis/Valkey + to coordinate with the main server process. Raises: - SystemExit: If Docket isn't enabled or using memory:// URL + SystemExit: If using memory:// URL """ import fastmcp - # Check if Docket is enabled - if not fastmcp.settings.enable_docket: - console.print( - "[bold red]✗ Docket not enabled[/bold red]\n\n" - "Docket task support is not enabled.\n\n" - "To enable Docket, set the environment variable:\n" - " [cyan]export FASTMCP_ENABLE_DOCKET=true[/cyan]\n\n" - "Then try again." - ) - sys.exit(1) - docket_url = fastmcp.settings.docket.url # Check for memory:// URL and provide helpful error @@ -86,7 +78,7 @@ def worker( """ import fastmcp - check_docket_enabled() + check_distributed_backend() # Load server to get task functions try: diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 0fb4be0c75..424df2d7ed 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -859,7 +859,7 @@ async def connect_session( experimental_capabilities = {} if fastmcp.settings.enable_tasks: - # Declare SEP-1686 task support (enable_tasks requires enable_docket via validator) + # Declare SEP-1686 task support experimental_capabilities["tasks"] = { "tools": True, "prompts": True, diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py index 51abc1f9cf..31b170c79f 100644 --- a/src/fastmcp/server/dependencies.py +++ b/src/fastmcp/server/dependencies.py @@ -297,21 +297,12 @@ class _CurrentDocket(Dependency): """Internal dependency class for CurrentDocket.""" async def __aenter__(self) -> Docket: - import fastmcp - - # Check if flag is enabled - if not fastmcp.settings.enable_docket: - raise RuntimeError( - "Docket support is not enabled. " - "Set FASTMCP_ENABLE_DOCKET=true to enable Docket support." - ) - # Get Docket from ContextVar (set by _docket_lifespan) docket = _current_docket.get() if docket is None: raise RuntimeError( - "No Docket instance found. This should not happen when " - "FASTMCP_ENABLE_DOCKET is enabled." + "No Docket instance found. Docket is only available within " + "a running FastMCP server context." ) return docket @@ -321,16 +312,13 @@ def CurrentDocket() -> Docket: """Get the current Docket instance managed by FastMCP. This dependency provides access to the Docket instance that FastMCP - automatically creates when Docket support is enabled. - - Requires: - - FASTMCP_ENABLE_DOCKET=true + automatically creates for background task scheduling. Returns: A dependency that resolves to the active Docket instance Raises: - RuntimeError: If flag not enabled (during resolution) + RuntimeError: If not within a FastMCP server context Example: ```python @@ -349,19 +337,11 @@ class _CurrentWorker(Dependency): """Internal dependency class for CurrentWorker.""" async def __aenter__(self) -> Worker: - import fastmcp - - if not fastmcp.settings.enable_docket: - raise RuntimeError( - "Docket support is not enabled. " - "Set FASTMCP_ENABLE_DOCKET=true to enable Docket support." - ) - worker = _current_worker.get() if worker is None: raise RuntimeError( - "No Worker instance found. This should not happen when " - "FASTMCP_ENABLE_DOCKET is enabled." + "No Worker instance found. Worker is only available within " + "a running FastMCP server context." ) return worker @@ -371,16 +351,13 @@ def CurrentWorker() -> Worker: """Get the current Docket Worker instance managed by FastMCP. This dependency provides access to the Worker instance that FastMCP - automatically creates when Docket support is enabled. - - Requires: - - FASTMCP_ENABLE_DOCKET=true + automatically creates for background task processing. Returns: A dependency that resolves to the active Worker instance Raises: - RuntimeError: If flag not enabled (during resolution) + RuntimeError: If not within a FastMCP server context Example: ```python @@ -463,8 +440,7 @@ async def __aenter__(self) -> DocketProgress: docket = _current_docket.get() if docket is None: raise RuntimeError( - "Progress dependency requires Docket to be enabled. " - "Set FASTMCP_ENABLE_DOCKET=true" + "Progress dependency requires a FastMCP server context." ) from None # Return in-memory progress for immediate execution diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 64dc3884e7..83223084b3 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import inspect import re import secrets @@ -19,6 +20,7 @@ AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager, + suppress, ) from dataclasses import dataclass from functools import partial @@ -213,6 +215,7 @@ def __init__( self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan self._lifespan_result: LifespanResultT | None = None self._lifespan_result_set: bool = False + self._started: asyncio.Event = asyncio.Event() # Generate random ID if no name provided self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[ @@ -374,34 +377,16 @@ def docket(self) -> Docket | None: return self._docket @asynccontextmanager - async def _docket_lifespan( - self, user_lifespan_result: LifespanResultT - ) -> AsyncIterator[LifespanResultT]: - """Manage Docket instance and Worker when experimental support is enabled. - - Args: - user_lifespan_result: The result from the user's lifespan function - - Yields: - User's lifespan result (Docket is managed via ContextVar, not lifespan result) - """ + async def _docket_lifespan(self) -> AsyncIterator[None]: + """Manage Docket instance and Worker for background task execution.""" from fastmcp import settings - from fastmcp.server.dependencies import _current_docket, _current_worker - - # Validate configuration - if settings.enable_tasks and not settings.enable_docket: - raise RuntimeError( - "Server requires enable_docket=True when enable_tasks=True. " - "Task protocol support needs Docket for background execution." - ) - - if not settings.enable_docket: - # Docket support not enabled, pass through user lifespan result - yield user_lifespan_result - return # Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles) - from fastmcp.server.dependencies import _current_server + from fastmcp.server.dependencies import ( + _current_docket, + _current_server, + _current_worker, + ) server_token = _current_server.set(weakref.ref(self)) @@ -414,9 +399,10 @@ async def _docket_lifespan( # Store on server instance for cross-task access (FastMCPTransport) self._docket = docket - # Register task-enabled tools/prompts/resources with Docket - tools = await self.get_tools() - for tool in tools.values(): + # Register local task-enabled tools/prompts/resources with Docket + for tool in self._tool_manager._tools.values(): + if not hasattr(tool, "fn"): + continue supports_task = ( tool.task if tool.task is not None @@ -425,8 +411,9 @@ async def _docket_lifespan( if supports_task: docket.register(tool.fn) - prompts = await self.get_prompts() - for prompt in prompts.values(): + for prompt in self._prompt_manager._prompts.values(): + if not hasattr(prompt, "fn"): + continue supports_task = ( prompt.task if prompt.task is not None @@ -435,8 +422,9 @@ async def _docket_lifespan( if supports_task: docket.register(prompt.fn) - resources = await self.get_resources() - for resource in resources.values(): + for resource in self._resource_manager._resources.values(): + if not hasattr(resource, "fn"): + continue supports_task = ( resource.task if resource.task is not None @@ -445,6 +433,17 @@ async def _docket_lifespan( if supports_task: docket.register(resource.fn) + for template in self._resource_manager._templates.values(): + if not hasattr(template, "fn"): + continue + supports_task = ( + template.task + if template.task is not None + else self._support_tasks_by_default + ) + if supports_task: + docket.register(template.fn) + # Set Docket in ContextVar so CurrentDocket can access it docket_token = _current_docket.set(docket) try: @@ -457,22 +456,21 @@ async def _docket_lifespan( if settings.docket.worker_name: worker_kwargs["name"] = settings.docket.worker_name - # Create and start Worker, then task group for run_forever() - async with ( - Worker(docket, **worker_kwargs) as worker, # type: ignore[arg-type] - anyio.create_task_group() as tg, - ): + # Create and start Worker + async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type] # Set Worker in ContextVar so CurrentWorker can access it worker_token = _current_worker.set(worker) try: - # Start worker as background task - tg.start_soon(worker.run_forever) - + worker_task = asyncio.create_task(worker.run_forever()) try: - yield user_lifespan_result + yield finally: - # Cancel task group when exiting (cancels worker) - tg.cancel_scope.cancel() + # Cancel worker task on exit with timeout to prevent hanging + worker_task.cancel() + with suppress( + asyncio.CancelledError, asyncio.TimeoutError + ): + await asyncio.wait_for(worker_task, timeout=2.0) finally: _current_worker.reset(worker_token) finally: @@ -492,9 +490,9 @@ async def _lifespan_manager(self) -> AsyncIterator[None]: async with ( self._lifespan(self) as user_lifespan_result, - self._docket_lifespan(user_lifespan_result) as lifespan_result, + self._docket_lifespan(), ): - self._lifespan_result = lifespan_result + self._lifespan_result = user_lifespan_result self._lifespan_result_set = True async with AsyncExitStack[bool | None]() as stack: @@ -503,7 +501,11 @@ async def _lifespan_manager(self) -> AsyncIterator[None]: cm=server.server._lifespan_manager() ) - yield + self._started.set() + try: + yield + finally: + self._started.clear() self._lifespan_result_set = False self._lifespan_result = None diff --git a/src/fastmcp/server/tasks/handlers.py b/src/fastmcp/server/tasks/handlers.py index 14edca901c..e8be0b4772 100644 --- a/src/fastmcp/server/tasks/handlers.py +++ b/src/fastmcp/server/tasks/handlers.py @@ -61,7 +61,7 @@ async def handle_tool_as_task( raise McpError( ErrorData( code=INTERNAL_ERROR, - message="Background tasks require Docket. Set FASTMCP_ENABLE_DOCKET=true", + message="Background tasks require a running FastMCP server context", ) ) @@ -169,7 +169,7 @@ async def handle_prompt_as_task( raise McpError( ErrorData( code=INTERNAL_ERROR, - message="Background tasks require Docket. Set FASTMCP_ENABLE_DOCKET=true", + message="Background tasks require a running FastMCP server context", ) ) diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 9ba30e4371..982f0f2f14 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -209,21 +209,7 @@ def normalize_log_level(cls, v): experimental: ExperimentalSettings = ExperimentalSettings() - # Docket/Tasks settings - enable_docket: Annotated[ - bool, - Field( - description=inspect.cleandoc( - """ - Enable Docket support for background task execution. - When enabled, FastMCP will create a Docket instance with a Worker - available via dependency injection. This allows tools, prompts, and - resources to schedule background work using CurrentDocket(). - """ - ), - ), - ] = False - + # Tasks settings enable_tasks: Annotated[ bool, Field( @@ -231,11 +217,11 @@ def normalize_log_level(cls, v): """ Enable MCP SEP-1686 task protocol support for background execution. - Server-side: Requires enable_docket=True (validated at server startup). - Advertises task capabilities and handles task/* protocol methods. + 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. No Docket needed - on client side. + Client-side: Advertises task capability to servers. """ ), ), diff --git a/src/fastmcp/utilities/tests.py b/src/fastmcp/utilities/tests.py index 45db76ab7f..d78ce3e621 100644 --- a/src/fastmcp/utilities/tests.py +++ b/src/fastmcp/utilities/tests.py @@ -209,16 +209,19 @@ async def test_greet(server: str): ) ) - # Give the server a moment to start + # Wait for server lifespan to be ready + await server._started.wait() + + # Give uvicorn a moment to bind the port after lifespan is ready await asyncio.sleep(0.1) try: yield f"http://{host}:{port}{path}" finally: - # Cleanup: cancel the task + # Cleanup: cancel the task with timeout to avoid hanging on Windows server_task.cancel() - with suppress(asyncio.CancelledError): - await server_task + with suppress(asyncio.CancelledError, asyncio.TimeoutError): + await asyncio.wait_for(server_task, timeout=2.0) @contextmanager diff --git a/tests/cli/test_tasks.py b/tests/cli/test_tasks.py index d0b7994af5..a48c44ff7a 100644 --- a/tests/cli/test_tasks.py +++ b/tests/cli/test_tasks.py @@ -2,38 +2,23 @@ import pytest -from fastmcp.cli.tasks import check_docket_enabled, tasks_app +from fastmcp.cli.tasks import check_distributed_backend, tasks_app from fastmcp.utilities.tests import temporary_settings -class TestCheckDocketEnabled: - """Test the Docket enabled checker function.""" +class TestCheckDistributedBackend: + """Test the distributed backend checker function.""" - def test_succeeds_when_docket_enabled_with_redis(self): - """Test that it succeeds when Docket is enabled with Redis.""" - with temporary_settings( - enable_docket=True, - docket__url="redis://localhost:6379/0", - ): - check_docket_enabled() - - def test_exits_when_docket_not_enabled(self): - """Test that it exits with helpful error when Docket not enabled.""" - with temporary_settings(enable_docket=False): - with pytest.raises(SystemExit) as exc_info: - check_docket_enabled() - - assert isinstance(exc_info.value, SystemExit) - assert exc_info.value.code == 1 + def test_succeeds_with_redis_url(self): + """Test that it succeeds with Redis URL.""" + with temporary_settings(docket__url="redis://localhost:6379/0"): + check_distributed_backend() def test_exits_with_helpful_error_for_memory_url(self): """Test that it exits with helpful error for memory:// URLs.""" - with temporary_settings( - enable_docket=True, - docket__url="memory://test-123", - ): + with temporary_settings(docket__url="memory://test-123"): with pytest.raises(SystemExit) as exc_info: - check_docket_enabled() + check_distributed_backend() assert isinstance(exc_info.value, SystemExit) assert exc_info.value.code == 1 diff --git a/tests/client/tasks/conftest.py b/tests/client/tasks/conftest.py index 4a004de95d..a08438659e 100644 --- a/tests/client/tasks/conftest.py +++ b/tests/client/tasks/conftest.py @@ -6,10 +6,7 @@ @pytest.fixture(autouse=True) -async def enable_docket_and_tasks(): - """Enable Docket and task protocol support for all client task tests.""" - with temporary_settings( - enable_docket=True, - enable_tasks=True, - ): +async def enable_tasks(): + """Enable task protocol support for all client task tests.""" + with temporary_settings(enable_tasks=True): yield diff --git a/tests/server/proxy/test_stateful_proxy_client.py b/tests/server/proxy/test_stateful_proxy_client.py index 9db4114d67..e7dc3cb474 100644 --- a/tests/server/proxy/test_stateful_proxy_client.py +++ b/tests/server/proxy/test_stateful_proxy_client.py @@ -58,8 +58,7 @@ async def stateless_server(stateful_proxy_server: FastMCP): host="127.0.0.1", port=port, stateless_http=True ) ) - async with Client(transport=url) as client: - assert await client.ping() + await stateful_proxy_server._started.wait() yield url task.cancel() try: diff --git a/tests/server/tasks/conftest.py b/tests/server/tasks/conftest.py index 59bf024f61..a527ae70fa 100644 --- a/tests/server/tasks/conftest.py +++ b/tests/server/tasks/conftest.py @@ -6,16 +6,12 @@ @pytest.fixture(autouse=True) -async def enable_docket_and_tasks(): - """Enable Docket and task protocol support for all task tests.""" - with temporary_settings( - enable_docket=True, - enable_tasks=True, - ): - # Verify both are enabled +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_docket, "Docket should be enabled after fixture" assert fastmcp.settings.enable_tasks, "Tasks should be enabled after fixture" yield diff --git a/tests/server/tasks/test_progress_dependency.py b/tests/server/tasks/test_progress_dependency.py index 9c1b8ddcbb..d6b121ba74 100644 --- a/tests/server/tasks/test_progress_dependency.py +++ b/tests/server/tasks/test_progress_dependency.py @@ -1,11 +1,8 @@ """Tests for FastMCP Progress dependency.""" -import pytest - from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.dependencies import Progress -from fastmcp.utilities.tests import temporary_settings async def test_progress_in_immediate_execution(): @@ -111,24 +108,6 @@ async def task_with_progress(progress: Progress = Progress()) -> str: assert result.content[0].text == "done" -async def test_progress_fails_without_docket(): - """Test Progress dependency fails when Docket is not enabled.""" - with temporary_settings(enable_docket=False, enable_tasks=False): - mcp = FastMCP("test") - - @mcp.tool() - async def test_tool(progress: Progress = Progress()) -> str: - return "done" - - async with Client(mcp) as client: - with pytest.raises(Exception) as exc_info: - await client.call_tool("test_tool", {}) - - error_str = str(exc_info.value) - assert "Failed to resolve dependency" in error_str - assert "progress" in error_str - - async def test_inmemory_progress_state(): """Test that in-memory progress stores and returns state correctly.""" mcp = FastMCP("test") diff --git a/tests/server/tasks/test_server_tasks_parameter.py b/tests/server/tasks/test_server_tasks_parameter.py index cc6c38b1cc..2a87434f63 100644 --- a/tests/server/tasks/test_server_tasks_parameter.py +++ b/tests/server/tasks/test_server_tasks_parameter.py @@ -81,10 +81,7 @@ async def my_resource() -> str: 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_docket=True, - enable_tasks=True, - ): + with temporary_settings(enable_tasks=True): mcp = FastMCP("test") # tasks=None, should use settings @mcp.tool() @@ -97,10 +94,7 @@ async def my_tool() -> str: assert not tool_task.returned_immediately # Test with enable_tasks=False in settings - with temporary_settings( - enable_docket=True, - enable_tasks=False, - ): + with temporary_settings(enable_tasks=False): mcp2 = FastMCP("test2") # tasks=None, should use settings @mcp2.tool() @@ -249,10 +243,7 @@ async def explicit_false_resource() -> str: 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_docket=True, - enable_tasks=True, - ): + with temporary_settings(enable_tasks=True): mcp = FastMCP("test", tasks=True) @mcp.tool() @@ -265,10 +256,7 @@ async def tool_inherits_true() -> str: assert not tool_task.returned_immediately # Server tasks=False sets component defaults - with temporary_settings( - enable_docket=True, - enable_tasks=True, - ): + with temporary_settings(enable_tasks=True): mcp2 = FastMCP("test2", tasks=False) @mcp2.tool() diff --git a/tests/server/tasks/test_task_capabilities.py b/tests/server/tasks/test_task_capabilities.py index 8e7177402b..4ad861445c 100644 --- a/tests/server/tasks/test_task_capabilities.py +++ b/tests/server/tasks/test_task_capabilities.py @@ -4,8 +4,6 @@ Verifies that the server correctly advertises task support based on settings. """ -import pytest - from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.utilities.tests import temporary_settings @@ -13,10 +11,7 @@ async def test_capabilities_include_tasks_when_enabled(): """Server capabilities include tasks when enable_tasks=True.""" - with temporary_settings( - enable_docket=True, - enable_tasks=True, - ): + with temporary_settings(enable_tasks=True): mcp = FastMCP("capability-test") @mcp.tool() @@ -40,10 +35,7 @@ async def test_tool() -> str: async def test_capabilities_exclude_tasks_when_disabled(): """Server capabilities do NOT include tasks when enable_tasks=False.""" - with temporary_settings( - enable_docket=True, - enable_tasks=False, - ): + with temporary_settings(enable_tasks=False): mcp = FastMCP("capability-test") @mcp.tool() @@ -59,51 +51,9 @@ def test_tool() -> str: assert "tasks" not in init_result.capabilities.experimental -async def test_capabilities_exclude_tasks_when_docket_disabled(): - """Server capabilities do NOT include tasks when enable_docket=False.""" - with temporary_settings( - enable_docket=False, - enable_tasks=False, - ): - mcp = FastMCP("capability-test") - - @mcp.tool() - 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_enable_tasks_requires_enable_docket(): - """Setting enable_tasks=True without enable_docket=True raises error at server startup.""" - with temporary_settings( - enable_docket=False, - enable_tasks=True, - ): - mcp = FastMCP("config-test") - - @mcp.tool() - async def test_tool() -> str: - return "test" - - # Should fail when trying to start server (during lifespan) - with pytest.raises(RuntimeError, match="requires.*enable_docket.*enable_tasks"): - async with Client(mcp): - pass # Should never reach here - - async def test_client_advertises_task_capability_when_enabled(): """Client advertises experimental.tasks capability when enable_tasks=True.""" - with temporary_settings( - enable_docket=True, - enable_tasks=True, - ): + with temporary_settings(enable_tasks=True): mcp = FastMCP("client-cap-test") @mcp.tool() @@ -117,10 +67,7 @@ async def test_tool() -> str: async def test_client_does_not_advertise_tasks_when_disabled(): """Client does NOT use custom session when enable_tasks=False.""" - with temporary_settings( - enable_docket=True, - enable_tasks=False, - ): + with temporary_settings(enable_tasks=False): mcp = FastMCP("no-tasks-client-test") @mcp.tool() diff --git a/tests/server/test_logging.py b/tests/server/test_logging.py index 1a3c09a63d..cfc376c0b0 100644 --- a/tests/server/test_logging.py +++ b/tests/server/test_logging.py @@ -36,7 +36,7 @@ async def test_uvicorn_logging_default_level( server_task = asyncio.create_task( mcp_server.run_http_async(log_level=test_log_level, port=8003) ) - await asyncio.sleep(0.01) + await mcp_server._started.wait() mock_uvicorn_config_constructor.assert_called_once() _, kwargs_config = mock_uvicorn_config_constructor.call_args @@ -96,7 +96,7 @@ async def test_uvicorn_logging_with_custom_log_config( uvicorn_config={"log_config": sample_log_config}, port=8004 ) ) - await asyncio.sleep(0.01) + await mcp_server._started.wait() mock_uvicorn_config_constructor.assert_called_once() _, kwargs_config = mock_uvicorn_config_constructor.call_args @@ -159,7 +159,7 @@ async def test_uvicorn_logging_custom_log_config_overrides_log_level_param( port=8005, ) ) - await asyncio.sleep(0.01) + await mcp_server._started.wait() mock_uvicorn_config_constructor.assert_called_once() _, kwargs_config = mock_uvicorn_config_constructor.call_args diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 844fd24649..0ab7dcb554 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -950,10 +950,8 @@ def hello(): async with Client(mcp) as client: await client.call_tool("hello", {}) - assert len(lifespan_check) > 0 - # in the present implementation the sub server will be invoked 3 times - # to call its tool - assert lifespan_check.count("start") >= 2 + # Lifespan is entered exactly once and kept alive by Docket worker + assert lifespan_check == ["start"] class TestResourceNamePrefixing: diff --git a/tests/server/test_server_docket.py b/tests/server/test_server_docket.py index 442d725178..44fe050ffe 100644 --- a/tests/server/test_server_docket.py +++ b/tests/server/test_server_docket.py @@ -3,43 +3,19 @@ import asyncio from contextlib import asynccontextmanager -import pytest from docket import Docket from docket.worker import Worker from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.dependencies import CurrentDocket, CurrentWorker -from fastmcp.exceptions import ToolError from fastmcp.server.dependencies import get_context -from fastmcp.utilities.tests import temporary_settings HUZZAH = "huzzah!" -@pytest.fixture(autouse=True) -def enable_docket(): - """Enable Docket support for all tests in this suite.""" - with temporary_settings(enable_docket=True): - yield - - -async def test_docket_disabled(): - """Verify that Docket errors when flag is disabled.""" - with temporary_settings(enable_docket=False): - mcp = FastMCP("test-server") - - @mcp.tool() - def needs_docket(docket: Docket = CurrentDocket()) -> str: - return f"Got docket: {type(docket).__name__}" - - async with Client(mcp) as client: - with pytest.raises(ToolError, match="Failed to resolve dependency"): - await client.call_tool("needs_docket", {}) - - -async def test_current_docket_with_flag_enabled(): - """CurrentDocket dependency works when experimental flag is enabled.""" +async def test_current_docket(): + """CurrentDocket dependency provides access to Docket instance.""" mcp = FastMCP("test-server") @mcp.tool() @@ -52,8 +28,8 @@ def check_docket(docket: Docket = CurrentDocket()) -> str: assert HUZZAH in str(result) -async def test_current_worker_with_flag_enabled(): - """CurrentWorker dependency works when experimental flag is enabled.""" +async def test_current_worker(): + """CurrentWorker dependency provides access to Worker instance.""" mcp = FastMCP("test-server") @mcp.tool() @@ -100,7 +76,7 @@ async def background_task(name: str): async def test_current_docket_in_resource(): - """CurrentDocket works in resources when flag is enabled.""" + """CurrentDocket works in resources.""" mcp = FastMCP("test-server") @mcp.resource("docket://info") @@ -114,7 +90,7 @@ def get_docket_info(docket: Docket = CurrentDocket()) -> str: async def test_current_docket_in_prompt(): - """CurrentDocket works in prompts when flag is enabled.""" + """CurrentDocket works in prompts.""" mcp = FastMCP("test-server") @mcp.prompt() @@ -128,7 +104,7 @@ def task_prompt(task_type: str, docket: Docket = CurrentDocket()) -> str: async def test_current_docket_in_resource_template(): - """CurrentDocket works in resource templates when flag is enabled.""" + """CurrentDocket works in resource templates.""" mcp = FastMCP("test-server") @mcp.resource("docket://tasks/{task_id}") diff --git a/tests/server/test_server_lifespan.py b/tests/server/test_server_lifespan.py index a4373a4a78..e09198e614 100644 --- a/tests/server/test_server_lifespan.py +++ b/tests/server/test_server_lifespan.py @@ -17,9 +17,11 @@ async def test_server_lifespan_basic(self): @asynccontextmanager async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict[str, Any]]: - _ = lifespan_events.append("enter") - yield {"initialized": True} - _ = lifespan_events.append("exit") + lifespan_events.append("enter") + try: + yield {"initialized": True} + finally: + lifespan_events.append("exit") mcp = FastMCP("TestServer", lifespan=server_lifespan)