# Lifespan

Most real servers hold something for their whole life: a database pool, an HTTP client, a loaded model.

You don't want to build it on every call, and you do want to close it cleanly. That's what the **lifespan** is for.

## A typed lifespan

A lifespan is an `@asynccontextmanager` that receives the server and `yield`s **one object**. Whatever you yield is available to every handler for as long as the server runs.

```python title="server.py" hl_lines="25-31 34 38 40"
# docs_src/lifespan/tutorial001.py
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:
    @classmethod
    async def connect(cls) -> "Database":
        return cls()

    async def disconnect(self) -> None: ...

    def query(self) -> int:
        return 3


@dataclass
class AppContext:
    db: Database


@asynccontextmanager
async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
    db = await Database.connect()
    try:
        yield AppContext(db=db)
    finally:
        await db.disconnect()


mcp = MCPServer("Bookshop", lifespan=app_lifespan)


@mcp.tool()
def count_books(genre: str, ctx: Context[AppContext]) -> str:
    """Count the books in a genre."""
    db = ctx.request_context.lifespan_context.db
    return f"{db.query()} books in {genre!r}."
```

Read it bottom-up:

* `app_lifespan` connects the `Database` **before** the `yield` and disconnects it **after**, in a `finally`. That's startup and shutdown.
* It yields an `AppContext`, a plain dataclass holding the things you set up. One field today, ten tomorrow.
* `MCPServer("Bookshop", lifespan=app_lifespan)` is the whole wiring.
* Inside the tool, the yielded object is `ctx.request_context.lifespan_context`.

The lifespan runs **once**. It is entered when the server starts (before the first request) and exited when the server stops. Every request in between shares the same `AppContext`.

!!! info
    If you've written a FastAPI `lifespan`, you already know this. Same decorator, same `yield`, same `finally`.

### What the model sees

Nothing new. `ctx` is a **Context** parameter, so the SDK injects it and it never reaches the input schema:

```json
{
  "type": "object",
  "properties": {
    "genre": {"title": "Genre", "type": "string"}
  },
  "required": ["genre"],
  "title": "count_booksArguments"
}
```

`genre` is the only argument the model can pass. The lifespan is your server's business.

`@mcp.resource()` and `@mcp.prompt()` functions can take a `ctx` parameter too, written as a bare `Context` for a reason the next section gets to. Everything `ctx` carries is in **[The Context](https://py.sdk.modelcontextprotocol.io/handlers/context/index.md)**.

### It really is typed

Look at the annotation again: `ctx: Context[AppContext]`.

That one type parameter is why `ctx.request_context.lifespan_context` **is** an `AppContext` to your type checker. `.db` autocompletes; `.dbb` is an error before you ever run the server.

Write a bare `Context` instead and `lifespan_context` is typed as `dict[str, Any]`: the type checker has no way to know what your lifespan yielded. The object is still there at runtime; you've lost the help.

!!! warning
    `Context[AppContext]` is a **tool-only** spelling. Put it on an `@mcp.resource()` or
    `@mcp.prompt()` function and every call to that handler fails. The client gets an error back,
    and the server log shows why:

    ```text
    Context is not available outside of a request
    ```

    In resources and prompts, write the bare `ctx: Context`. The object your lifespan yielded is
    still `ctx.request_context.lifespan_context` at runtime; you give up the type parameter, not
    the object.

!!! tip
    There is always a lifespan. If you don't pass one, the SDK's default yields an empty `dict`,
    so `ctx.request_context.lifespan_context` is `{}`, never `None`. That default is also why a
    bare `Context` types it as `dict[str, Any]`.

## Watch it happen

"Startup runs before the first request" is the kind of sentence you should not have to take on faith.

Strip the server down to the lifecycle: give `Database` a `connected` flag, flip it in `connect()` and `disconnect()`, and add a tool that reports it.

```python title="server.py" hl_lines="11 14 17 25 44"
# docs_src/lifespan/tutorial002.py
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) -> None:
        self.connected = False

    async def connect(self) -> None:
        self.connected = True

    async def disconnect(self) -> None:
        self.connected = False


@dataclass
class AppContext:
    db: Database


database = Database()


@asynccontextmanager
async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
    await database.connect()
    try:
        yield AppContext(db=database)
    finally:
        await database.disconnect()


mcp = MCPServer("Bookshop", lifespan=app_lifespan)


@mcp.tool()
def database_status(ctx: Context[AppContext]) -> str:
    """Report whether the database connection is up."""
    db = ctx.request_context.lifespan_context.db
    return "connected" if db.connected else "disconnected"
```

`database` lives at module level for one reason: so you can look at it from *outside* the server.

!!! check
    Three moments, three values:

    * Before the server starts, `database.connected` is `False`. Importing the module connected nothing.
    * While it's running, call `database_status` and the result is `"connected"`.
    * Stop the server and the `finally` block runs: `database.connected` is `False` again.

    The work happened exactly where you put it: around the `yield`, not at import time and not per request.

## Recap

* `lifespan=` takes an `@asynccontextmanager` that receives the server and `yield`s one object.
* Code before the `yield` is startup. The `finally` after it is shutdown.
* It runs once, around the whole life of the server, not per request.
* Whatever you `yield` is `ctx.request_context.lifespan_context` in every tool, resource, and prompt.
* `ctx: Context[AppContext]` makes that access fully typed in tools. Resources and prompts take the bare `Context`.
* No `lifespan=` means an empty `dict`, never `None`.

A handler that stops mid-call to ask the user for something only they know is **[Elicitation](https://py.sdk.modelcontextprotocol.io/handlers/elicitation/index.md)**.
