6
6
from collections .abc import AsyncIterator , Callable
7
7
from contextlib import (
8
8
AbstractAsyncContextManager ,
9
+ AsyncExitStack ,
9
10
asynccontextmanager ,
10
11
)
11
12
from typing import TYPE_CHECKING , Any , Generic , Literal
18
19
from mcp .server .lowlevel .helper_types import ReadResourceContents
19
20
from mcp .server .lowlevel .server import LifespanResultT
20
21
from mcp .server .lowlevel .server import Server as MCPServer
21
- from mcp .server .lowlevel .server import lifespan as default_lifespan
22
22
from mcp .server .session import ServerSession
23
23
from mcp .server .sse import SseServerTransport
24
24
from mcp .server .stdio import stdio_server
56
56
logger = get_logger (__name__ )
57
57
58
58
59
+ @asynccontextmanager
60
+ async def default_lifespan (server : "FastMCP" ) -> AsyncIterator [Any ]:
61
+ """Default lifespan context manager that does nothing.
62
+
63
+ Args:
64
+ server: The server instance this lifespan is managing
65
+
66
+ Returns:
67
+ An empty context object
68
+ """
69
+ yield {}
70
+
71
+
59
72
def lifespan_wrapper (
60
73
app : "FastMCP" ,
61
74
lifespan : Callable [["FastMCP" ], AbstractAsyncContextManager [LifespanResultT ]],
@@ -64,7 +77,18 @@ def lifespan_wrapper(
64
77
]:
65
78
@asynccontextmanager
66
79
async def wrap (s : MCPServer [LifespanResultT ]) -> AsyncIterator [LifespanResultT ]:
67
- async with lifespan (app ) as context :
80
+ async with AsyncExitStack () as stack :
81
+ # enter main app's lifespan
82
+ context = await stack .enter_async_context (lifespan (app ))
83
+
84
+ # Enter all mounted app lifespans
85
+ for prefix , mounted_app in app ._mounted_apps .items ():
86
+ mounted_context = mounted_app ._mcp_server .lifespan (
87
+ mounted_app ._mcp_server
88
+ )
89
+ await stack .enter_async_context (mounted_context )
90
+ logger .debug (f"Prepared lifespan for mounted app '{ prefix } '" )
91
+
68
92
yield context
69
93
70
94
return wrap
@@ -84,10 +108,16 @@ def __init__(
84
108
self .tags : set [str ] = tags or set ()
85
109
self .settings = fastmcp .settings .ServerSettings (** settings )
86
110
111
+ # Setup for mounted apps - must be initialized before _mcp_server
112
+ self ._mounted_apps : dict [str , FastMCP ] = {}
113
+
114
+ if lifespan is None :
115
+ lifespan = default_lifespan
116
+
87
117
self ._mcp_server = MCPServer [LifespanResultT ](
88
118
name = name or "FastMCP" ,
89
119
instructions = instructions ,
90
- lifespan = lifespan_wrapper (self , lifespan ) if lifespan else default_lifespan , # type: ignore
120
+ lifespan = lifespan_wrapper (self , lifespan ),
91
121
)
92
122
self ._tool_manager = ToolManager (
93
123
duplicate_behavior = self .settings .on_duplicate_tools
@@ -100,9 +130,6 @@ def __init__(
100
130
)
101
131
self .dependencies = self .settings .dependencies
102
132
103
- # Setup for mounted apps
104
- self ._mounted_apps : dict [str , FastMCP ] = {}
105
-
106
133
# Set up MCP protocol handlers
107
134
self ._setup_handlers ()
108
135
@@ -554,10 +581,15 @@ def mount(
554
581
Example: If app has a template with URI "weather://location/{id}", it will be available as "weather+weather://location/{id}"
555
582
- The prompts are imported with prefixed names using the prompt_separator
556
583
Example: If app has a prompt named "weather_prompt", it will be available as "weather_weather_prompt"
584
+ - The mounted app's lifespan will be executed when the parent app's lifespan runs,
585
+ ensuring that any setup needed by the mounted app is performed
557
586
558
587
Args:
559
588
prefix: The prefix to use for the mounted application
560
589
app: The FastMCP application to mount
590
+ tool_separator: Separator for tool names (defaults to "_")
591
+ resource_separator: Separator for resource URIs (defaults to "+")
592
+ prompt_separator: Separator for prompt names (defaults to "_")
561
593
"""
562
594
if tool_separator is None :
563
595
tool_separator = "_"
0 commit comments