Lifespan: a database the whole server shares
Connecting to a database on every call is slow. A lifespan connects once when the server starts and disconnects when it stops.
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; the SDK docs note that this typed form works in tools, and resources and prompts take a bare Context.
import asyncio
from mcp import Client
from shop 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())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. It disconnected after the client block ended, when the in-memory server shut down. Over HTTP, the lifespan lasts as long as the server process.
- 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.
You understood something today that you didn't yesterday.