Construindo um Cliente MCP Personalizado com o Agent SDK
Todo servidor MCP precisa de algo na outra ponta da conexão.
Esse algo é um cliente, e o mesmo SDK Python do MCP que fornece primitivas para construção de servidores também fornece primitivas de cliente: conectar a um servidor, listar o que ele oferece, chamar uma ferramenta, ler um recurso.
Quando esse cliente vive dentro de um aplicativo construído no Claude Agent SDK, as ferramentas que um servidor MCP expõe se tornam ferramentas que o próprio Claude pode usar durante uma conversa.
Resumo
Um cliente MCP personalizado se conecta a um servidor por meio de um transporte, mais comumente stdio_client para um processo de servidor local.
A conexão produz uma ClientSession, o objeto que você usa para negociar capacidades e, em seguida, enviar solicitações.
list_tools(), call_tool() e read_resource() são as três primitivas que cobrem o fluxo de trabalho diário: descobrir o que está disponível, invocá-lo e ler dados.
Um cliente personalizado é útil sempre que você precisar de acesso programático a um servidor MCP fora do Claude Desktop ou Claude Code, por exemplo, integrando as ferramentas de um servidor MCP a um aplicativo construído no Agent SDK.
Esta página detalha o lado do cliente do mesmo ciclo de vida de solicitação coberto na página explicativa de tratamento de solicitações.
Receita
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(command="python", args=["server.py"])
async def main():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
asyncio.run(main())Quando usar isso:
- Você está construindo um aplicativo que precisa chamar ferramentas MCP diretamente, fora do Claude Desktop ou Claude Code.
- Você deseja integrar as ferramentas de um servidor MCP a um aplicativo Claude Agent SDK como parte de um loop de agente maior.
- Você precisa escrever testes de integração que exercitem um servidor real através do mesmo caminho de código que um cliente de produção usa.
- Você está depurando um servidor e deseja ver exatamente o que ele relata durante a negociação de capacidades.
Exemplo de Trabalho
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(
command="python",
args=["tickets_server.py"],
)
async def run_client() -> None:
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools_result = await session.list_tools()
print("Ferramentas disponíveis:")
for tool in tools_result.tools:
print(f" - {tool.name}: {tool.description}")
create_result = await session.call_tool(
"create_ticket",
arguments={"title": "Impressora offline", "priority": "low"},
)
ticket_id = create_result.content[0].text
print(f"Ticket criado: {ticket_id}")
resource_result = await session.read_resource("tickets://open")
print("Tickets abertos:")
print(resource_result.contents[0].text)
close_result = await session.call_tool(
"close_ticket",
arguments={"ticket_id": ticket_id},
)
print(close_result.content[0].text)
if __name__ == "__main__":
asyncio.run(run_client())O que isso demonstra:
- O ciclo completo de conexão-negociação-troca do lado do cliente:
stdio_clientabre o transporte,ClientSessionnegocia,initialize()completa o handshake. list_tools()usado para descobrir o que o servidor oferece antes de assumir que qualquer ferramenta existe.call_tool()invocado duas vezes, uma para criar um ticket e outra para fechá-lo, mostrando como um cliente gerencia um fluxo de trabalho de várias etapas.read_resource()usado para buscar dados somente leitura por URI, distinto das chamadas de ferramenta orientadas a ação ao redor dele.
Mergulho Profundo
Como Funciona
stdio_client(server_params)inicia o servidor como um subprocesso usando o comando e os argumentos fornecidos, e retorna um par de fluxos para leitura e escrita.ClientSession(read_stream, write_stream)envolve esses fluxos na camada de protocolo, fornecendo as primitivas de solicitação/resposta em vez de bytes brutos.session.initialize()realiza a negociação de capacidades, o cliente e o servidor trocam versões suportadas, e o servidor retorna seu manifesto de ferramentas, recursos e prompts.- Cada chamada após
initialize(),list_tools(),call_tool(),read_resource(), é um único ciclo de solicitação/resposta sobre a sessão já negociada. - Ambos os blocos
async withlidam com a limpeza: fechando a sessão e terminando o subprocesso quando o bloco sai, mesmo que uma exceção seja levantada dentro dele.
Primitivas do Cliente em Resumo
| Primitiva | Propósito | Retorna |
|---|---|---|
list_tools() | Descobrir ferramentas disponíveis | Uma lista de definições de ferramentas com nome, descrição, esquema de entrada |
call_tool(name, arguments) | Invocar uma ferramenta | Um resultado com uma lista content |
read_resource(uri) | Buscar conteúdo de recurso | Um resultado com uma lista contents |
list_resources() | Descobrir recursos disponíveis | Uma lista de definições de recursos |
Tratamento de Erros de Ferramenta
from mcp import McpError
try:
result = await session.call_tool("close_ticket", arguments={"ticket_id": "T-999"})
except McpError as exc:
print(f"chamada de ferramenta falhou: {exc}")Uma chamada de ferramenta que levanta uma exceção no lado do servidor é apresentada ao cliente como um erro estruturado em vez de uma falha, capture-a da mesma forma que você capturaria qualquer modo de falha esperado em seu aplicativo.
Notas de Python
# Sempre chame session.initialize() antes de qualquer outro método de sessão.
# Chamar list_tools() ou call_tool() antes que a inicialização seja concluída
# falhará, pois o cliente ainda não negociou o que o servidor oferece.
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
# a sessão agora é segura para usoParâmetros e Valores de Retorno
| Método | Argumento Chave | Notas |
|---|---|---|
call_tool(name, arguments) | arguments: dict | Deve corresponder ao esquema de entrada declarado da ferramenta |
read_resource(uri) | uri: str | Deve corresponder a uma URI que o servidor realmente registrou |
list_tools() | nenhum | Seguro para chamar repetidamente, reflete o manifesto atual do servidor |
Armadilhas
- Chamar
list_tools()oucall_tool()antes deinitialize(). A sessão ainda não negociou as capacidades, então o servidor não tem contexto para a solicitação. Correção: sempreawait session.initialize()como a primeira chamada dentro do bloco de sessão. - Passar argumentos que não correspondem ao esquema de uma ferramenta. Uma chave com erro de digitação ou tipo incorreto falha na validação no servidor e retorna um erro que o cliente deve tratar. Correção: verifique a saída de
list_tools()para o esquema exato antes de codificar um payload decall_tool(). - Não tratar
McpErrorem torno decall_tool(). Um erro de ferramenta não tratado se propaga como uma exceção inesperada no código do aplicativo que não espera uma falha em nível de protocolo. Correção: envolva chamadas de ferramenta que possam falhar (IDs inválidos, registros ausentes) em um try/except paraMcpError. - Vazar o subprocesso ao pular o gerenciador de contexto
async with. Chamarstdio_client()sem o gerenciador de contexto pode deixar o processo do servidor em execução após a saída do seu script. Correção: sempre useasync with stdio_client(...)para que a limpeza seja executada automaticamente. - Assumir que um recurso sempre existe.
read_resource()em uma URI que o servidor nunca registrou, ou cujo dado subjacente foi removido, levanta um erro assim como uma chamada de ferramenta inválida. Correção: chamelist_resources()primeiro se você não tiver certeza de que a URI ainda é válida. - Reutilizar uma
ClientSessionem tarefas concorrentes não relacionadas sem coordenação. Uma sessão não é automaticamente segura para uso concorrente arbitrário de múltiplas corrotinas emitindo chamadas sobrepostas. Correção: serialize as chamadas através de uma tarefa, ou abra uma sessão por unidade lógica de trabalho.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Claude Desktop / Claude Code como cliente | Você só precisa que o Claude use o servidor interativamente, sem lógica de aplicativo personalizada | Você precisa de controle programático, orquestração personalizada ou testes automatizados |
| Um cliente HTTP genérico contra um servidor HTTP/SSE | O servidor está implantado remotamente e você não precisa do modelo de subprocesso stdio | O servidor é apenas local e stdio é mais simples |
O Inspetor MCP (npx @modelcontextprotocol/inspector) | Você está explorando manualmente um servidor durante o desenvolvimento | Você precisa desse comportamento incorporado em um aplicativo em execução |
FAQs
Qual é o código mínimo necessário para se conectar a um servidor MCP?
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()Essa é a sequência completa de conexão e negociação; todo o resto é construído em cima de uma sessão inicializada.
Por que preciso de `stdio_client` e `ClientSession`?
stdio_clientlida com o transporte, iniciando o subprocesso e fornecendo fluxos brutos de leitura/escrita.ClientSessionlida com a camada de protocolo em cima desses fluxos, negociação, solicitações, respostas.- Separar eles permite que a mesma API
ClientSessionfuncione sobre outros transportes, como HTTP/SSE, sem alterar seu código de nível de protocolo.
Como sei quais argumentos uma ferramenta espera?
Chame list_tools() e inspecione o esquema de entrada de cada ferramenta. É o mesmo esquema que o servidor gerou a partir das dicas de tipo do manipulador, portanto, é a fonte autoritativa, não o código-fonte do servidor.
O que acontece se eu chamar uma ferramenta que não existe?
A chamada falha com um McpError. Capture-o explicitamente em vez de assumir que cada invocação de call_tool() é bem-sucedida, especialmente se o nome da ferramenta veio de entrada do usuário ou configuração.
Um cliente pode se conectar a vários servidores ao mesmo tempo?
Sim, abra um par stdio_client/ClientSession separado por servidor. Cada sessão rastreia suas próprias capacidades negociadas independentemente, não há estado compartilhado entre elas.
Como isso se relaciona com o Claude Agent SDK?
Um aplicativo construído no Agent SDK pode usar essas mesmas primitivas de cliente para se conectar a um servidor MCP e expor suas ferramentas como parte do conjunto de ferramentas disponíveis do agente, permitindo que o Claude as chame durante uma conversa da mesma forma que faria com qualquer outra ferramenta.
Preciso chamar `list_resources()` antes de `read_resource()`?
Não estritamente, se você já souber a URI. É útil quando você não sabe quais recursos existem antecipadamente, da mesma forma que list_tools() é útil antes de um call_tool() desconhecido.
O que `call_tool()` retorna exatamente?
Um objeto de resultado com uma lista content, tipicamente contendo blocos de conteúdo de texto. Extraia o valor real com algo como result.content[0].text, correspondendo à forma que o manipulador do servidor retornou.
A conexão do cliente é síncrona ou assíncrona?
Assíncrona. Cada primitiva, initialize(), list_tools(), call_tool(), read_resource(), é um método async e deve ser aguardada dentro de um loop de eventos asyncio.
O que acontece com o processo do servidor quando meu script cliente sai?
Se você usou async with stdio_client(...), o gerenciador de contexto termina o subprocesso como parte da limpeza quando o bloco sai, inclusive quando uma exceção é levantada.
Posso testar um servidor com este cliente em vez de uma implantação real?
Sim, esse é um padrão comum. Conectar um cliente de teste ao seu servidor via stdio e chamar suas ferramentas diretamente é a base para testes de integração, abordado com mais detalhes na página dedicada a testes.
Relacionados
- Como Servidores MCP Lidam com Solicitações - o ciclo de vida que este cliente gerencia do outro lado
- Noções Básicas de Servidor MCP - o servidor mínimo ao qual este cliente pode se conectar
- Construindo um Servidor MCP com o SDK Python - os manipuladores do lado do servidor que este cliente chama
- Testando Servidores MCP Antes da Implantação - usando uma sessão de cliente em testes automatizados
- Comparação de Implantação MCP stdio vs HTTP/SSE - conectando-se a um servidor por meio de um transporte diferente
Versões de Stack: Escrito contra a linha de modelos Claude atual em aproximadamente junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão), e Claude Haiku 4.5 - e os SDKs atuais do Model Context Protocol Python/TypeScript. Nomes de modelos, versões de SDK e a especificação MCP mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs e modelcontextprotocol.io antes de confiar neles.