Skip to content

New lifespan for every session? #166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
strawgate opened this issue Apr 15, 2025 · 3 comments
Open

New lifespan for every session? #166

strawgate opened this issue Apr 15, 2025 · 3 comments

Comments

@strawgate
Copy link
Contributor

It seems like maybe the server is invoking a new lifespan for every sse connection?

I think it should only be invoking the lifespan once but providing the yielded objects in the context to each request?

@jlowin
Copy link
Owner

jlowin commented Apr 15, 2025

Can you please provide an MRE? I am not sure if that is behavior caused by FastMCP or MCP (or both, if a FastMCP client is dynamically invoking a low level server). FastMCP's only interaction with the lifespan object is to pass it directly to the low-level server.

@strawgate
Copy link
Contributor Author

strawgate commented Apr 15, 2025

@asynccontextmanager
async def lifespan(app):
    yield "lifespan"

mcp = FastMCP(name="Parent MCP Server", lifespan=lifespan)

if __name__ == "__main__":
    mcp.run(transport="sse")

Run via sse, set a breakpoint in the lifespan context manager and then open MCP Inspector and connect. The breakpoint will not hit after running the server.

The breakpoint will hit when you connect, and every time you press reconnect.

i.e. it looks like the lifespan is only used when a user connects and is not actually for server startup / shutdown?

@strawgate
Copy link
Contributor Author

strawgate commented Apr 15, 2025

I may have lied about it not hitting after starting the server, I am seeing it hit when starting the server and for each connection.


    def sse_app(self) -> Starlette:
        """Return an instance of the SSE server app."""
        sse = SseServerTransport(self.settings.message_path)

        async def handle_sse(request: Request) -> None:
            async with sse.connect_sse(
                request.scope,
                request.receive,
                request._send,  # type: ignore[reportPrivateUsage]
            ) as streams:
                await self._mcp_server.run(
                    streams[0],
                    streams[1],
                    self._mcp_server.create_initialization_options(),
                )

        return Starlette(
            debug=self.settings.debug,
            routes=[
                Route(self.settings.sse_path, endpoint=handle_sse),
                Mount(self.settings.message_path, app=sse.handle_post_message),
            ],
        )

It looks like mcp_server.run() is calling run for every sse session and every call to run invokes the lifespan context manager again.

It looks like this is the intended behavior, I would have expected it to be called a server session or something similar instead of a server but it seems like if we want to share things, like connections to databases, across sessions the lifespan is not the way to do that and that the lifespan should only be used for per-session dependencies

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants