Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

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.

Examplelifespan_demo.py, part 1
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.

Examplelifespan_demo.py, part 2
@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.

Examplelifespan_demo.py, part 3
@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.

Example
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())
  • 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=True shows 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 toolIn a lifespan
ConnectsOn every callOnce at startup
SharedNoEvery request shares it
ClosedYou must remember toIn 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.
Watch out
An imported or mounted server does not run its lifespan on import. If lifespan_context is empty, check that the server was started, not only constructed.
Try it yourself
  • 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=lifespan and print what lifespan_context is.
PreviousContext

This is what real progress feels like.