Phase 13 - Lesson 08
Construindo um Cliente MCP — Descoberta, Invocação, Gerenciamento de Sessão
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
A maior parte do conteúdo de MCP foca em tutoriais de servidor e ignora o cliente. O código do cliente é onde a orquestração complexa vive: inicialização de processos, negociação de capacidades, mesclagem de lista de ferramentas em múltiplos servidores, callbacks de amostragem, reconexão e resolução de colisões de namespace. Esta lição constrói um cliente multi-servidor que eleva três servidores MCP diferentes para um namespace de ferramentas plano para o modelo.
Tipo: Build Idiomas: Python (stdlib, cliente MCP multi-servidor) Pré-requisitos: Phase 13 · 07 (building an MCP server) Tempo: ~75 minutos
Objetivos de Aprendizado
- Inicializar um servidor MCP como um subprocesso, concluir o
initializee enviar umnotifications/initialized. - Manter o estado da sessão por servidor (capacidades, lista de ferramentas, ids das últimas notificações vistas).
- Mesclar listas de ferramentas de múltiplos servidores em um único namespace com tratamento de colisões.
- Rotear uma chamada de ferramenta para o servidor proprietário e remontar a resposta.
O Problema
Um host de agente real (Claude Desktop, Cursor, Goose, Gemini CLI) carrega múltiplos servidores MCP simultaneamente. Um usuário pode ter um servidor de sistema de arquivos, um servidor Postgres e um servidor GitHub em execução ao mesmo tempo. O trabalho do cliente é:
- Inicializar cada servidor.
- Realizar o handshake com cada um de forma independente.
- Chamar
tools/listem cada um e unificar o resultado. - Quando o modelo emitir
notes_search, buscá-lo no namespace mesclado e rotear para o servidor correto. - Tratar notificações de qualquer servidor (
tools/list_changed) sem bloquear. - Reconectar em caso de falha de transporte.
Desenvolver tudo isso manualmente é o que separa um projeto de brinquedo de um utilizável na prática. Os SDKs oficiais encapsulam isso, mas o modelo mental precisa ser seu.
O Conceito
Inicialização de subprocesso
subprocess.Popen com stdin=PIPE, stdout=PIPE, stderr=PIPE. Defina bufsize=1 e use o modo de texto para leituras linha por linha. Cada servidor é um processo; o cliente mantém um handle Popen por servidor.
Estado da sessão por servidor
Um objeto Session por servidor armazena:
process— o handle do Popen.capabilities— o que o servidor declarou eminitialize.tools— o último resultado detools/list.pending— mapeamento de id de requisição para uma promessa/future aguardando a resposta.
As requisições são assíncronas por natureza; um tools/call enviado para o servidor A enquanto o servidor B está no meio de uma chamada não deve bloquear. Use threads com filas ou asyncio.
Namespace mesclado
Quando o cliente visualiza a lista agregada de ferramentas, os nomes podem colidir. Dois servidores podem expor search. O cliente tem três opções:
- Prefixar com o nome do servidor.
notes/search,files/search. Claro, mas esteticamente poluído. - Substituição silenciosa (primeiro a chegar). A ferramenta
searchdo servidor mais recente substitui a do anterior. Arriscado; esconde colisões. - Rejeição de colisão. Recusar o carregamento do segundo servidor; notificar o usuário. Mais seguro para hosts sensíveis à segurança.
O Claude Desktop usa prefixação por servidor. O Cursor usa rejeição de colisão com um erro claro. O VS Code MCP também adota a prefixação por servidor.
Roteamento
Após a mesclagem, uma tabela de despacho mapeia tool_name -> session. O modelo emite uma chamada por nome; o cliente localiza a sessão e escreve uma mensagem tools/call no stdin do servidor correspondente, aguardando a resposta.
Callback de amostragem (sampling)
Se o servidor declarou a capacidade sampling em initialize, ele pode enviar sampling/createMessage solicitando que o cliente execute seu LLM. O cliente deve:
- Bloquear novas requisições para esse servidor até que a amostragem seja resolvida, ou enfileirar em pipeline se sua implementação suportar concorrência.
- Chamar seu provedor de LLM.
- Enviar a resposta de volta ao servidor.
A Lição 11 cobre amostragem de ponta a ponta. Esta lição apenas cria um esboço para fins de completude.
Tratamento de notificações
notifications/tools/list_changed significa chamar novamente tools/list. notifications/resources/updated significa reler o recurso caso esteja em uso. As notificações não devem gerar respostas — não tente enviar acks para elas.
Um bug comum em clientes: bloquear a thread de leitura em um tools/call enquanto uma notificação aguarda no fluxo de dados. Use uma thread leitora em segundo plano para enviar cada mensagem a uma fila; a thread principal retira os itens da fila e realiza o despacho.
Reconexão
O transporte pode falhar: o servidor travou, o SO encerrou o processo ou o pipe do stdio quebrou. O cliente detecta EOF no stdout e trata a sessão como encerrada. Opções:
- Reiniciar silenciosamente o servidor e refazer o handshake. Adequado para servidores puramente de leitura.
- Exibir a falha para o usuário. Adequado para servidores com estado que possuem sessões visíveis ao usuário.
A Phase 13 · 09 cobre as semânticas de reconexão via HTTP Streamable; a comunicação via stdio é mais simples.
Keepalive e id de sessão
O HTTP Streamable utiliza um cabeçalho Mcp-Session-Id. A comunicação via stdio não possui id de sessão — a identidade do processo É a sessão. Pings de keepalive são opcionais; os pipes do stdio não sofrem interrupções por inatividade.
Use
code/main.py inicializa três servidores MCP simulados como subprocessos, realiza o handshake de cada um, mescla suas listas de ferramentas e roteia chamadas de ferramentas para o servidor correto. Os "servidores" são, na verdade, outros processos Python executando simuladores de resposta simples (sem LLM real). Execute o código para ver:
- Três inicializações, cada uma com seu próprio conjunto de capacidades.
- Três resultados de
tools/listmesclados em um namespace de 7 ferramentas. - Uma decisão de roteamento com base no nome da ferramenta.
- Uma colisão evitada por meio de prefixos de namespace.
O que observar:
- A dataclass
Sessionmantém o estado por servidor de forma limpa. - A thread leitora em segundo plano retira da fila cada linha do stdout sem bloquear a thread principal.
- A tabela de despacho é um
dict[str, Session]simples. - O tratamento de colisões é explícito: quando dois servidores declaram o mesmo nome, o segundo é renomeado com um prefixo.
Envie
Esta lição produz outputs/skill-mcp-client-harness.md. Dada uma lista declarativa de servidores MCP (nome, comando, argumentos), a habilidade produz uma estrutura que os inicializa, mescla as listas de ferramentas e fornece uma função de roteamento com resolução de colisões.
Exercícios
Execute
code/main.pye acompanhe o log de inicialização dos servidores. Encerre um dos processos simuladores com um SIGTERM e observe como o cliente detecta o EOF e marca a sessão correspondente como encerrada.Implemente a prefixação de namespaces. Quando dois servidores expuserem
search, renomeie o segundo como<server>/search. Atualize a tabela de despacho e verifique se as chamadas de ferramenta são roteadas corretamente.Adicione uma estratégia de recuo (backoff) estilo pool de conexões para reinicialização de servidores: recuo exponencial em falhas consecutivas, limitado a 30 segundos, emitindo uma notificação ao usuário após três falhas.
Esboce um cliente que suporte 100 servidores MCP simultâneos. Qual estrutura de dados substitui o dicionário de despacho simples? (Dica: trie para namespaces com prefixos, além de uma métrica para quantidade de ferramentas por servidor.)
Portabilidade do cliente para o SDK Python oficial de MCP. O SDK encapsula
stdio_clienteClientSession. O código deve encolher de cerca de 200 linhas para aproximadamente 40 linhas, mantendo o roteamento multi-servidor.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Cliente MCP | "O host do agente" | Processo que inicializa servidores e orquestra chamadas de ferramentas |
| Sessão | "Estado por servidor" | Controle de capacidades, lista de ferramentas e requisições pendentes |
| Namespace mesclado | "Uma lista única de ferramentas" | Conjunto plano de nomes de ferramentas entre todos os servidores ativos |
| Colisão de namespace | "Dois servidores com a mesma ferramenta" | O cliente deve prefixar, rejeitar ou aplicar substituição simples no duplicado |
| Roteamento | "Quem recebe esta chamada?" | Despacho do nome da ferramenta para o servidor proprietário |
| Leitor em segundo plano | "Stdout não-bloqueante" | Thread ou tarefa que esvazia o stdout do servidor para uma fila |
| Callback de amostragem | "LLM como serviço" | Manipulador do cliente para sampling/createMessage originado no servidor |
notifications/*_changed |
"Primitiva mutada" | Sinalizador de que o cliente deve redescobrir ou reler |
| Política de reconexão | "Quando o servidor morre" | Semântica de reinicialização em caso de falha de transporte |
| Sessão stdio | "Processo = sessão" | Sem id de sessão; o tempo de vida do processo filho define a sessão |
Leituras Adicionais
- Model Context Protocol — Client spec — comportamento canônico do cliente
- MCP — Quickstart client guide — tutorial básico do cliente com o SDK Python
- MCP Python SDK — client module — referência para
ClientSessionestdio_client - MCP TypeScript SDK — Client — paralelo em TS
- VS Code — MCP in extensions — como o VS Code multiplexa múltiplos servidores MCP em um único editor host