Pular para conteúdo

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:

server.py
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_delete lê pelo nome o próprio argumento path da ferramenta, lista a pasta e só elicita quando é necessário - uma pasta vazia resolve para Confirm(ok=True) sem nenhuma ida e volta até o cliente.
  • delete_folder anota ElicitationResult[Confirm], então o framework injeta o resultado inteiro e a ferramenta faz match em todos os casos: aceitar-e-confirmar, aceitar-mas-manter (ok=False), recusar, cancelar.
  • O parâmetro confirm nunca aparece no schema de entrada da ferramenta - o cliente fornece path, o resolvedor fornece confirm.

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:

server.py
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 a ctx.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 de AlternativeDate, 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:

server.py
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 um elicitation_id que 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(...):

client.py
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 de ElicitRequestFormParams e ElicitRequestURLParams; isinstance faz a ramificação.
  • Para uma URL, você mostra params.url ao usuário e retorna a ação que ele escolheu. Nunca nenhum content.
  • Para um formulário, uma aplicação de verdade renderiza params.requested_schema e retorna a entrada do usuário como content. 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 retorna Elicit(...) 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.data só existe no accept.
  • await ctx.elicit(message, schema=Model) pergunta de dentro do corpo da ferramenta, e await 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.