diff --git a/docs/deployment/asgi.mdx b/docs/deployment/asgi.mdx index fe613fd1b..6fc5624fb 100644 --- a/docs/deployment/asgi.mdx +++ b/docs/deployment/asgi.mdx @@ -124,7 +124,7 @@ app = Starlette( Mount("/mcp-server", app=mcp_app), # Add other routes as needed ], - lifespan=mcp_app.router.lifespan_context, + lifespan=mcp_app.lifespan, ) ``` @@ -154,7 +154,7 @@ mcp_app = mcp.http_app(path='/mcp') inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)]) app = Starlette( routes=[Mount("/outer", app=inner_app)], - lifespan=mcp_app.router.lifespan_context, + lifespan=mcp_app.lifespan, ) ``` @@ -181,7 +181,7 @@ mcp = FastMCP("MyServer") mcp_app = mcp.http_app(path='/mcp') # Create a FastAPI app and mount the MCP server -app = FastAPI(lifespan=mcp_app.router.lifespan_context) +app = FastAPI(lifespan=mcp_app.lifespan) app.mount("/mcp-server", mcp_app) ``` diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 8dbeacfee..2437e85f8 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -27,7 +27,7 @@ from starlette.middleware.authentication import AuthenticationMiddleware from starlette.requests import Request from starlette.responses import Response from starlette.routing import BaseRoute, Mount, Route -from starlette.types import Receive, Scope, Send +from starlette.types import Lifespan, Receive, Scope, Send from fastmcp.utilities.logging import get_logger @@ -43,6 +43,12 @@ _current_http_request: ContextVar[Request | None] = ContextVar( ) +class StarletteWithLifespan(Starlette): + @property + def lifespan(self) -> Lifespan: + return self.router.lifespan_context + + @contextmanager def set_http_request(request: Request) -> Generator[Request, None, None]: token = _current_http_request.set(request) @@ -122,7 +128,7 @@ def create_base_app( middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None, -) -> Starlette: +) -> StarletteWithLifespan: """Create a base Starlette app with common middleware and routes. Args: @@ -137,7 +143,7 @@ def create_base_app( # Always add RequestContextMiddleware as the outermost middleware middleware.append(Middleware(RequestContextMiddleware)) - return Starlette( + return StarletteWithLifespan( routes=routes, middleware=middleware, debug=debug, @@ -157,7 +163,7 @@ def create_sse_app( debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None, -) -> Starlette: +) -> StarletteWithLifespan: """Return an instance of the SSE server app. Args: @@ -262,7 +268,7 @@ def create_streamable_http_app( debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None, -) -> Starlette: +) -> StarletteWithLifespan: """Return an instance of the StreamableHTTP server app. Args: diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 1d520bae5..c6aaeedba 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -35,7 +35,6 @@ from mcp.types import Resource as MCPResource from mcp.types import ResourceTemplate as MCPResourceTemplate from mcp.types import Tool as MCPTool from pydantic import AnyUrl -from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.requests import Request from starlette.responses import Response @@ -48,7 +47,11 @@ from fastmcp.prompts import Prompt, PromptManager from fastmcp.prompts.prompt import PromptResult from fastmcp.resources import Resource, ResourceManager from fastmcp.resources.template import ResourceTemplate -from fastmcp.server.http import create_sse_app +from fastmcp.server.http import ( + StarletteWithLifespan, + create_sse_app, + create_streamable_http_app, +) from fastmcp.tools import ToolManager from fastmcp.tools.tool import Tool from fastmcp.utilities.cache import TimedCache @@ -59,7 +62,6 @@ if TYPE_CHECKING: from fastmcp.client import Client from fastmcp.server.openapi import FastMCPOpenAPI from fastmcp.server.proxy import FastMCPProxy - logger = get_logger(__name__) DuplicateBehavior = Literal["warn", "error", "replace", "ignore"] @@ -806,7 +808,7 @@ class FastMCP(Generic[LifespanResultT]): path: str | None = None, message_path: str | None = None, middleware: list[Middleware] | None = None, - ) -> Starlette: + ) -> StarletteWithLifespan: """ Create a Starlette app for the SSE server. @@ -837,7 +839,7 @@ class FastMCP(Generic[LifespanResultT]): self, path: str | None = None, middleware: list[Middleware] | None = None, - ) -> Starlette: + ) -> StarletteWithLifespan: """ Create a Starlette app for the StreamableHTTP server. @@ -858,7 +860,7 @@ class FastMCP(Generic[LifespanResultT]): path: str | None = None, middleware: list[Middleware] | None = None, transport: Literal["streamable-http", "sse"] = "streamable-http", - ) -> Starlette: + ) -> StarletteWithLifespan: """Create a Starlette app using the specified HTTP transport. Args: @@ -869,7 +871,6 @@ class FastMCP(Generic[LifespanResultT]): Returns: A Starlette application configured with the specified transport """ - from fastmcp.server.http import create_streamable_http_app if transport == "streamable-http": return create_streamable_http_app( diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index 9b1528c5d..be860950e 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -119,7 +119,7 @@ def run_nested_server(host: str, port: int) -> None: mount = Starlette(routes=[Mount("/nest-inner", app=mcp_app)]) mount2 = Starlette( routes=[Mount("/nest-outer", app=mount)], - lifespan=mcp_app.router.lifespan_context, + lifespan=mcp_app.lifespan, ) server = uvicorn.Server( config=uvicorn.Config(