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 initialize e enviar um notifications/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 é:

  1. Inicializar cada servidor.
  2. Realizar o handshake com cada um de forma independente.
  3. Chamar tools/list em cada um e unificar o resultado.
  4. Quando o modelo emitir notes_search, buscá-lo no namespace mesclado e rotear para o servidor correto.
  5. Tratar notificações de qualquer servidor (tools/list_changed) sem bloquear.
  6. 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 em initialize.
  • tools — o último resultado de tools/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:

  1. Prefixar com o nome do servidor. notes/search, files/search. Claro, mas esteticamente poluído.
  2. Substituição silenciosa (primeiro a chegar). A ferramenta search do servidor mais recente substitui a do anterior. Arriscado; esconde colisões.
  3. 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:

  1. Bloquear novas requisições para esse servidor até que a amostragem seja resolvida, ou enfileirar em pipeline se sua implementação suportar concorrência.
  2. Chamar seu provedor de LLM.
  3. 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/list mesclados 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 Session manté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

  1. Execute code/main.py e 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.

  2. 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.

  3. 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.

  4. 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.)

  5. Portabilidade do cliente para o SDK Python oficial de MCP. O SDK encapsula stdio_client e ClientSession. 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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).