Callbacks do cliente
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
Quase toda requisição no MCP vai em um só sentido: do cliente para o servidor.
Um servidor também pode pedir coisas ao cliente: fazer uma pergunta ao usuário, amostrar o modelo do usuário, listar as pastas do workspace do usuário. Você responde a essas requisições passando callbacks para Client(...).
Um servidor que pergunta
Aqui está um servidor cuja ferramenta não consegue terminar sozinha:
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Library")
class CardHolder(BaseModel):
name: str
@mcp.tool()
async def issue_card(ctx: Context) -> str:
"""Issue a new library card."""
answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
if answer.action == "accept":
return f"Card issued to {answer.data.name}."
return "No card issued."
ctx.elicit(...)envia uma requisiçãoelicitation/createpara o cliente e espera.- A ferramenta não retorna até que alguém (uma pessoa em um formulário, ou o seu código) forneça um
name.
Essa é a metade do servidor, e a página Elicitação cuida dela. Esta página é a outra ponta do fio.
O callback de elicitação
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitResult
async def handle_elicitation(
context: ClientRequestContext,
params: ElicitRequestParams,
) -> ElicitResult:
return ElicitResult(action="accept", content={"name": "Ada Lovelace"})
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("issue_card")
print(result.content)
- Um callback de elicitação (elicitation) é
async (context, params) -> ElicitResult. params.messageé a pergunta.params.requested_schemaé o JSON Schema da resposta que o servidor quer. Um cliente de verdade renderiza um formulário a partir dele; este aqui preenche automaticamente.- Você retorna
ElicitResult(action="accept", content={...}), ouaction="decline", ouaction="cancel". A única outra opção éErrorData(...), que recusa a requisição e faz a chamada inteira falhar. contexté umClientRequestContext: asessionativa, orequest_iddo servidor e qualquermetaque ele tenha anexado.
Tip
params é uma união dos dois modos de elicitação. Aqui params.mode é "form"; uma requisição "url"
traz params.url em vez de um schema. Um único callback trata os dois; ramifique em params.mode.
Elicitação mostra o padrão completo.
Experimente
Chame issue_card e observe as duas pontas.
Seu callback recebe a pergunta do servidor, já analisada:
params.mode # 'form'
params.message # 'What name should go on the card?'
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
Ele responde, ctx.elicit(...) retoma dentro da ferramenta, e a ferramenta termina:
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
Um tools/call seu, um elicitation/create de volta do servidor, respondido pela sua função, tudo dentro de uma única chamada de ferramenta.
Info
O mode="legacy" na chamada Client(...) está fazendo trabalho de verdade. Por padrão, Client(...) negocia o caminho
moderno do protocolo, e esse caminho não tem canal de retorno (back-channel) para requisições do servidor ao cliente: ctx.elicit
falha antes mesmo de o seu callback rodar. Não é o transporte que decide isso; é o protocolo
negociado, tanto em memória quanto por uma URL. Fixe mode="legacy" sempre que o seu cliente tiver
que responder a uma; todos os testes por trás desta página fazem isso. Versões do protocolo tem a história completa.
Em uma sessão 2026-07-28 o callback não está morto, ele é alimentado de outro jeito: quando uma ferramenta retorna um
InputRequiredResult carregando um ElicitRequest, o Client despacha essa entrada para o mesmo
elicitation_callback e refaz a chamada para você. Esse fluxo está em Requisições de múltiplas idas e voltas.
Um callback é uma capacidade
Você nunca disse ao servidor que o seu cliente consegue responder a requisições de elicitação. O SDK disse.
Quando um cliente se conecta, ele declara suas capabilities, a imagem espelhada das do servidor. Você não escreve esse objeto. Registrar um callback é a declaração.
| você passa | o cliente declara |
|---|---|
elicitation_callback= |
"elicitation": {"form": {}, "url": {}} |
sampling_callback= |
"sampling": {} |
list_roots_callback= |
"roots": {"listChanged": true} |
| nenhum deles | {} |
As subcapacidades de amostragem (sampling) são o único refinamento: passe sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability()) junto com sampling_callback quando o seu amostrador trata os parâmetros tools / tool_choice. Os servidores precisam ver sampling.tools declarado antes de poderem enviá-los.
logging_callback e message_handler não estão na tabela. Eles tratam notificações, e notificações não precisam de capacidade.
O servidor lê a declaração de volta com ctx.session.check_client_capability(...). Adicione uma ferramenta que faça isso:
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.types import ClientCapabilities, ElicitationCapability, RootsCapability, SamplingCapability
mcp = MCPServer("Library")
class CardHolder(BaseModel):
name: str
@mcp.tool()
async def issue_card(ctx: Context) -> str:
"""Issue a new library card."""
answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
if answer.action == "accept":
return f"Card issued to {answer.data.name}."
return "No card issued."
@mcp.tool()
def client_features(ctx: Context) -> list[str]:
"""Which optional features the connected client declared."""
declared = {
"elicitation": ClientCapabilities(elicitation=ElicitationCapability()),
"sampling": ClientCapabilities(sampling=SamplingCapability()),
"roots": ClientCapabilities(roots=RootsCapability()),
}
return [name for name, capability in declared.items() if ctx.session.check_client_capability(capability)]
Conecte com apenas elicitation_callback e chame-a:
result.structured_content # {'result': ['elicitation']}
Passe os três callbacks e você recebe ['elicitation', 'sampling', 'roots']. Não passe nenhum e você recebe [].
Check
Agora faça a coisa errada: conecte sem elicitation_callback e chame issue_card mesmo assim.
A requisição elicitation/create do servidor ainda chega ao seu cliente, e o SDK a responde por
você, com um erro, porque você nunca disse que conseguiria tratá-la. Esse erro afunda a chamada inteira.
call_tool não retorna um resultado is_error; ele levanta uma exceção:
MCPError: Elicitation not supported
Isso é um erro de protocolo (-32600, invalid request), não um erro de ferramenta: não há nada para
o modelo ler e tentar de novo. É por isso que vale a pena ter client_features: um servidor bem-comportado
verifica antes de perguntar.
O par descontinuado
sampling_callback responde a sampling/createMessage: o servidor pedindo ao seu modelo que complete algo. list_roots_callback responde a roots/list: o servidor perguntando em quais diretórios ele pode trabalhar.
Os dois funcionam. Os dois seguem a regra acima. E os dois atendem RPCs que a spec 2026-07-28 remove: um servidor moderno não chama de volta o seu cliente no meio de uma requisição, ele devolve a requisição para você como parte do resultado da ferramenta (Requisições de múltiplas idas e voltas). Os callbacks em si não estão mortos. Quando um InputRequiredResult carrega um CreateMessageRequest ou um ListRootsRequest, o loop automático do Client o despacha para o mesmo sampling_callback ou list_roots_callback que você registrou aqui. A lista inteira está em Funcionalidades descontinuadas.
Você ainda precisa dos callbacks para falar com servidores que não migraram. As assinaturas:
from pydantic import FileUrl
from mcp.client import ClientRequestContext
from mcp.types import CreateMessageRequestParams, CreateMessageResult, ListRootsResult, Root, TextContent
async def handle_sampling(
context: ClientRequestContext,
params: CreateMessageRequestParams,
) -> CreateMessageResult:
return CreateMessageResult(
role="assistant",
content=TextContent(type="text", text="The answer is 42."),
model="my-llm",
)
async def handle_list_roots(context: ClientRequestContext) -> ListRootsResult:
return ListRootsResult(roots=[Root(uri=FileUrl("file:///home/ada/notebooks"), name="notebooks")])
- Um callback de amostragem recebe o
CreateMessageRequestParamscompleto (messages,model_preferences,max_tokens) e retorna umCreateMessageResult. Você executa o modelo, do jeito que quiser; o SDK só transporta a requisição. - Um callback de roots não recebe parâmetro nenhum e retorna um
ListRootsResult. - Qualquer um dos dois pode retornar
ErrorData(...)no lugar, para recusar.
Passe-os para Client(...) exatamente como elicitation_callback.
Os callbacks de notificação
Mais dois. Nenhum deles declara nada.
logging_callback recebe as notifications/message que um servidor envia, como LoggingMessageNotificationParams (level, logger, data). O logging de protocolo em si foi descontinuado pela spec 2026-07-28 (Logging diz o que fazer no lugar), então esse callback existe para os servidores que ainda o emitem. Em uma conexão da era 2026, o callback sozinho não te dá nada, porque servidores 2026 enviam mensagens de log apenas para requisições que optam por recebê-las: passe log_level="info" (ou outro nível) para Client(...) para carimbar essa opção em toda requisição e receber esse nível e acima. Servidores pré-2026 o ignoram e mantêm o comportamento de logging/setLevel.
message_handler é o pega-tudo: toda notificação do servidor que a sessão expõe chega até ele (além do callback específico dela), e em um transporte baseado em stream toda Exception no nível do transporte também. Duas nunca chegam: notifications/cancelled é aplicada pelo SDK em vez de exposta, e a confirmação de assinatura de um stream listen() ativo é consumida por esse stream. Anote o parâmetro com IncomingMessage (ServerNotification | Exception, exportado de mcp.client). O único padrão que vale conhecer é if isinstance(message, Exception): raise message, para que uma conexão quebrada falhe em alto e bom som em vez de sumir.
Recapitulando
- Um servidor pode enviar requisições ao cliente. Você as responde com callbacks passados para
Client(...). - O callback de elicitação é o atual:
async (context, params) -> ElicitResult, uma função para os modos formulário e URL. - Registrar um callback é declarar a capacidade. Sem ele, o SDK recusa a requisição do servidor em seu nome e a chamada inteira falha com
MCPError. - Um servidor descobre antes de perguntar com
ctx.session.check_client_capability(...). sampling_callbackelist_roots_callbackfuncionam do mesmo jeito, mas atendem funcionalidades descontinuadas; servidores modernos usam requisições de múltiplas idas e voltas no lugar.logging_callbackemessage_handlerrecebem notificações. Eles não declaram nada.
O primeiro argumento de Client(...) é um objeto de transporte. Transportes do cliente cobre todos os tipos.