Dependencias
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.
Los argumentos de una herramienta vienen del modelo. Algunos valores nunca deberían: un precio consultado en tus registros, una confirmación que solo una persona puede dar, cualquier cosa que el modelo podría estropear inventándosela.
Las dependencias son parámetros que rellenan tus propias funciones. Anotas el parámetro, nombras la función, y el SDK la llama antes de que se ejecute tu herramienta.
Declara una
Envuelve el tipo del parámetro en Annotated[...] y añade Resolve(fn):
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve
mcp = MCPServer("Bookshop")
INVENTORY = {"Dune": 7, "Neuromancer": 0}
class Stock(BaseModel):
title: str
copies: int
async def check_stock(title: str) -> Stock:
return Stock(title=title, copies=INVENTORY.get(title, 0))
@mcp.tool()
async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) -> str:
"""Reserve a copy of a book."""
if stock.copies == 0:
return f"{title!r} is out of stock."
return f"Reserved {title!r} ({stock.copies - 1} copies left)."
check_stockes un resolutor: una función normal que el SDK ejecuta antes dereserve_book, y cuyo valor devuelto se convierte en el argumentostock.- Su parámetro
titlees el propio argumentotitlede la herramienta, emparejado por nombre. El resolutor ve exactamente el valor validado que verá el cuerpo de la herramienta. - El cuerpo de la herramienta parte de un
Stockque ya existe. Nada de código de consulta en la herramienta, nada de preámbulo del tipo "y si falta".
Info
Si has usado FastAPI, esto es Depends. El mismo mecanismo, por la misma razón: la función declara lo
que necesita, el framework lo proporciona, y el cableado vive en la anotación de tipo.
Invisible para el modelo
Este es el esquema de entrada que tools/list reporta para reserve_book:
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
Una sola propiedad. Igual que el Context en El Context, un parámetro resuelto es un contrato entre tú y el SDK: stock no está en el esquema, al modelo nunca se le habla de él, y a un cliente que envíe un valor stock de todos modos se le ignora. El valor del resolutor es el único que puede recibir tu herramienta.
Esa última parte es la clave. Un parámetro que el modelo no puede proporcionar es un parámetro que el modelo no puede estropear.
Pruébalo
Ejecuta el servidor con el MCP Inspector:
uv run mcp dev server.py
El formulario de reserve_book tiene un único campo title. stock no aparece por ningún lado. Llámala con Dune:
Reserved 'Dune' (6 copies left).
El cuerpo de la herramienta nunca consultó nada: check_stock se ejecutó primero, y el Stock que devolvió llegó como argumento. Prueba con Neuromancer y el mismo resolutor le entrega un cero a la herramienta.
Tip
Podrías simplemente llamar a check_stock(title) en el cuerpo de la herramienta. Decláralo como
dependencia cuando el valor merezca más que una llamada a una función auxiliar: todas las herramientas
que necesitan el stock declaran el mismo parámetro, y el SDK ejecuta el resolutor como mucho una vez por
llamada, sin importar cuántas lo declaren. Las siguientes secciones añaden el resto: resolutores que
dependen unos de otros, y resolutores que preguntan al usuario.
Dependencias de dependencias
Un resolutor puede declarar sus propias dependencias, con la misma anotación:
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve
mcp = MCPServer("Bookshop")
INVENTORY = {"Dune": 7, "Neuromancer": 0}
class Stock(BaseModel):
title: str
copies: int
async def check_stock(title: str) -> Stock:
return Stock(title=title, copies=INVENTORY.get(title, 0))
async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) -> str:
return "tomorrow" if stock.copies > 0 else "in 2-3 weeks"
@mcp.tool()
async def order_book(
title: str,
stock: Annotated[Stock, Resolve(check_stock)],
delivery: Annotated[str, Resolve(estimate_delivery)],
) -> str:
"""Order a book from the shop."""
if stock.copies == 0:
return f"{title!r} is on backorder; it would arrive {delivery}."
return f"Ordered {title!r}; it arrives {delivery}."
estimate_deliverydepende decheck_stock. El SDK ejecuta el grafo en orden: primero el stock, luego la estimación, luego la herramienta.- Tanto
stockcomodeliverynecesitancheck_stocken última instancia, pero se ejecuta una vez por llamada. Una consulta de inventario, dos consumidores. - No hay nada que registrar. El grafo son las anotaciones.
Check
No te creas lo de una vez por llamada sin comprobarlo. Pon un print en check_stock y llama a
order_book desde el Inspector: una línea por llamada. Dos consumidores, una consulta.
El SDK analiza el grafo cuando se registra la herramienta, no cuando se llama. Un parámetro que no puede clasificar (ni un Context, ni un Resolve(...), ni el nombre de un argumento de la herramienta) y un ciclo de resolutores lanzan ambos InvalidSignature al arrancar. El servidor falla antes de que ningún cliente se conecte, con el parámetro o resolutor problemático nombrado en el error.
Los parámetros de un resolutor se resuelven exactamente igual que los de una herramienta: otro Resolve(...), los argumentos de la propia herramienta por nombre, o el Context: ctx.headers, el objeto del lifespan, todo.
Warning
En los transportes HTTP el Context incluye ctx.headers. Las cabeceras son entrada proporcionada
por el cliente, como cualquier argumento de herramienta: bien para una configuración regional o un
feature flag, nunca para una identidad. Quién es el que llama viene de tu capa de autorización
(Autorización), no de una cabecera que cualquiera puede establecer.
Tip
Una vez por llamada significa exactamente eso: el siguiente tools/call ejecuta check_stock otra
vez. Un recurso que debe sobrevivir a una solicitud (un pool de base de datos, un cliente HTTP)
pertenece al Lifespan, y un resolutor puede llegar a él a través de
ctx.request_context.lifespan_context.
Pregunta cuando debas
Un resolutor no tiene por qué saber la respuesta. Puede devolver Elicit(message, Model) y el SDK pregunta al usuario: la maquinaria de Elicitación (elicitation), ejecutada por ti:
from typing import Annotated
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver import Elicit, Resolve
mcp = MCPServer("Bookshop")
INVENTORY = {"Dune": 7, "Neuromancer": 0}
class Stock(BaseModel):
title: str
copies: int
class Backorder(BaseModel):
confirm: bool = Field(description="Order anyway and wait?")
async def check_stock(title: str) -> Stock:
return Stock(title=title, copies=INVENTORY.get(title, 0))
async def confirm_backorder(
title: str,
stock: Annotated[Stock, Resolve(check_stock)],
) -> Backorder | Elicit[Backorder]:
if stock.copies > 0:
return Backorder(confirm=True) # in stock: nothing to ask
return Elicit(f"{title!r} is out of stock (2-3 weeks). Order anyway?", Backorder)
@mcp.tool()
async def order_book(
title: str,
stock: Annotated[Stock, Resolve(check_stock)],
backorder: Annotated[Backorder, Resolve(confirm_backorder)],
) -> str:
"""Order a book from the shop."""
if not backorder.confirm:
return "No order placed."
if stock.copies == 0:
return f"Backordered {title!r}; it ships in 2-3 weeks."
return f"Ordered {title!r}."
- Con stock:
confirm_backorderdevuelve unBackorderdirectamente. Sin pregunta, sin ida y vuelta. Solo se interrumpe al usuario cuando su respuesta importa. - Sin stock: el SDK envía la elicitación, valida la respuesta contra
Backordery la inyecta. Tu resolutor nunca toca el protocolo. - La herramienta lee
backorder.confirmcomo cualquier otro argumento. Responder no sigue siendo una respuesta: la elicitación se acepta conconfirm=False, la herramienta se ejecuta y no se hace ningún pedido. Preguntar se convirtió en una precondición, no en fontanería dentro del cuerpo de la herramienta.
¿Y si el usuario no responde en absoluto, si rechaza la pregunta o la cancela?
Check
Ejecuta order_book para Neuromancer y rechaza la pregunta. Con la anotación escrita como
Annotated[Backorder, Resolve(...)] el cuerpo de la herramienta nunca se ejecuta; la llamada falla con
un resultado de error que el modelo puede leer:
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
Ese es el valor por defecto correcto para una precondición: sin respuesta, no hay pedido. Cuando rechazar es un resultado que tu herramienta quiere manejar (omitir el pedido pendiente pero aun así sugerir otro título), anota ElicitationResult[Backorder] en su lugar y la herramienta recibe el resultado completo de aceptar/rechazar/cancelar para bifurcar según él. Elicitación muestra esa forma, y todo lo demás sobre preguntar: las reglas del esquema, las tres respuestas, el lado del cliente en la conversación.
Info
El framework elige el transporte de la pregunta a partir de la versión del protocolo negociada; el
código de arriba es idéntico en ambas. En 2026-07-28 y posteriores la pregunta viaja dentro de un
tools/call de varias idas y vueltas (multi-round-trip): el servidor la devuelve, el
elicitation_callback del cliente la responde, y el Client reintenta la llamada por ti
(Solicitudes de varias idas y vueltas). En 2025-11-25 y anteriores es
una solicitud de elicitación síncrona a mitad de llamada. Cada pregunta se hace exactamente una vez por
llamada: una garantía sobre la pregunta, no sobre el resolutor. En la forma de varias idas y vueltas
cualquier resolutor puede volver a ejecutarse cada vez que la llamada se reanuda tras una pregunta, así
que el código anterior a un return Elicit(...) se ejecuta en cada una de esas rondas; la respuesta
registrada satisface entonces la pregunta repetida sin volver a preguntar al usuario. Una respuesta
registrada solo se consulta cuando el resolutor pregunta; un resolutor que responde sin preguntar,
como check_stock, siempre proporciona su propio valor calculado. Como cada respuesta se empareja con
su pregunta, un resolutor que elicita debe derivar su pregunta de forma determinista a partir de los
argumentos de la herramienta y las respuestas anteriores. Un valor generado por llamada (un id de
default_factory, una marca de tiempo) se vuelve a derivar en cada ronda y no debe aparecer en una
pregunta a la que la respuesta deba quedar vinculada. Una pregunta construida con datos tan volátiles
hace que toda respuesta registrada parezca obsoleta, así que el servidor la vuelve a hacer en cada
ronda hasta que el límite de rondas del cliente termina la llamada.
Pregunta al cliente, no al usuario
La elicitación es una de las tres preguntas que puede hacer un resolutor, y el flujo de varias idas y vueltas no permite otras. Las otras dos van al cliente en lugar de al usuario: devuelve Sample(...) para ejecutar una llamada a un LLM a través del cliente (una solicitud sampling/createMessage), o ListRoots() para obtener los roots actuales del cliente. Ninguna tiene un resultado de aceptar/rechazar; el consumidor anota el tipo de resultado directamente, CreateMessageResult (CreateMessageResultWithTools cuando la solicitud lleva tools o tool_choice) o ListRootsResult:
from typing import Annotated
from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample
from mcp.types import CreateMessageResult, SamplingMessage, TextContent
mcp = MCPServer("Bookshop")
def suggest_title(genre: str) -> Sample:
prompt = f"Suggest one {genre} book title. Answer with the title only."
return Sample(
[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=50,
)
@mcp.tool()
async def recommend_book(
genre: str,
suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
) -> str:
"""Recommend a book in the given genre."""
title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
return f"Today's {genre} pick: {title}"
- El framework las enruta exactamente igual que
Elicit: dentro deltools/callde varias idas y vueltas en 2026-07-28, sobre la solicitud independiente servidor->cliente en 2025-11-25. Una capacidad no declarada rechaza la llamada con un error de protocolo-32021(sampling,roots,elicitationen modo formulario;sampling.toolscuando la solicitud llevatoolsotool_choice). - Todo lo que dice el recuadro informativo de arriba sobre las preguntas se aplica sin cambios: una solicitud
Samplese empareja con su resultado registrado por su representación exacta, así que constrúyela de forma determinista a partir de los argumentos de la herramienta y las respuestas anteriores; el cliente paga entonces la llamada al LLM una vez por llamada a la herramienta, no una vez por ronda. El resultado registrado viaja enrequest_statedurante el resto de la llamada, así que una respuesta del modelo muy grande hace más pesada cada ida y vuelta restante. - Las funcionalidades independientes de muestreo (sampling) y roots quedan obsoletas en 2026-07-28 (SEP-2577). Los servidores nuevos que necesitan el modelo del cliente preguntan a través de este mecanismo; los que no, deberían integrarse directamente con un proveedor de LLM. Los valores de
include_contextdistintos de"none"están ellos mismos obsoletos; evítalos.
Resumen
Annotated[T, Resolve(fn)]en un parámetro de herramienta: el SDK ejecutafne inyecta su valor devuelto.- Un parámetro resuelto es invisible para el modelo y un cliente no puede proporcionarlo. Los valores que el modelo no debe inventar (precios, identidades, permisos) van aquí.
- Los parámetros de un resolutor se resuelven del mismo modo: el
Context, otroResolve(...), o un argumento de la herramienta por nombre. El grafo ejecuta cada resolutor como mucho una vez por ronda, tenga los consumidores que tenga; cada pregunta se hace exactamente una vez, y cualquier resolutor puede volver a ejecutarse cuando una llamada se reanuda tras una pregunta. - Los grafos incorrectos fallan en el registro con
InvalidSignature, no a mitad de llamada. - Devuelve
Elicit(message, Model)para preguntar al usuario, solo cuando tengas que hacerlo. Las anotaciones sin envolver abortan al rechazar;ElicitationResult[T]permite a la herramienta bifurcar. - Devuelve
Sample(...)oListRoots()para pedir al cliente una respuesta del modelo o la lista de roots; se inyecta el resultado sin más.
El estado que tu servidor construye una vez al arrancar, y cómo llega a él un handler, es la página de Lifespan.