Saltar a contenido

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):

server.py
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_stock es un resolutor: una función normal que el SDK ejecuta antes de reserve_book, y cuyo valor devuelto se convierte en el argumento stock.
  • Su parámetro title es el propio argumento title de 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 Stock que 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:

server.py
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_delivery depende de check_stock. El SDK ejecuta el grafo en orden: primero el stock, luego la estimación, luego la herramienta.
  • Tanto stock como delivery necesitan check_stock en ú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:

server.py
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_backorder devuelve un Backorder directamente. 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 Backorder y la inyecta. Tu resolutor nunca toca el protocolo.
  • La herramienta lee backorder.confirm como cualquier otro argumento. Responder no sigue siendo una respuesta: la elicitación se acepta con confirm=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:

server.py
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 del tools/call de 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, elicitation en modo formulario; sampling.tools cuando la solicitud lleva tools o tool_choice).
  • Todo lo que dice el recuadro informativo de arriba sobre las preguntas se aplica sin cambios: una solicitud Sample se 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 en request_state durante 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_context distintos de "none" están ellos mismos obsoletos; evítalos.

Resumen

  • Annotated[T, Resolve(fn)] en un parámetro de herramienta: el SDK ejecuta fn e 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, otro Resolve(...), 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(...) o ListRoots() 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.