From 687889c598216fd0236d2118967994062a7fdff7 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Mon, 12 Jan 2026 09:59:11 -0500 Subject: [PATCH] Simplify ContextVar management - trust Context to set them at request time Remove redundant ContextVar handling: - _lifespan_manager no longer re-sets ContextVars in early-return branch - Handler fallback logic removed (no more `if docket is None: docket = server._docket`) The authoritative place for request-context ContextVars is now Context.__aenter__, which sets _current_docket and _current_worker from server instance attributes. Co-Authored-By: Claude Opus 4.5 --- src/fastmcp/server/server.py | 29 +++++----------------------- src/fastmcp/server/tasks/handlers.py | 17 ++++------------ 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 1a8cab4d3..0be959518 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -613,32 +613,13 @@ class FastMCP(Generic[LifespanResultT]): logger.info(f"[{instance_id}] _lifespan_manager ENTERING") if self._lifespan_result_set: - # Lifespan already ran in a parent async context. We need to set ContextVars - # again in THIS context because ContextVars don't propagate across different - # async contexts (e.g., Starlette's ASGI lifespan vs FastMCP's outer lifespan). - # Request handlers inherit from Starlette's context, so we must set here. - from fastmcp.server.dependencies import ( - _current_docket, - _current_server, - _current_worker, - ) - + # Lifespan already ran - ContextVars will be set by Context.__aenter__ + # at request time, so we just yield here. logger.info( - f"[{instance_id}] _lifespan_manager: already set, setting ContextVars " - f"for this context (docket={self._docket}, worker={self._worker})" + f"[{instance_id}] _lifespan_manager: already set, yielding " + f"(ContextVars managed by Context at request time)" ) - - server_token = _current_server.set(weakref.ref(self)) - docket_token = _current_docket.set(self._docket) if self._docket else None - worker_token = _current_worker.set(self._worker) if self._worker else None - try: - yield - finally: - _current_server.reset(server_token) - if docket_token is not None: - _current_docket.reset(docket_token) - if worker_token is not None: - _current_worker.reset(worker_token) + yield return logger.info( diff --git a/src/fastmcp/server/tasks/handlers.py b/src/fastmcp/server/tasks/handlers.py index 4f850b921..1a63cc158 100644 --- a/src/fastmcp/server/tasks/handlers.py +++ b/src/fastmcp/server/tasks/handlers.py @@ -63,16 +63,11 @@ async def handle_tool_as_task( ctx = get_context() session_id = ctx.session_id - # Try ContextVar first, fall back to server instance attribute. - # ContextVars don't propagate to request handlers in ASGI servers because - # request tasks are created from the main event loop, not the lifespan context. + # Get Docket from ContextVar (set by Context.__aenter__ at request time) docket = _current_docket.get() - if docket is None: - docket = server._docket _logger.info( f"[{instance_id}] handle_tool_as_task: tool={tool_name}, " - f"_current_docket.get()={_current_docket.get()}, server._docket={server._docket}, " - f"using docket={docket}" + f"_current_docket.get()={docket}" ) if docket is None: _logger.error( @@ -225,10 +220,8 @@ async def handle_prompt_as_task( ctx = get_context() session_id = ctx.session_id - # Try ContextVar first, fall back to server instance attribute + # Get Docket from ContextVar (set by Context.__aenter__ at request time) docket = _current_docket.get() - if docket is None: - docket = server._docket if docket is None: raise McpError( ErrorData( @@ -337,10 +330,8 @@ async def handle_resource_as_task( ctx = get_context() session_id = ctx.session_id - # Try ContextVar first, fall back to server instance attribute + # Get Docket from ContextVar (set by Context.__aenter__ at request time) docket = _current_docket.get() - if docket is None: - docket = server._docket if docket is None: raise McpError( ErrorData(