# Por que as falhas de inicialização do servidor MCP parecem iguais?

Um cliente MCP pode informar que um servidor «falhou ao iniciar» mesmo depois de o sistema operacional ter iniciado o processo, de o processo ter lido a entrada e de o servidor já ter contatado algo fora da máquina. Essa mensagem não é um diagnóstico. Ela reúne falhas de inicialização, de protocolo, de descoberta e, às vezes, de execução de ferramentas. A correção não é aumentar o timeout. É separar os estados e tornar cada um observável.

Trate a inicialização como uma sequência de limites que produzem evidências. Se você não consegue dizer qual limite o servidor atravessou, não consegue informar ao operador se é seguro repetir a tentativa, se uma credencial pode ter sido usada ou se o cliente simplesmente não conseguiu exibir um servidor saudável. Separe os estados e torne cada um observável.

## Um único status vermelho esconde quatro falhas diferentes

O operador precisa de quatro respostas, nesta ordem: o cliente iniciou o comando configurado? Os dois lados concluíram a inicialização do MCP? O cliente recebeu uma lista utilizável de ferramentas? Algum código alcançou um canal externo? Cada resposta comprova algo diferente.

Um processo pode falhar antes de existir no sentido habitual. O executável pode estar ausente, o diretório de trabalho pode não existir, um executor de pacotes pode falhar antes de chamar seu código ou o processo filho pode terminar imediatamente porque falta uma variável de ambiente obrigatória. Chame isso de **falha de inicialização**. Não há uma sessão MCP, e o código da aplicação pode nem ter sido executado.

Um processo também pode existir e ainda falhar na troca de protocolo. Com stdio, o processo filho tem stdin e stdout conectados ao cliente. O servidor precisa ler JSON-RPC de stdin e escrever somente mensagens JSON-RPC em stdout. Depois, precisa responder à requisição `initialize` do cliente com uma versão compatível do protocolo e as capacidades declaradas. O cliente envia então `notifications/initialized`. Se essa sequência não for concluída, chame-a de **falha de handshake**.

Um handshake bem-sucedido não prova que o cliente descobriu as ferramentas. O servidor pode declarar suporte a ferramentas, mas falhar ao registrá-las, gerar um esquema de entrada inválido, retornar um resultado malformado de `tools/list` ou retornar uma lista vazia porque sua própria configuração desativou todas as ferramentas. Chame isso de **falha de descoberta de ferramentas**. Cliente e servidor podem estar saudáveis o bastante para trocar mensagens, mas ainda assim não existir nada que o agente possa chamar.

Por fim, o servidor pode concluir a descoberta e falhar apenas quando uma ferramenta é executada. Essa é uma **falha de execução de ferramenta**. Ela pertence a outro registro de incidente. Se você a misturar com a inicialização, alguém acabará repetindo uma tentativa em um servidor que já enviou uma requisição HTTP ou abriu uma conexão SSH.

A documentação do Model Context Protocol deixa essa separação visível, mesmo que muitas interfaces de cliente não deixem. As orientações de depuração distinguem problemas de processo e configuração dos registros de protocolo e alertam que servidores stdio locais devem manter logs comuns fora de stdout. O ciclo de inicialização do protocolo e a requisição `tools/list` são trocas distintas. Preserve essa distinção na sua telemetria, em vez de aceitar o rótulo genérico do cliente.

## A falha de inicialização termina antes de o MCP existir

Uma falha de inicialização significa que o cliente não obteve um processo filho utilizável com um fluxo de protocolo legível. Não significa que o comando «parecia correto» em um terminal.

Os shells interativos escondem muita coisa. Seu shell tem um `PATH`, um diretório atual, gerenciadores de versões de linguagem, credenciais e arquivos de configuração que um aplicativo de desktop ou subprocesso do agente pode não herdar. Um cliente pode iniciar com `/` como diretório de trabalho no macOS. Pode usar um ambiente restrito. Também pode passar o comando como um executável acompanhado de um array de argumentos, em vez de usar um shell. Nesse caso, aliases e redirecionamentos do shell não fazem nada.

Capture o registro exato da inicialização antes de tentar raciocinar sobre o MCP:

```text
run_id=run_01JX...
phase=launch
command=/usr/local/bin/node
argv=["/Users/dev/work/acme-mcp/dist/index.js"]
cwd=/
pid=84217
started_at=2026-07-22T14:03:12.417Z
```

Depois, capture um evento terminal quando o processo terminar ou quando o prazo do handshake expirar:

```text
run_id=run_01JX...
phase=launch
exit_code=1
signal=null
stderr=Error: ENOENT: no such file or directory, open './config.json'
```

Esse registro resolve rapidamente uma discussão comum. O servidor não «teve um problema de MCP». Ele presumiu que um caminho relativo seria resolvido no diretório do projeto, mas o cliente o iniciou a partir de `/`.

Use caminhos absolutos para o executável, o ponto de entrada, os arquivos de configuração e todos os arquivos lidos durante a inicialização. O guia de depuração do MCP destaca os diretórios de trabalho indefinidos para servidores iniciados pelo cliente e recomenda caminhos absolutos. Isso não é uma precaução de estilo. Remove uma fonte de falhas que só aparece quando alguém instala a mesma configuração em outra máquina.

Não declare a inicialização bem-sucedida apenas porque recebeu um PID. Um PID informa que o kernel criou um processo. Não informa se o programa foi carregado, se o canal de stdout está intacto ou se o processo já virou um zumbi aguardando ser coletado.

Uma máquina de estados útil para a inicialização é pequena:

```text
not_requested
  -> spawn_requested
  -> spawned
  -> executable_ready
  -> handshake_pending
```

`spawned` significa que o processo pai recebeu o PID do processo filho. `executable_ready` significa que o filho escreveu em stderr um evento deliberado, não pertencente ao protocolo, depois de carregar a configuração e instalar seu manipulador de erros fatais. Não envie esse evento para stdout. Em um servidor stdio, stdout não é um canal de logs que está por acaso próximo. É o fio do protocolo.

O evento de prontidão não deve afirmar que o servidor está conectado a uma API, banco de dados ou host remoto. Deve dizer apenas o que comprova: o processo chegou à configuração do transporte MCP. Uma linha como `ready=true` se torna perigosa quando as equipes passam a interpretá-la silenciosamente como «é seguro chamar ferramentas». Dê nome à fase.

## O handshake tem uma definição precisa

Uma falha de handshake MCP começa depois que existe um processo utilizável e termina antes que o ciclo de inicialização seja concluído. Não a chame de falha de conexão sem verificar as mensagens.

Em um transporte stdio, os primeiros bytes em stdout são importantes. Um banner de inicialização pode corromper o fluxo antes que o servidor veja a requisição. O mesmo pode acontecer com uma dependência que imprime um aviso de atualização, um `console.log`, um `print` do Python, um formatador de exceções do framework ou um script wrapper que escreve texto de status em stdout. As orientações oficiais de construção e depuração do MCP são claras: em servidores stdio, escreva os logs em stderr, porque stdout transporta as mensagens do protocolo.

Este é o menor rastreamento útil para uma inicialização saudável:

```json
{"direction":"in","id":1,"method":"initialize"}
{"direction":"out","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"acme","version":"1.4.0"}}}
{"direction":"in","method":"notifications/initialized"}
```

A versão exata do protocolo depende das versões compatíveis com o cliente e o servidor. O importante é que o servidor tenha escolhido uma versão aceita pelo cliente, retornado um resultado válido e recebido a notificação de conclusão. Armazene um evento analisado para cada mensagem, não payloads brutos que contenham credenciais.

Se o rastreamento começar assim, o diagnóstico muda:

```text
stdout: Starting Acme MCP server
{"jsonrpc":"2.0","id":1,"method":"initialize",...}
```

O servidor pode ser perfeitamente capaz de responder, mas o analisador JSON do cliente já encontrou uma entrada inválida. Um timeout depois disso não informa que o servidor estava lento. Informa que o transporte foi corrompido.

Outra falha conhecida parece mais saudável:

```text
phase=handshake
initialize_received=true
initialize_response_sent=false
fatal_error=Cannot read properties of undefined (reading 'tools')
```

O processo filho foi iniciado. Recebeu a requisição. Falhou ao preparar uma resposta. Isso é um bug do servidor ou uma suposição de configuração não tratada, não uma configuração incorreta do cliente.

Torne os limites explícitos nos logs:

```text
phase=handshake event=initialize_received run_id=run_01JX request_id=1
phase=handshake event=initialize_responded run_id=run_01JX request_id=1 protocol_version=2025-06-18
phase=handshake event=initialized_received run_id=run_01JX
```

Se você escrever apenas `connected=true`, apagará a informação que diferencia uma resposta enviada de um ciclo de inicialização concluído. Os clientes podem fechar ou reiniciar depois de receber a resposta, mas antes de enviar a notificação. Isso é operacionalmente diferente de um erro no analisador da inicialização.

Dê ao handshake seu próprio prazo. Inicie-o quando o processo for criado ou, se puder observar esse momento, quando o transporte estiver pronto. Encerre-o quando `notifications/initialized` chegar. Quando o prazo expirar, informe o último evento confirmado, como `spawned_no_initialize`, `initialize_received_no_response` ou `response_sent_no_initialized`. Esses nomes indicam ao operador qual lado deve ser investigado primeiro.

## A descoberta de ferramentas falha depois que o servidor já está acessível

Uma falha de descoberta de ferramentas significa que cliente e servidor conseguem falar MCP, mas o cliente não recebeu uma resposta utilizável para `tools/list`. Muitas vezes ela é reportada como falha de inicialização porque vários clientes descobrem as ferramentas logo depois da inicialização.

Não presuma que um painel de ferramentas vazio prova que o resultado de `tools/list` veio vazio. Alguns clientes ocultam ferramentas depois de uma falha na validação do esquema. Alguns armazenam os resultados da descoberta em cache. Alguns solicitam ferramentas sob demanda, apenas quando o agente começa uma tarefa. Outros se conectam a um servidor MCP para acessar recursos ou prompts e nunca pedem ferramentas. Suas evidências devem capturar a requisição e a resposta.

Um rastreamento saudável de descoberta tem esta forma:

```json
{"direction":"in","id":2,"method":"tools/list"}
{"direction":"out","id":2,"result":{"tools":[{"name":"issue_lookup","description":"Fetch one issue by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}}]}}
```

Um registro de descoberta deve incluir a quantidade de ferramentas e um resumo criptográfico dos esquemas normalizados. Esse resumo ajuda a verificar se duas execuções anunciaram a mesma interface sem armazenar descrições ou configurações sensíveis. Ele também detecta mudanças acidentais em que uma ferramenta continua existindo, mas seus parâmetros obrigatórios desapareceram.

Não crie definições de ferramentas acessando um serviço externo durante `tools/list`. Esse desenho transforma a descoberta em um efeito colateral, faz uma atualização do cliente parecer uma execução e cria a pior pergunta possível em um incidente: «Listar as ferramentas alterou alguma coisa?» Sempre que possível, o registro das ferramentas deve ser local e determinístico.

O servidor pode precisar de configuração para decidir se anuncia uma ferramenta. Leia essa configuração na inicialização e registre o resultado, mas não faça a descoberta esperar por uma atualização de token ou uma sondagem SSH. Se uma ferramenta exigir uma credencial, valide a presença da referência local sem usá-la. Adie a ação remota real até que o cliente chame a ferramenta.

Essa distinção importa para o controle do agente. Se um agente chegar ao Sallyport por meio de `sp mcp`, um registro de descoberta MCP bem-sucedido mostra apenas que o adaptador expôs operações chamáveis. Não mostra que o Sallyport executou uma ação HTTP ou SSH.

Há uma razão legítima para retornar uma lista vazia: o servidor não tem ferramentas habilitadas para a configuração atual. Diga isso em uma resposta estruturada ou em um log voltado ao cliente. Não falhe durante o registro e deixe o cliente tentar deduzir se o conjunto de ferramentas está vazio de propósito.

```text
phase=discovery event=tools_list_responded run_id=run_01JX tool_count=0 reason=no_enabled_tools
```

Esse registro dá ao operador um problema de configuração para corrigir. Um erro genérico de inicialização dá apenas uma superstição para repetir.

## A acessibilidade externa precisa de uma prova própria

A pergunta «o processo alcançou um canal externo?» não pode ser respondida por um PID, um handshake bem-sucedido ou uma lista de ferramentas preenchida. Você precisa de um evento no limite em que seu código tenta realizar a ação externa.

Defina canal externo de forma restrita. Para este fim, ele inclui uma requisição HTTP de saída, uma chamada a um auxiliar SSH, uma conexão de banco de dados fora do processo local, uma atualização de credencial na nuvem, uma publicação em uma fila de mensagens ou qualquer chamada que possa criar um efeito ou revelar informações fora da sessão MCP. Ler um arquivo de configuração local não conta. Carregar uma credencial de um armazenamento local protegido também não conta por si só. Enviá-la em uma requisição conta.

Registre a tentativa antes de iniciar a chamada e depois registre o resultado. Use um ID de ação opaco que possa ser associado ao ID da requisição MCP e ao ID de execução do servidor.

```text
run_id=run_01JX phase=execution event=external_attempt action_id=act_8Qf tool=issue_lookup channel=https host=api.example.test
run_id=run_01JX phase=execution event=external_result action_id=act_8Qf status=200 duration_ms=184
```

Não coloque nesses registros cabeçalhos de autorização, tokens bearer, URLs assinadas, argumentos de comando com segredos ou corpos completos de resposta. Um log de incidente que vaza a credencial investigada torna o incidente pior.

A posição de `external_attempt` não é uma questão acadêmica. Coloque-o cedo demais e você afirmará que uma chamada externa ocorreu quando o código apenas montou um objeto de requisição. Coloque-o tarde demais e um timeout ou uma falha do processo poderá deixar uma lacuna depois que os bytes já saíram da máquina. Emita-o imediatamente antes da chamada da biblioteca que pode iniciar a atividade de rede ou SSH. Se a biblioteca expuser um hook de conexão ou requisição de nível inferior, registre um segundo evento ali apenas se puder fazer isso sem confundir o significado de «tentativa».

Um exemplo de falha mostra por que isso importa. Um operador adiciona um servidor MCP que lê um token do sistema de gestão de incidentes durante a inicialização do módulo e chama um endpoint «quem sou eu» para validá-lo. O processo filho inicia, escreve uma linha de depuração em stdout e corrompe a primeira mensagem MCP. O cliente mostra «o servidor falhou ao iniciar». A equipe reinicia o cliente duas vezes.

Sem registros por fase, a equipe conclui que nenhuma requisição saiu da máquina porque o servidor nunca apareceu na interface do cliente. Essa conclusão é falsa. A chamada de inicialização do módulo ocorreu antes de o cliente enviar `initialize` e acessou o sistema de incidentes três vezes. O estado da interface não dizia nada sobre a acessibilidade externa.

Mova a verificação de identidade para uma ferramenta deliberadamente somente leitura ou torne-a parte da primeira ação que realmente precise do serviço remoto. Depois, registre-a como execução de ferramenta. O servidor pode iniciar, ser inicializado e listar ferramentas sem tocar na rede. Agora o operador consegue distinguir «o servidor está disponível» de «a credencial e o serviço remoto funcionam». São fatos separados e devem continuar separados.

## Um livro-caixa de fases transforma incidentes vagos em afirmações testáveis

Crie uma entrada no livro-caixa para cada execução e acrescente eventos de fase imutáveis. Você não precisa de um mecanismo de regras complicado. Precisa de nomes estáveis, timestamps e campos de correlação suficientes para reconstruir o que aconteceu.

Use este formato:

```json
{
  "run_id": "run_01JX",
  "server_name": "acme",
  "pid": 84217,
  "phase": "discovery",
  "event": "tools_list_responded",
  "request_id": 2,
  "tool_count": 4,
  "at": "2026-07-22T14:03:13.083Z"
}
```

O livro-caixa deve registrar eventos, não conclusões coladas em uma string. `phase=handshake` e `event=initialize_received` podem ser contados, consultados e testados. `message="MCP parece travado"` não pode.

Mantenha o modelo de estados deliberadamente simples:

1. `spawn_requested`, `spawned`, `executable_ready` e `exited` pertencem à inicialização.
2. `initialize_received`, `initialize_responded` e `initialized_received` pertencem ao handshake.
3. `tools_list_received` e `tools_list_responded` pertencem à descoberta.
4. `tool_call_received`, `external_attempt` e `external_result` pertencem à execução.
5. `revoked`, `terminated` e `client_disconnected` descrevem interrupção, não sucesso.

A distinção que as equipes costumam misturar é **estabelecimento da sessão versus autorização para agir**. Um servidor pode estabelecer uma sessão MCP sem ter permissão para usar uma credencial ou se conectar a um sistema remoto. Se você tratar esses estados como um só, um evento de aprovação poderá parecer um evento de conectividade, e uma ação negada poderá parecer uma falha de inicialização.

Mantenha os eventos de autorização ao lado da chamada que governam. Por exemplo, registre `authorization_requested` e `authorization_granted` depois de `tool_call_received`, mas antes de `external_attempt`. Assim, o operador poderá dizer, com evidências, que a requisição da ferramenta chegou, que a pessoa a negou e que nenhuma tentativa externa ocorreu. Isso é muito mais forte do que dizer que a requisição «não foi concluída».

Use um ID de execução que exista apenas para um processo filho. Não reutilize o nome do servidor como identificador de correlação. Um cliente pode iniciar duas cópias do mesmo servidor, reiniciar uma delas depois de um timeout e manter metadados antigos das ferramentas. Identificadores reutilizados transformam essas tentativas separadas em uma história inventada.

Faça hash ou oculte valores que revelem dados do usuário. Em geral, você precisa do nome da ferramenta, do host do endpoint, da classe do status, da classe do erro e da duração. Raramente precisa da query string, do corpo da requisição ou da resposta. O operador precisa provar que o limite foi atravessado, não reproduzir dados do usuário a partir dos logs.

## Teste os limites sem confiar no cliente completo

Um cliente completo é útil para testes de integração, mas é uma primeira testemunha ruim. Sua interface pode compactar erros, armazenar capacidades em cache, reiniciar processos filhos e aplicar seu próprio timeout. Teste cada limite por um caminho mais estreito antes de culpar o servidor ou o cliente.

Comece com o comando, o ambiente e o diretório de trabalho exatos usados pelo cliente. Não substitua o comando configurado por `npm run dev`. Não o execute na pasta do projeto se o cliente o inicia de outro lugar. Redirecione stderr para um arquivo para inspeção, mas deixe stdout intocado se outro processo for falar MCP por ele.

Para um servidor stdio, use o MCP Inspector oficial como primeiro teste de protocolo. A documentação do MCP recomenda o Inspector para testar servidores em diferentes transportes, e o projeto Inspector consegue iniciar diretamente um comando stdio. Ele permite observar a troca de inicialização e chamar `tools/list` sem adivinhar o que um cliente de desktop fez com o resultado.

Depois, reduza o teste a três verificações:

```text
1. O comando configurado permanece ativo tempo suficiente para receber initialize?
2. Ele retorna uma resposta initialize válida e recebe initialized?
3. tools/list retorna os nomes e esquemas esperados das ferramentas?
```

Só depois que essas verificações passarem chame uma ferramenta que alcance um sistema externo. Escolha uma ação somente leitura com um alvo inofensivo. Confirme que o livro-caixa de execução contém um `external_attempt` e um resultado terminal. Se uma chamada puder alterar dados, teste-a em um ambiente descartável ou ofereça uma operação específica de simulação que não acesse o endpoint de produção.

O repositório oficial do MCP Inspector é útil aqui porque torna visível o limite do transporte. Ele não é um proxy de interceptação de rede para o tráfego do servidor. Atua como cliente MCP do servidor escolhido e fornece uma interface no navegador para o teste. Essa diferença importa ao investigar corrupção do transporte: o Inspector pode reproduzir o lado cliente do protocolo, mas não pode provar o que um cliente de produção separado escreveu no canal.

Para transportes HTTP, acrescente evidências HTTP sem confundi-las com o estado do MCP. Registre o método da requisição, o caminho do endpoint, o status, o identificador da sessão quando presente e se a resposta continha JSON ou começava um fluxo de eventos. Uma conexão TCP ou um HTTP 200 não significa automaticamente que a inicialização MCP foi concluída. Aplique os mesmos registros de ciclo depois que a requisição HTTP chegar ao servidor.

Mantenha na suíte de testes um servidor de fixture que falhe deliberadamente em cada limite. Um fixture termina antes de ler a entrada. Outro escreve `hello` em stdout antes de responder. Um terceiro responde a `initialize` e depois retorna um esquema de ferramenta inválido. Um quarto lista uma ferramenta cujo manipulador registra uma tentativa externa e retorna um erro controlado. Se a integração do cliente transformar os quatro casos no mesmo alerta, corrija a integração antes que um servidor real obrigue você a depurar às cegas.

## Timeouts e repetições precisam ter uma fase responsável

Um único timeout de inicialização incentiva a correção errada. Ele faz um download lento de pacote, um erro de análise da inicialização, uma exceção de esquema e uma espera na API remota parecerem iguais. Use prazos separados, porque cada prazo pertence a um responsável diferente.

O iniciador é responsável pelo período entre `spawn_requested` e `spawned`. O servidor e o transporte são responsáveis pelo período entre a criação do processo e `initialized_received`. O caminho de registro do servidor é responsável pela descoberta. O manipulador da ferramenta e sua dependência remota são responsáveis pela execução. Dê ao timeout o nome de seu responsável e emita o último evento de fase confirmado.

```text
error=handshake_timeout last_event=initialize_received run_id=run_01JX
```

Isso permite agir. Informa ao responsável pelo servidor que ele deve verificar a construção da resposta e stderr, não a API remota.

A repetição automática só é segura quando a fase que falhou não tem efeito externo. Repetir uma criação de processo que falhou porque o executável estava temporariamente indisponível pode ser aceitável. Repetir uma requisição de descoberta geralmente é aceitável se a descoberta for local e pura. Repetir uma chamada de ferramenta depois de `external_attempt` é perigoso, a menos que a operação remota tenha um mecanismo documentado de idempotência e você associe a ela um valor de idempotência.

Não esconda a repetição atrás de um reinício do servidor. Se o código de inicialização atualiza um token, cria um túnel, envia um evento de telemetria ou valida uma identidade remota, o reinício já é uma ação externa. Essa é outra razão para manter a inicialização local e mover o trabalho remoto para ferramentas explícitas.

Quando um cliente encerra um processo filho depois de um prazo, emita um evento de interrupção antes da terminação, se puder. O servidor pode não conseguir liberá-lo. O processo pai deve registrar a solicitação de encerramento como seu próprio evento, incluindo o último evento do filho que observou. Assim, o registro permanece honesto: o processo pode estar prestes a responder, mas você não afirma que respondeu.

## Torne a inicialização previsível antes de torná-la rápida

Um bom servidor MCP pode iniciar sem rede, sem usar credenciais, sem efeitos colaterais mutáveis e sem ambiguidade sobre o estado do protocolo. Ele carrega a configuração local, instala o transporte, responde à inicialização e anuncia uma interface determinística. Esse comportamento é mais fácil de operar e mais seguro de repetir.

A recomendação ruim mais popular é «verificar tudo na inicialização». Ela parece responsável porque os erros aparecem cedo. Na prática, mistura configuração local, identidade, disponibilidade remota e autorização em um ritual opaco. Também faz os clientes repetirem ações externas sob o rótulo de erro de inicialização.

Valide localmente o que puder. Informe a acessibilidade remota por meio de uma ferramenta explícita ou da primeira operação que precise dela. Mantenha estáveis os nomes das fases. Em stdio, faça stdout transportar apenas o protocolo. Teste um servidor deliberadamente quebrado em cada limite que você afirma observar.

Quando o próximo cliente disser que um servidor MCP falhou ao iniciar, você deverá conseguir responder a quatro perguntas usando um único registro de execução: o processo foi iniciado? A inicialização foi concluída? As ferramentas puderam ser descobertas? Algo chegou ao mundo externo? Se não conseguir responder às quatro, o status ainda é ambíguo.
