"""Lifespan utilities for combining async context manager lifespans.""" from __future__ import annotations from collections.abc import AsyncIterator, Callable, Mapping from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager from typing import Any, TypeVar AppT = TypeVar("AppT") def combine_lifespans( *lifespans: Callable[[AppT], AbstractAsyncContextManager[Mapping[str, Any] | None]], ) -> Callable[[AppT], AbstractAsyncContextManager[dict[str, Any]]]: """Combine multiple lifespans into a single lifespan. Useful when mounting FastMCP into FastAPI and you need to run both your app's lifespan and the MCP server's lifespan. Works with both FastAPI-style lifespans (yield None) and FastMCP-style lifespans (yield dict). Results are merged; later lifespans override earlier ones on key conflicts. Lifespans are entered in order and exited in reverse order (LIFO). Example: ```python from fastmcp import FastMCP from fastmcp.utilities.lifespan import combine_lifespans from fastapi import FastAPI mcp = FastMCP("Tools") mcp_app = mcp.http_app() app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan)) app.mount("/mcp", mcp_app) # MCP endpoint at /mcp ``` Args: *lifespans: Lifespan context manager factories to combine. Returns: A combined lifespan context manager factory. """ @asynccontextmanager async def combined(app: AppT) -> AsyncIterator[dict[str, Any]]: merged: dict[str, Any] = {} async with AsyncExitStack() as stack: for ls in lifespans: result = await stack.enter_async_context(ls(app)) if result is not None: merged.update(result) yield merged return combined