Elicitación
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.
Una herramienta que va por la mitad de su trabajo y a la que le falta una respuesta no tiene por qué fallar.
La elicitación (elicitation) le permite preguntar. En medio de una llamada a la herramienta, el usuario recibe una pregunta y su respuesta vuelve a la misma llamada de función.
Hay dos modos:
- Modo formulario: necesitas un valor (una confirmación, una fecha, una cantidad). Describes los campos y el cliente muestra el formulario.
- Modo URL: necesitas que el usuario vaya a otro sitio (una pantalla de consentimiento OAuth, una página de pago). Nada de lo que haga allí pasa por el protocolo.
Y hay dos formas de preguntar. La que conviene usar es un resolutor: cuelgas la pregunta de un parámetro y el SDK pregunta, en cualquier conexión, sea cual sea la generación del protocolo que hable el cliente. La forma directa, await ctx.elicit(...), es una solicitud del servidor al cliente, un canal que solo existe para un cliente en una conexión heredada (versión de la especificación 2025-11-25 o anterior). Ambas están en esta página; empieza por el resolutor.
Preguntar con un resolutor
Una pregunta que condiciona toda la herramienta (¿estás seguro?, ¿cuál de las tres cuentas que coinciden?) puede sacarse del cuerpo de la herramienta a un resolutor, y el framework la hace por ti.
Un parámetro anotado como Annotated[T, Resolve(fn)] se rellena ejecutando fn antes del cuerpo de la herramienta. El resolutor devuelve el valor directamente cuando ya lo conoce, o devuelve Elicit(...) para que el framework pregunte:
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import (
AcceptedElicitation,
CancelledElicitation,
DeclinedElicitation,
Elicit,
ElicitationResult,
Resolve,
)
mcp = MCPServer("Files")
_FOLDERS: dict[str, list[str]] = {"/tmp/empty": [], "/tmp/project": ["main.py", "README.md"]}
class Confirm(BaseModel):
ok: bool
async def confirm_delete(path: str) -> Confirm | Elicit[Confirm]:
"""Resolver: ask for confirmation only when the folder is not empty."""
file_count = len(_FOLDERS.get(path, []))
if file_count == 0:
return Confirm(ok=True) # nothing to confirm, no round-trip to the client
return Elicit(f"{path} has {file_count} file(s). Delete anyway?", Confirm)
@mcp.tool()
async def delete_folder(
path: str,
confirm: Annotated[ElicitationResult[Confirm], Resolve(confirm_delete)],
) -> str:
"""Delete a folder, asking for confirmation when it is not empty."""
match confirm:
case AcceptedElicitation(data=Confirm(ok=True)):
_FOLDERS.pop(path, None)
return f"deleted {path}"
case AcceptedElicitation():
return "kept the folder"
case DeclinedElicitation():
return "declined: folder not deleted"
case CancelledElicitation():
return "cancelled: folder not deleted"
confirm_deletelee por nombre el propio argumentopathde la herramienta, lista la carpeta y solo pregunta cuando debe: una carpeta vacía se resuelve aConfirm(ok=True)sin ninguna ida y vuelta al cliente.delete_folderanotaElicitationResult[Confirm], así que el framework inyecta el resultado completo y la herramienta usamatchpara cubrir cada caso: aceptar y confirmar, aceptar pero conservar (ok=False), rechazar, cancelar.- El parámetro
confirmnunca aparece en el esquema de entrada de la herramienta: el cliente aportapath, el resolutor aportaconfirm.
Anota en su lugar el modelo sin envolver (Annotated[Confirm, Resolve(confirm_delete)]) cuando la herramienta no necesita bifurcar: recibe el modelo si el usuario acepta y la llamada se interrumpe con un error si rechaza o cancela.
Un resolutor funciona en todas las conexiones. A un cliente en una conexión heredada, el SDK le envía la pregunta directamente; en una conexión 2026-07-28, el SDK devuelve la pregunta desde la llamada y el siguiente intento del cliente lleva la respuesta. Tu resolutor nunca nota la diferencia; lo que ocurre por debajo está en Solicitudes de varias idas y vueltas (multi-round-trip).
Preguntar es solo una de las cosas que puede hacer un resolutor. El mecanismo general (dependencias que calculan sin preguntar, dependencias de dependencias, qué puede aportar el modelo y qué no) está en la página Dependencias.
Preguntar desde dentro de la herramienta
Una herramienta también puede detenerse en medio de su propio cuerpo y preguntar.
Warning
ctx.elicit() y ctx.elicit_url() son solicitudes del servidor al cliente: un
canal que solo existe para un cliente en una conexión heredada (versión de la especificación
2025-11-25 o anterior). En una conexión 2026-07-28 no hay solicitudes iniciadas por el
servidor, así que estas llamadas fallan. Un resolutor funciona en ambas.
Versiones del protocolo tiene todos los detalles.
await ctx.elicit() recibe un mensaje y un modelo de Pydantic:
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Bistro")
class AlternativeDate(BaseModel):
accept_alternative: bool = Field(description="Try another date?")
date: str = Field(default="2025-12-26", description="Alternative date (YYYY-MM-DD)")
@mcp.tool()
async def book_table(date: str, party_size: int, ctx: Context) -> str:
"""Book a table at the bistro."""
if date != "2025-12-25":
return f"Booked a table for {party_size} on {date}."
result = await ctx.elicit(
message=f"No tables for {party_size} on {date}. Would you like to try another date?",
schema=AlternativeDate,
)
if result.action == "accept" and result.data.accept_alternative:
return await book_table(result.data.date, party_size, ctx)
return "No booking made."
- El parámetro
Contextes lo que te dactx.elicit; cualquier herramienta puede recibir uno. Ese objeto tiene su propia página: El Context. AlternativeDatees el esquema de la respuesta que quieres.- La herramienta es
async def. Tiene que serlo: se detiene a mitad y espera a una persona. - En cualquier otra fecha la herramienta devuelve enseguida. Solo pregunta cuando tiene que hacerlo.
- La fecha que acepta el usuario vuelve a pasar por el propio
book_table. Una respuesta es una entrada como cualquier otra: una alternativa que también está completa provoca una nueva pregunta, no se confirma a ciegas.
Qué recibe el cliente
El cliente recibe tu mensaje y, junto a él, un JSON Schema generado a partir del modelo:
{
"properties": {
"accept_alternative": {
"description": "Try another date?",
"title": "Accept Alternative",
"type": "boolean"
},
"date": {
"default": "2025-12-26",
"description": "Alternative date (YYYY-MM-DD)",
"title": "Date",
"type": "string"
}
},
"required": ["accept_alternative"],
"title": "AlternativeDate",
"type": "object"
}
Ese esquema es el formulario. Field(description=...) es la etiqueta; un valor por defecto rellena el campo de antemano y lo hace opcional. Es la misma maquinaria de Pydantic a JSON Schema que Herramientas describe para los argumentos de una herramienta.
Warning
Un esquema de elicitación no es tan expresivo como el esquema de entrada de una herramienta.
Solo campos planos y primitivos: str, int, float, bool o un Literal de cadenas (se
convierte en un enum). Pon un modelo dentro del modelo y ctx.elicit lanza una excepción
antes de que se envíe nada al cliente:
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
Estás interrumpiendo a una persona en plena tarea. Si la respuesta necesita anidamiento, debería haber sido un argumento de la herramienta.
Las tres respuestas
result.action te dice qué hizo el usuario, y hay exactamente tres posibilidades:
"accept": envió el formulario.result.dataes una instancia deAlternativeDate, ya validada."decline": dijo que no."cancel": descartó la pregunta sin elegir.
result.data solo existe con "accept", y por eso el ejemplo comprueba primero result.action. Tu verificador de tipos impone el orden: después de result.action == "accept", result.data es un AlternativeDate; antes, no hay ningún .data.
Una negativa no es un error. La herramienta decide qué significa rechazar (aquí, no hay reserva) y responde al modelo con normalidad.
Tip
La respuesta se valida contra tu modelo antes de que tu código la vea. Un cliente que envía
"maybe" para un bool no corrompe tu reserva: la llamada falla con un error de
discrepancia de esquema y tu if nunca se ejecuta.
Enviar al usuario a una URL
Algunas cosas no deben pasar por el modelo ni por el cliente: credenciales, números de tarjeta, consentimiento OAuth. Para esas no pides datos; pides al usuario que vaya a algún sitio:
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Bistro")
@mcp.tool()
async def pay_deposit(booking_id: str, ctx: Context) -> str:
"""Take the deposit that confirms a booking."""
result = await ctx.elicit_url(
message="A 20 EUR deposit confirms your booking.",
url=f"https://pay.example.com/deposit/{booking_id}",
elicitation_id=f"deposit-{booking_id}",
)
if result.action == "accept":
return "Complete the payment in your browser."
return "No deposit taken. The booking expires in one hour."
@mcp.tool()
async def confirm_deposit(booking_id: str, ctx: Context) -> str:
"""Record a payment reported by the payment provider."""
await ctx.session.send_elicit_complete(f"deposit-{booking_id}")
return f"Deposit received for booking {booking_id}."
ctx.elicit_url()recibe el mensaje, la URL que hay que visitar y unelicitation_idque eliges tú: cualquier cadena que identifique esta elicitación dentro de tu servidor.- El resultado tiene una acción y nada más.
"accept"significa que el usuario aceptó abrir la URL, no que haya terminado lo que hay al otro lado. - El pago ocurre fuera de banda, entre el navegador del usuario y tu proveedor de pagos. Ningún contenido vuelve nunca a través de MCP.
Fíjate en la segunda herramienta. Cuando el servidor se entera de que el flujo fuera de banda terminó (un webhook, un sondeo; aquí se modela como una segunda herramienta), ctx.session.send_elicit_complete(...) envía notifications/elicitation/complete con el mismo elicitation_id. Así es como el cliente sabe que puede dejar de mostrar "waiting for payment...". Sin eso, el cliente solo puede adivinar.
El lado del cliente
Los servidores preguntan. Los clientes responden pasando un elicitation_callback a Client(...):
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitRequestURLParams, ElicitResult
async def handle_elicitation(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
if isinstance(params, ElicitRequestURLParams):
print(f"Open this link to continue: {params.url}")
return ElicitResult(action="accept")
print(params.message)
return ElicitResult(action="accept", content={"accept_alternative": True, "date": "2025-12-27"})
async def main() -> None:
async with Client(
"http://127.0.0.1:8000/mcp",
mode="legacy",
elicitation_callback=handle_elicitation,
) as client:
result = await client.call_tool("book_table", {"date": "2025-12-25", "party_size": 2})
print(result.content)
- Un solo callback maneja ambos modos.
paramses una unión deElicitRequestFormParamsyElicitRequestURLParams;isinstancees la bifurcación. - Para una URL, muestras
params.urlal usuario y devuelves la acción que eligió. Nunca ningúncontent. - Para un formulario, una aplicación real muestra
params.requested_schemay devuelve la entrada del usuario comocontent. Este siempre dice que sí con una respuesta predefinida, que es justo el callback que quieres en una prueba. - Pasar el callback es también la declaración de capacidad: es como el servidor se entera de que a este cliente se le puede preguntar. Las demás cosas que un cliente puede responder a un servidor están en Callbacks del cliente.
Info
La elicitación es una solicitud del servidor al cliente, y esas solo existen en una
sesión con handshake clásico, por eso este cliente pasa mode="legacy".
En una conexión 2026-07-28, una herramienta pregunta devolviendo la pregunta desde la
llamada; ese flujo está en Solicitudes de varias idas y vueltas.
Pruébalo
Arranca el server.py del modo formulario con ctx.elicit (el de book_table) sobre Streamable HTTP (Ejecutar tu servidor tiene el comando de una línea), luego ejecuta el main() del cliente y pide a book_table el día de Navidad.
El callback imprime la pregunta que recibió:
No tables for 2 on 2025-12-25. Would you like to try another date?
Responde con {"accept_alternative": True, "date": "2025-12-27"}, y la herramienta, que ha estado esperando dentro de await ctx.elicit(...) todo este tiempo, termina la reserva:
Booked a table for 2 on 2025-12-27.
Ahora cambia al server.py del modo URL y apunta el mismo main() a pay_deposit: el mismo callback toma la otra rama, imprime el enlace de pago y la herramienta vuelve con "Complete the payment in your browser." Una ida y vuelta, en mitad de la llamada, en ambos sentidos.
Check
Ahora quita elicitation_callback= del Client y vuelve a llamar a book_table para el día
de Navidad. Toda la llamada falla con un error de protocolo:
Elicitation not supported
Un cliente que no registró ningún callback nunca declaró la capacidad elicitation, así que
no hay nadie a quien preguntar. Tu herramienta no recibió un "decline"; recibió una
excepción. Diseña para ello: toda elicitación necesita una respuesta sensata a "¿y si no
puedo preguntar?".
Resumen
- Un parámetro anotado como
Annotated[T, Resolve(fn)]lo rellena un resolutor, que devuelveElicit(...)cuando tiene que preguntar. Funciona en todas las conexiones. - El esquema es un modelo plano de Pydantic: solo campos primitivos, validados al volver.
result.actiones"accept","decline"o"cancel";result.datasolo existe en accept.await ctx.elicit(message, schema=Model)pregunta desde dentro del cuerpo de la herramienta, yawait ctx.elicit_url(message, url, elicitation_id)es para todo lo que no debe pasar por el modelo (ctx.session.send_elicit_complete(elicitation_id)indica que la parte fuera de banda terminó). Ambas son solicitudes del servidor al cliente: necesitan al cliente en una conexión heredada.- El cliente responde con un solo
elicitation_callback, bifurcando según el tipo de params; registrarlo es lo que declara la capacidad. - En una conexión 2026-07-28 el servidor devuelve la pregunta en lugar de enviarla; el mismo callback se alimenta desde Solicitudes de varias idas y vueltas.
Todo lo que hay debajo de ese retorno (el bucle de reintentos, proteger requestState, manejarlo tú mismo) está en Solicitudes de varias idas y vueltas.