Elicitação
Esta página foi traduzida por máquina
As traduções desta documentação são geradas automaticamente a partir das páginas em inglês, e a versão em inglês desta página é a oficial.
Encontrou um problema na tradução? Veja como as traduções funcionam e como reportar um problema.
Uma ferramenta que está na metade do trabalho e à qual falta uma resposta não precisa falhar.
Elicitação (elicitation) permite que ela pergunte. No meio de uma chamada de ferramenta, o usuário recebe uma pergunta, e a resposta dele volta para a mesma chamada de função.
Há dois modos:
- Modo formulário: você precisa de um valor (uma confirmação, uma data, uma quantidade). Você descreve os campos e o cliente renderiza o formulário.
- Modo URL: você precisa que o usuário vá a outro lugar (uma tela de consentimento OAuth, uma página de pagamento). Nada do que ele faz lá passa pelo protocolo.
E há duas formas de perguntar. A que você deve escolher é um resolvedor: você pendura a pergunta em um parâmetro e o SDK pergunta - em qualquer conexão, seja qual for a era do protocolo que o cliente fala. A forma direta, await ctx.elicit(...), é uma requisição do servidor para o cliente, um canal que só existe para um cliente em uma conexão legada (versão da especificação 2025-11-25 ou anterior). As duas estão nesta página; comece pelo resolvedor.
Pergunte com um resolvedor
Uma pergunta que controla a ferramenta inteira - tem certeza? qual das três contas correspondentes? - pode sair do corpo da ferramenta e virar um resolvedor, e o framework pergunta por você.
Um parâmetro anotado com Annotated[T, Resolve(fn)] é preenchido executando fn antes do corpo da ferramenta. O resolvedor retorna o valor direto quando já o conhece, ou retorna Elicit(...) para que o framework pergunte:
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_deletelê pelo nome o próprio argumentopathda ferramenta, lista a pasta e só elicita quando é necessário - uma pasta vazia resolve paraConfirm(ok=True)sem nenhuma ida e volta até o cliente.delete_folderanotaElicitationResult[Confirm], então o framework injeta o resultado inteiro e a ferramenta fazmatchem todos os casos: aceitar-e-confirmar, aceitar-mas-manter (ok=False), recusar, cancelar.- O parâmetro
confirmnunca aparece no schema de entrada da ferramenta - o cliente fornecepath, o resolvedor fornececonfirm.
Anote o modelo sem o invólucro (Annotated[Confirm, Resolve(confirm_delete)]) quando a ferramenta não precisar ramificar: ela recebe o modelo no accept e a chamada aborta com um erro no decline ou no cancel.
Um resolvedor funciona em todas as conexões. Para um cliente em uma conexão legada, o SDK envia a pergunta diretamente; em uma conexão 2026-07-28, o SDK retorna a pergunta a partir da chamada, e a próxima tentativa do cliente carrega a resposta. Seu resolvedor nunca percebe a diferença; o que acontece por baixo dos panos é Requisições com múltiplas idas e voltas.
Perguntar é só uma das coisas que um resolvedor pode fazer. O mecanismo geral - dependências que computam sem perguntar, dependências de dependências, o que o modelo pode e não pode fornecer - está na página Dependências.
Pergunte de dentro da ferramenta
Uma ferramenta também pode parar no meio do próprio corpo e perguntar.
Warning
ctx.elicit() e ctx.elicit_url() são requisições do servidor para o cliente - um
canal que só existe para um cliente em uma conexão legada (versão da especificação 2025-11-25
ou anterior). Em uma conexão 2026-07-28 não existem requisições iniciadas pelo servidor, então
essas chamadas falham. Um resolvedor funciona nas duas. Versões do protocolo
tem a história completa.
await ctx.elicit() recebe uma mensagem e um modelo 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."
- O parâmetro
Contexté o que dá acesso actx.elicit; qualquer ferramenta pode receber um. Esse objeto tem uma página só dele: O Context. AlternativeDateé o schema da resposta que você quer.- A ferramenta é
async def. Tem que ser: ela para no meio e espera por uma pessoa. - Em qualquer outra data, a ferramenta retorna na hora. Ela só pergunta quando precisa.
- A data que o usuário aceita volta pela própria
book_table. Uma resposta é entrada como qualquer outra: uma alternativa que também está lotada gera uma nova pergunta, em vez de ser confirmada às cegas.
O que o cliente recebe
O cliente recebe a sua mensagem e, ao lado dela, um JSON Schema gerado a partir do 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"
}
Esse schema é o formulário. Field(description=...) é o rótulo; um valor padrão preenche o campo de antemão e o torna opcional. É a mesma maquinaria de Pydantic para JSON Schema que Ferramentas descreve para os argumentos de uma ferramenta.
Warning
Um schema de elicitação não é tão expressivo quanto o schema de entrada de uma ferramenta. Só campos
planos e primitivos: str, int, float, bool, ou um Literal de strings (que vira um enum).
Coloque um modelo dentro do modelo e ctx.elicit levanta um erro antes de qualquer coisa ser enviada ao cliente:
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
Você está interrompendo uma pessoa no meio de uma tarefa. Se a resposta precisa de aninhamento, ela deveria ter sido um argumento da ferramenta.
As três respostas
result.action informa o que o usuário fez, e há exatamente três possibilidades:
"accept": ele enviou o formulário.result.dataé uma instância deAlternativeDate, já validada."decline": ele disse não."cancel": ele dispensou a pergunta sem escolher.
result.data só existe no "accept", e é por isso que o exemplo verifica result.action primeiro. Seu verificador de tipos garante a ordem: depois de result.action == "accept", result.data é um AlternativeDate; antes disso, não existe .data nenhum.
Uma recusa não é um erro. A ferramenta decide o que recusar significa (aqui, nenhuma reserva) e responde ao modelo normalmente.
Tip
A resposta é validada contra o seu modelo antes de o seu código vê-la. Um cliente que envia
"maybe" para um bool não corrompe a sua reserva: a chamada falha com um
erro de incompatibilidade de schema e o seu if nunca executa.
Envie o usuário para uma URL
Algumas coisas não podem passar pelo modelo nem pelo cliente: credenciais, números de cartão, consentimento OAuth. Para essas você não pede dados; você pede que o usuário vá a algum lugar:
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()recebe a mensagem, a URL a ser visitada e umelicitation_idque você escolhe: qualquer string que identifique essa elicitação dentro do seu servidor.- O resultado tem uma ação e nada mais.
"accept"significa que o usuário concordou em abrir a URL, não que ele terminou o que havia do outro lado. - O pagamento acontece fora de banda, entre o navegador do usuário e o seu provedor de pagamento. Nenhum conteúdo volta pelo MCP.
Olhe para a segunda ferramenta. Quando o seu servidor descobre que o fluxo fora de banda terminou (um webhook, um polling; aqui isso está modelado como uma segunda ferramenta), ctx.session.send_elicit_complete(...) envia notifications/elicitation/complete com o mesmo elicitation_id. É assim que o cliente sabe que pode parar de mostrar "aguardando pagamento...". Sem isso, o cliente só pode adivinhar.
O lado do cliente
Servidores perguntam. Clientes respondem passando um elicitation_callback para 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)
- Um único callback cobre os dois modos.
paramsé uma união deElicitRequestFormParamseElicitRequestURLParams;isinstancefaz a ramificação. - Para uma URL, você mostra
params.urlao usuário e retorna a ação que ele escolheu. Nunca nenhumcontent. - Para um formulário, uma aplicação de verdade renderiza
params.requested_schemae retorna a entrada do usuário comocontent. Este aqui sempre diz sim com uma resposta pronta, que é exatamente o callback que você quer em um teste. - Passar o callback também é a declaração de capacidade: é assim que o servidor descobre que esse cliente pode ser questionado. As outras coisas que um cliente pode responder para um servidor estão em Callbacks do cliente.
Info
A elicitação é uma requisição do servidor para o cliente, e essas só existem em uma
sessão com handshake clássico, e é por isso que este cliente passa mode="legacy".
Em uma conexão 2026-07-28, uma ferramenta pergunta retornando a pergunta a partir da chamada;
esse fluxo é Requisições com múltiplas idas e voltas.
Experimente
Inicie o server.py de modo formulário com ctx.elicit (o do book_table) em Streamable HTTP (Executando seu servidor tem o comando de uma linha), depois execute o main() do cliente e peça uma mesa ao book_table para o dia de Natal.
O callback imprime a pergunta que recebeu:
No tables for 2 on 2025-12-25. Would you like to try another date?
Ele responde com {"accept_alternative": True, "date": "2025-12-27"}, e a ferramenta, que esteve esperando dentro de await ctx.elicit(...) esse tempo todo, conclui a reserva:
Booked a table for 2 on 2025-12-27.
Agora troque pelo server.py de modo URL e aponte o mesmo main() para pay_deposit: o mesmo callback segue o outro ramo, imprime o link de pagamento, e a ferramenta volta com "Complete the payment in your browser." Uma ida e volta, no meio da chamada, nos dois sentidos.
Check
Agora remova elicitation_callback= do Client e chame book_table para o dia de Natal
de novo. A chamada inteira falha com um erro de protocolo:
Elicitation not supported
Um cliente que não registrou nenhum callback nunca declarou a capacidade elicitation, então não há
ninguém a quem perguntar. Sua ferramenta não recebeu um "decline"; ela recebeu uma exceção. Projete pensando nisso: toda
elicitação precisa de uma resposta sensata para "e se eu não puder perguntar?".
Resumo
- Um parâmetro anotado com
Annotated[T, Resolve(fn)]é preenchido por um resolvedor, que retornaElicit(...)quando precisa perguntar. Funciona em todas as conexões. - O schema é um modelo Pydantic plano: só campos primitivos, validados na volta.
result.actioné"accept","decline"ou"cancel";result.datasó existe no accept.await ctx.elicit(message, schema=Model)pergunta de dentro do corpo da ferramenta, eawait ctx.elicit_url(message, url, elicitation_id)serve para tudo que não pode passar pelo modelo (ctx.session.send_elicit_complete(elicitation_id)avisa que a parte fora de banda terminou). Ambas são requisições do servidor para o cliente: precisam do cliente em uma conexão legada.- O cliente responde com um único
elicitation_callback, ramificando pelo tipo dos params; registrá-lo é o que declara a capacidade. - Em uma conexão 2026-07-28, o servidor retorna a pergunta em vez de empurrá-la; o mesmo callback é alimentado por Requisições com múltiplas idas e voltas.
Tudo o que está por baixo desse retorno (o loop de retry, proteger o requestState, conduzir o processo você mesmo) está em Requisições com múltiplas idas e voltas.