Lifespan
A lifespan is a startup and shutdown hook that opens a shared resource once when the server starts and closes it when the server stops.
Last updated: 29 Sep, 2026 · MCP 2.2
This lesson's server goes in a file of its own, lifespan_demo.py, and shop.py stays as it is.
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
class Database:
def __init__(self):
self.connected = False
self.orders = {"A17": "shipped", "B42": "processing"}
async def connect(self):
self.connected = True
print("database connected")
async def disconnect(self):
self.connected = False
print("database disconnected")A stand-in database: it only flips a flag and prints, so you can see exactly when it connects. A real one would open a connection pool here.
@dataclass
class AppContext:
db: Database
@asynccontextmanager
async def lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
db = Database()
await db.connect()
try:
yield AppContext(db=db)
finally:
await db.disconnect()
mcp = MCPServer("Shop support", lifespan=lifespan)A lifespan is an @asynccontextmanager function. Code before yield runs at startup and the finally block at shutdown. The object it yields, here an AppContext holding the database, is shared by every request.
@mcp.tool()
def order_status(order_id: str, ctx: Context[AppContext]) -> str:
"""Look up an order's status in the database."""
db = ctx.request_context.lifespan_context.db
return f"{order_id}: {db.orders.get(order_id, 'unknown')} (connected={db.connected})"ctx.request_context.lifespan_context is the yielded object. Context[AppContext] tells your editor its type, so .db autocompletes. This typed form works in tools; resources and prompts take a bare Context.
import asyncio
from mcp import Client
from lifespan_demo import mcp
async def main():
async with Client(mcp) as client:
print("inside the client block")
for order_id in ("A17", "B42"):
result = await client.call_tool("order_status", {"order_id": order_id})
print(result.content[0].text)
print("leaving the client block")
asyncio.run(main())database connected inside the client block A17: shipped (connected=True) B42: processing (connected=True) leaving the client block database disconnected
- The database connected once, when the client connected and the in-memory server started, before the first line inside the block.
- Both calls shared it, and
connected=Trueshows each request read the object the lifespan yielded. - It disconnected after the block, when the server shut down. Over HTTP the lifespan lasts as long as the process.
A lifespan vs connecting in the tool
| In the tool | In a lifespan | |
|---|---|---|
| Connects | On every call | Once at startup |
| Shared | No | Every request shares it |
| Closed | You must remember to | In the finally block at shutdown |
When a lifespan pays off
- A database or connection pool that is slow to open and safe to share.
- A cache or model client built once and read on every call.
- Anything that must be closed cleanly when the server stops.
lifespan_context is empty, check that the server was started, not only constructed.Related
- Previous: Context
- Next: Progress notifications
- Add a second tool that counts
db.orders, and call both. - Move
await db.connect()into the tool and watch how often it prints. - Remove
lifespan=lifespanand print whatlifespan_contextis.
This is what real progress feels like.