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 <noreply@anthropic.com>
This commit is contained in:
Chris Guidry 2026-01-12 09:59:11 -05:00
commit 687889c598
2 changed files with 9 additions and 37 deletions

View file

@ -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(

View file

@ -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(