Lifespan
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
La mayoría de los servidores reales mantienen algo durante toda su vida: un pool de conexiones a la base de datos, un cliente HTTP, un modelo cargado.
No quieres construirlo en cada llamada, y sí quieres cerrarlo limpiamente. Para eso está el lifespan (ciclo de vida del servidor).
Un lifespan tipado
Un lifespan es un @asynccontextmanager que recibe el servidor y hace yield de un solo objeto. Lo que sea que entregues queda disponible para todos los handlers mientras el servidor esté en ejecución.
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}."
Léelo de abajo hacia arriba:
app_lifespanconecta laDatabaseantes delyieldy la desconecta después, en unfinally. Eso es el arranque y el apagado.- Entrega un
AppContext, una dataclass simple que contiene las cosas que configuraste. Un campo hoy, diez mañana. MCPServer("Bookshop", lifespan=app_lifespan)es todo el cableado necesario.- Dentro de la herramienta, el objeto entregado es
ctx.request_context.lifespan_context.
El lifespan se ejecuta una sola vez. Se entra en él cuando el servidor arranca (antes de la primera solicitud) y se sale cuando el servidor se detiene. Todas las solicitudes intermedias comparten el mismo AppContext.
Info
Si has escrito un lifespan de FastAPI, ya conoces esto. Mismo decorador, mismo yield, mismo finally.
Lo que ve el modelo
Nada nuevo. ctx es un parámetro Context, así que el SDK lo inyecta y nunca llega al esquema de entrada:
{
"type": "object",
"properties": {
"genre": {"title": "Genre", "type": "string"}
},
"required": ["genre"],
"title": "count_booksArguments"
}
genre es el único argumento que el modelo puede pasar. El lifespan es asunto de tu servidor.
Las funciones @mcp.resource() y @mcp.prompt() también pueden recibir un parámetro ctx, escrito como un Context a secas por una razón que se explica en la siguiente sección. Todo lo que lleva ctx está en El Context.
De verdad está tipado
Mira de nuevo la anotación: ctx: Context[AppContext].
Ese único parámetro de tipo es la razón por la que ctx.request_context.lifespan_context es un AppContext para tu verificador de tipos. .db se autocompleta; .dbb es un error antes de que llegues a ejecutar el servidor.
Si escribes un Context a secas, lifespan_context queda tipado como dict[str, Any]: el verificador de tipos no tiene forma de saber qué entregó tu lifespan. El objeto sigue ahí en tiempo de ejecución; lo que pierdes es la ayuda.
Warning
Context[AppContext] es una forma de escribirlo exclusiva de las herramientas. Ponla en una
función @mcp.resource() o @mcp.prompt() y todas las llamadas a ese handler fallan. El cliente
recibe un error, y el log del servidor muestra por qué:
Context is not available outside of a request
En recursos y prompts, escribe ctx: Context a secas. El objeto que entregó tu lifespan
sigue siendo ctx.request_context.lifespan_context en tiempo de ejecución; renuncias al
parámetro de tipo, no al objeto.
Tip
Siempre hay un lifespan. Si no pasas uno, el lifespan por defecto del SDK entrega un dict vacío,
así que ctx.request_context.lifespan_context es {}, nunca None. Ese valor por defecto es también
la razón por la que un Context a secas lo tipa como dict[str, Any].
Míralo en acción
"El arranque se ejecuta antes de la primera solicitud" es el tipo de frase que no deberías tener que creerte sin más.
Reduce el servidor al ciclo de vida: dale a Database un indicador connected, cámbialo en connect() y disconnect(), y añade una herramienta que informe de su valor.
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 vive a nivel de módulo por una sola razón: para que puedas observarla desde fuera del servidor.
Check
Tres momentos, tres valores:
- Antes de que el servidor arranque,
database.connectedesFalse. Importar el módulo no conectó nada. - Mientras está en ejecución, llama a
database_statusy el resultado es"connected". - Detén el servidor y se ejecuta el bloque
finally:database.connectedesFalsede nuevo.
El trabajo ocurrió exactamente donde lo pusiste: alrededor del yield, no al importar ni en cada solicitud.
Resumen
lifespan=recibe un@asynccontextmanagerque recibe el servidor y haceyieldde un solo objeto.- El código anterior al
yieldes el arranque. Elfinallyposterior es el apagado. - Se ejecuta una sola vez, alrededor de toda la vida del servidor, no en cada solicitud.
- Lo que sea que entregues con
yieldesctx.request_context.lifespan_contexten cada herramienta, recurso y prompt. ctx: Context[AppContext]hace que ese acceso esté completamente tipado en las herramientas. Los recursos y prompts reciben elContexta secas.- Sin
lifespan=, obtienes undictvacío, nuncaNone.
Un handler que se detiene a mitad de una llamada para preguntarle al usuario algo que solo él sabe es Elicitación.