22
33from __future__ import annotations
44
5+ import asyncio
56import inspect
67import re
78import secrets
1920 AbstractAsyncContextManager ,
2021 AsyncExitStack ,
2122 asynccontextmanager ,
23+ suppress ,
2224)
2325from dataclasses import dataclass
2426from functools import partial
@@ -213,6 +215,7 @@ def __init__(
213215 self ._lifespan : LifespanCallable [LifespanResultT ] = lifespan or default_lifespan
214216 self ._lifespan_result : LifespanResultT | None = None
215217 self ._lifespan_result_set : bool = False
218+ self ._started : asyncio .Event = asyncio .Event ()
216219
217220 # Generate random ID if no name provided
218221 self ._mcp_server : LowLevelServer [LifespanResultT , Any ] = LowLevelServer [
@@ -374,34 +377,16 @@ def docket(self) -> Docket | None:
374377 return self ._docket
375378
376379 @asynccontextmanager
377- async def _docket_lifespan (
378- self , user_lifespan_result : LifespanResultT
379- ) -> AsyncIterator [LifespanResultT ]:
380- """Manage Docket instance and Worker when experimental support is enabled.
381-
382- Args:
383- user_lifespan_result: The result from the user's lifespan function
384-
385- Yields:
386- User's lifespan result (Docket is managed via ContextVar, not lifespan result)
387- """
380+ async def _docket_lifespan (self ) -> AsyncIterator [None ]:
381+ """Manage Docket instance and Worker for background task execution."""
388382 from fastmcp import settings
389- from fastmcp .server .dependencies import _current_docket , _current_worker
390-
391- # Validate configuration
392- if settings .enable_tasks and not settings .enable_docket :
393- raise RuntimeError (
394- "Server requires enable_docket=True when enable_tasks=True. "
395- "Task protocol support needs Docket for background execution."
396- )
397-
398- if not settings .enable_docket :
399- # Docket support not enabled, pass through user lifespan result
400- yield user_lifespan_result
401- return
402383
403384 # Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
404- from fastmcp .server .dependencies import _current_server
385+ from fastmcp .server .dependencies import (
386+ _current_docket ,
387+ _current_server ,
388+ _current_worker ,
389+ )
405390
406391 server_token = _current_server .set (weakref .ref (self ))
407392
@@ -414,9 +399,10 @@ async def _docket_lifespan(
414399 # Store on server instance for cross-task access (FastMCPTransport)
415400 self ._docket = docket
416401
417- # Register task-enabled tools/prompts/resources with Docket
418- tools = await self .get_tools ()
419- for tool in tools .values ():
402+ # Register local task-enabled tools/prompts/resources with Docket
403+ for tool in self ._tool_manager ._tools .values ():
404+ if not hasattr (tool , "fn" ):
405+ continue
420406 supports_task = (
421407 tool .task
422408 if tool .task is not None
@@ -425,8 +411,9 @@ async def _docket_lifespan(
425411 if supports_task :
426412 docket .register (tool .fn )
427413
428- prompts = await self .get_prompts ()
429- for prompt in prompts .values ():
414+ for prompt in self ._prompt_manager ._prompts .values ():
415+ if not hasattr (prompt , "fn" ):
416+ continue
430417 supports_task = (
431418 prompt .task
432419 if prompt .task is not None
@@ -435,8 +422,9 @@ async def _docket_lifespan(
435422 if supports_task :
436423 docket .register (prompt .fn )
437424
438- resources = await self .get_resources ()
439- for resource in resources .values ():
425+ for resource in self ._resource_manager ._resources .values ():
426+ if not hasattr (resource , "fn" ):
427+ continue
440428 supports_task = (
441429 resource .task
442430 if resource .task is not None
@@ -445,6 +433,17 @@ async def _docket_lifespan(
445433 if supports_task :
446434 docket .register (resource .fn )
447435
436+ for template in self ._resource_manager ._templates .values ():
437+ if not hasattr (template , "fn" ):
438+ continue
439+ supports_task = (
440+ template .task
441+ if template .task is not None
442+ else self ._support_tasks_by_default
443+ )
444+ if supports_task :
445+ docket .register (template .fn )
446+
448447 # Set Docket in ContextVar so CurrentDocket can access it
449448 docket_token = _current_docket .set (docket )
450449 try :
@@ -457,22 +456,21 @@ async def _docket_lifespan(
457456 if settings .docket .worker_name :
458457 worker_kwargs ["name" ] = settings .docket .worker_name
459458
460- # Create and start Worker, then task group for run_forever()
461- async with (
462- Worker (docket , ** worker_kwargs ) as worker , # type: ignore[arg-type]
463- anyio .create_task_group () as tg ,
464- ):
459+ # Create and start Worker
460+ async with Worker (docket , ** worker_kwargs ) as worker : # type: ignore[arg-type]
465461 # Set Worker in ContextVar so CurrentWorker can access it
466462 worker_token = _current_worker .set (worker )
467463 try :
468- # Start worker as background task
469- tg .start_soon (worker .run_forever )
470-
464+ worker_task = asyncio .create_task (worker .run_forever ())
471465 try :
472- yield user_lifespan_result
466+ yield
473467 finally :
474- # Cancel task group when exiting (cancels worker)
475- tg .cancel_scope .cancel ()
468+ # Cancel worker task on exit with timeout to prevent hanging
469+ worker_task .cancel ()
470+ with suppress (
471+ asyncio .CancelledError , asyncio .TimeoutError
472+ ):
473+ await asyncio .wait_for (worker_task , timeout = 2.0 )
476474 finally :
477475 _current_worker .reset (worker_token )
478476 finally :
@@ -492,9 +490,9 @@ async def _lifespan_manager(self) -> AsyncIterator[None]:
492490
493491 async with (
494492 self ._lifespan (self ) as user_lifespan_result ,
495- self ._docket_lifespan (user_lifespan_result ) as lifespan_result ,
493+ self ._docket_lifespan () ,
496494 ):
497- self ._lifespan_result = lifespan_result
495+ self ._lifespan_result = user_lifespan_result
498496 self ._lifespan_result_set = True
499497
500498 async with AsyncExitStack [bool | None ]() as stack :
@@ -503,7 +501,11 @@ async def _lifespan_manager(self) -> AsyncIterator[None]:
503501 cm = server .server ._lifespan_manager ()
504502 )
505503
506- yield
504+ self ._started .set ()
505+ try :
506+ yield
507+ finally :
508+ self ._started .clear ()
507509
508510 self ._lifespan_result_set = False
509511 self ._lifespan_result = None
0 commit comments