diff --git a/fastmcp_slim/fastmcp/server/mixins/lifespan.py b/fastmcp_slim/fastmcp/server/mixins/lifespan.py index bfb424c96..77374d3f7 100644 --- a/fastmcp_slim/fastmcp/server/mixins/lifespan.py +++ b/fastmcp_slim/fastmcp/server/mixins/lifespan.py @@ -135,10 +135,14 @@ class LifespanMixin: from fastmcp.server.tasks.context import restore_task_snapshot # Create Docket instance using configured name and URL - async with Docket( - name=settings.docket.name, - url=settings.docket.url, - ) as docket: + async with ( + SharedContext(), + Docket( + name=settings.docket.name, + url=settings.docket.url, + ) as docket, + ): + self._capture_shared_context() self._docket = docket # Register task-enabled components with Docket diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py index 8555551a0..818cbaca4 100644 --- a/tests/server/test_dependencies.py +++ b/tests/server/test_dependencies.py @@ -9,6 +9,7 @@ from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.dependencies import CurrentContext, Depends, Shared from fastmcp.server.context import Context +from fastmcp.server.dependencies import is_docket_available from tests.conftest import make_server_request_context HUZZAH = "huzzah!" @@ -1193,3 +1194,43 @@ class TestSharedDependencies: in result.messages[0].content.text ) assert call_count == 1 + + @pytest.mark.skipif( + not is_docket_available(), + reason="requires pydocket for the Docket/Worker lifespan path", + ) + async def test_shared_resolves_on_task_capable_server(self): + """Shared() dependencies resolve on a normal request even when the server + has task-enabled components. + + When pydocket is installed AND a task-enabled component exists, the + lifespan takes the Docket/Worker branch. That branch must still capture + the app-scoped SharedContext snapshot so FastMCPServerMiddleware can + re-apply it per request; otherwise Shared() dependencies fail to resolve + on ordinary (non-task) calls. + """ + mcp = FastMCP("task-capable-server") + + call_count = 0 + + def get_config() -> dict[str, str]: + nonlocal call_count + call_count += 1 + return {"key": "value"} + + @mcp.tool(task=True) + async def background_tool(x: int) -> int: + return x + + @mcp.tool() + async def normal_tool(config: dict[str, str] = Shared(get_config)) -> str: + return config["key"] + + async with Client(mcp) as client: + result_a = await client.call_tool("normal_tool", {}) + result_b = await client.call_tool("normal_tool", {}) + + assert result_a.content[0].text == "value" + assert result_b.content[0].text == "value" + # App-scoped: resolved once and reused across requests. + assert call_count == 1