What happened?
FastMCP's lifespan manager is reference-counted so multiple in-process client sessions can share server resources. With two independent Client(server) contexts, if the first client exits while the second remains connected, the first context still closes the user lifespan. A later tool call through the second client can then observe a resource that its lifespan already closed.
Reproduction
- Create a
FastMCP server whose lifespan yields a mutable resource and closes it in finally; add a tool that reports whether the resource is open.
- Enter
Client(server) A and then enter a separate Client(server) B while A remains active.
- Exit A while B remains connected, then call the tool through B.
The local reproduction uses the supported in-memory Client transport. In three fresh processes, B reported resource_open=false and active=false after A exited, while the lifespan manager still reported _lifespan_result_set=true and a positive reference count.
Additional cold-start case
If two first clients start together while the async user lifespan is suspended during setup, both can enter startup before _lifespan_result_set becomes true. Three fresh-process reproductions observed two user-lifespan entries and a final _lifespan_ref_count of -1 after all client contexts closed.
Expected behavior
The user/provider lifespan should start once and remain open until the final overlapping client context exits, regardless of entry or exit order. Concurrent first entries should share one startup, and the manager's reference count should return to zero after shutdown.
This appears to be an uncovered edge in merged PR #3415, which added reference counting to prevent early teardown across overlapping in-process sessions. Its regression test closes the nested second client first; it does not cover the first owner exiting before a later client or overlapping cold-start setup.
Example Code
Version Information
Observed on FastMCP main at `5baeacfe20eca735cb949564b4915f93a622b916`.
What happened?
FastMCP's lifespan manager is reference-counted so multiple in-process client sessions can share server resources. With two independent
Client(server)contexts, if the first client exits while the second remains connected, the first context still closes the user lifespan. A later tool call through the second client can then observe a resource that its lifespan already closed.Reproduction
FastMCPserver whose lifespan yields a mutable resource and closes it infinally; add a tool that reports whether the resource is open.Client(server)A and then enter a separateClient(server)B while A remains active.The local reproduction uses the supported in-memory Client transport. In three fresh processes, B reported
resource_open=falseandactive=falseafter A exited, while the lifespan manager still reported_lifespan_result_set=trueand a positive reference count.Additional cold-start case
If two first clients start together while the async user lifespan is suspended during setup, both can enter startup before
_lifespan_result_setbecomes true. Three fresh-process reproductions observed two user-lifespan entries and a final_lifespan_ref_countof-1after all client contexts closed.Expected behavior
The user/provider lifespan should start once and remain open until the final overlapping client context exits, regardless of entry or exit order. Concurrent first entries should share one startup, and the manager's reference count should return to zero after shutdown.
This appears to be an uncovered edge in merged PR #3415, which added reference counting to prevent early teardown across overlapping in-process sessions. Its regression test closes the nested second client first; it does not cover the first owner exiting before a later client or overlapping cold-start setup.
Example Code
Version Information