Nos bastidores do MCP do CaptainDNS

Por CaptainDNS
Publicado em 27 de novembro de 2025

  • #MCP
  • #Arquitetura
  • #DNS
  • #E-mail
  • #Integrações IA
TL;DR

TL;DR - 🔨 Antes de ligar o CaptainDNS ao ChatGPT via MCP, era preciso uma arquitetura clara: um servidor MCP dedicado, colocado entre os hosts (ChatGPT, ferramentas internas) e a API existente do CaptainDNS.

  • O servidor MCP não contém lógica de DNS ou e-mail: delega tudo ao backend do CaptainDNS através de uma API interna segura.
  • O transporte MCP usa HTTP + JSON-RPC, com um único ponto de entrada /stream alinhado ao modelo HTTP+SSE moderno.
  • As ferramentas expostas (dns_lookup, dns_propagation, email_auth_audit) são tipadas para que a IA possa descobri-las e chamá-las sozinha.
  • Os primeiros testes revelaram timeouts, erros 424 e sutilezas do protocolo (notifications, tools/list, tools/call) que obrigaram a endurecer o contrato.
  • Este artigo detalha como estruturamos a arquitetura, protegemos as trocas e depuramos toda a cadeia.

Por que um servidor MCP dedicado para o CaptainDNS?

Do ponto de vista de produto, o CaptainDNS continua sendo um SaaS clássico: uma interface web Next.js (frontend) e uma API Go (services/api) que concentra a lógica de negócio (DNS, e-mail, resolvedores, registro, scoring).

Adicionar MCP não significa expor a API diretamente ao modelo: optamos por inserir um servidor MCP dedicado (services/mcp-server) que atua como adaptador.

Na prática:

  • services/api permanece a única fonte de verdade (DNS, propagação, e-mail).
  • services/mcp-server é um cliente forte dessa API:
    • autentica-se com um token de serviço para provar que vem da infraestrutura CaptainDNS;
    • repassa um token Auth0 de usuário, quando existe, para respeitar quotas, registro e permissões.
  • Os hosts (ChatGPT, ferramentas internas, agentes) veem apenas o servidor MCP e falam MCP/JSON-RPC, nunca a API bruta.

Essa separação permite:

  • desacoplar a evolução da API da evolução do contrato MCP;
  • adicionar proteções específicas ao mundo de IA (rate limiting, controles de formato, timeouts agressivos); e
  • manter uma arquitetura limpa.

Visão geral da arquitetura

Em alto nível, a arquitetura fica assim:

  • Hosts MCP: ChatGPT (Connectors), ferramentas internas, outros clientes MCP.
  • Servidor MCP do CaptainDNS:
    • expõe ferramentas MCP (dns_lookup, dns_propagation, email_auth_audit, etc.) via JSON-RPC;
    • valida e normaliza entradas (domínios, tipos de registro, seletores DKIM);
    • aplica timeouts, quotas e classificação de erros.
  • API do CaptainDNS (services/api):
    • endpoints /resolve, /resolve/propagation, mail/domain-check;
    • banco de dados e resolvedores;
    • logs, scoring, perfil do usuário.

O servidor MCP é uma ponte de protocolo: converte chamadas MCP (tools/list, tools/call) em chamadas HTTP internas e reempacota a resposta num formato utilizável pelo cliente MCP e pelo modelo.

Contrato MCP: initialize, tools/list, tools/call

O protocolo se apoia em JSON-RPC 2.0 e três métodos principais do lado do servidor:

  • initialize: negociação da versão do protocolo e das capacidades.
  • tools/list: descoberta das ferramentas disponíveis.
  • tools/call: execução de uma ferramenta nomeada com argumentos tipados.

initialize: dizer quem você é e o que suporta

Quando um host (por exemplo, o ChatGPT) abre uma sessão com o servidor MCP do CaptainDNS, ele começa enviando:

  • method: "initialize";
  • params.protocolVersion: uma versão de protocolo (por exemplo "2025-06-18");
  • params.clientInfo: nome e versão do cliente (openai-mcp, 1.0.0, etc.).

O servidor MCP responde com:

  • protocolVersion: a versão que aceita (geralmente a mesma do cliente);
  • capabilities: especialmente tools: { listChanged: false } para indicar uma lista de ferramentas estável;
  • serverInfo: nome ("captaindns-mcp-server") e versão ("0.1.0") do servidor.

Nesse ponto nenhuma chamada de negócio saiu para a API do CaptainDNS: trata-se apenas de alinhar versão de protocolo e capacidades gerais.

tools/list: anunciar as ferramentas do CaptainDNS

Depois de initialize, o cliente envia tools/list. O MCP do CaptainDNS responde com um array de definições de ferramentas, cada uma contendo:

  • name: identificador da ferramenta, por exemplo dns_lookup, dns_propagation, email_auth_audit;
  • description: o que a ferramenta faz, em linguagem natural;
  • inputSchema: um esquema JSON descrevendo os argumentos esperados;
  • annotations: metadados (tags, escopos recomendados como captaindns:dns:read).

As ferramentas do CaptainDNS são intencionalmente somente leitura: consultam DNS ou configuração de e-mail, mas não alteram nada.

Exemplos de parâmetros tipados:

  • dns_lookup:
    • domain (obrigatório);
    • record_type (enum A, AAAA, TXT, MX, etc.);
    • resolver_preset (opcional);
    • trace (booleano) para pedir uma trilha iterativa.
  • dns_propagation:
    • mesma lógica, aplicada a um varrimento de vários resolvedores.
  • email_auth_audit:
    • domain (obrigatório);
    • rp_domain (opcional, para reports/policies);
    • dkim_selectors (lista opcional de seletores a sondar).

tools/call: executar uma ferramenta de negócio

Quando o modelo decide usar uma ferramenta, ele não faz um dns_lookup direto: envia um tools/call com:

  • params.name: nome da ferramenta ("dns_lookup", "dns_propagation", "email_auth_audit");
  • params.arguments: um objeto JSON conforme o inputSchema.

O servidor MCP:

  1. valida e normaliza os argumentos (domínios, tipos DNS, etc.);
  2. chama o endpoint backend correto (/resolve, /resolve/propagation, /mail/domain-check) via um cliente interno;
  3. reempacota a resposta em um CallToolResult com:
    • structuredContent: a resposta estruturada do CaptainDNS (respostas DNS, pontuação de e-mail, detalhes SPF/DKIM/DMARC/BIMI, etc.);
    • opcionalmente content com um resumo em texto para a IA;
    • isError: false se tudo funcionou, true se a ferramenta foi executada mas retornou um erro de negócio (por exemplo, timeout de DNS).

Erros estruturais (ferramenta desconhecida, parâmetros inválidos, JSON malformado) continuam sendo erros JSON-RPC clássicos (-32601, -32602, etc.), permitindo ao cliente MCP distinguir claramente problemas de protocolo de problemas de negócio.

Transporte MCP: HTTP + JSON-RPC e SSE

Para esta primeira versão, escolhemos o transporte moderno baseado em HTTP + JSON-RPC, com suporte SSE opcional.

Diagrama do transporte MCP do CaptainDNS

Ponto de entrada: POST /stream

O ponto de entrada principal do servidor MCP é um endpoint HTTP:

  • POST /stream com um corpo JSON-RPC 2.0.

Os clientes modernos usam para:

  • abrir a sessão;
  • enviar initialize;
  • encadear tools/list e tools/call.

O servidor MCP:

  • lê a requisição JSON-RPC (jsonrpc, id, method, params);
  • roteia para o handler apropriado (initialize, tools/list, tools/call);
  • retorna uma resposta JSON-RPC estruturada:
    • result (sucesso);
    • ou error (falha de protocolo).

SSE: compatibilidade e introspecção

Para clientes mais antigos ou cenários específicos, o servidor expõe também:

  • GET /stream com Accept: text/event-stream;

Esse fluxo SSE:

  • anuncia as metadados básicas (endpoint HTTP para as requisições);
  • pode exibir uma lista simplificada de ferramentas;
  • envia pings regulares para manter as conexões sob controle.

Na prática, a integração com o ChatGPT se apoia principalmente no POST /stream JSON-RPC. As primeiras falhas (timeouts, 424, etc.) vinham de um handshake SSE incompleto; convergir para um único fluxo JSON-RPC bem definido resolveu o problema.

Interações servidor MCP ↔ API do CaptainDNS

Para cada tools/call, o servidor MCP atua como cliente autenticado da API do CaptainDNS.

Autenticação e identidade

O MCP envia sempre:

  • um token de serviço para que a API reconheça uma origem interna confiável;
  • um token de usuário, quando o host MCP fornece, para que a API possa:
    • vincular as requisições a um perfil (para logs e histórico);
    • aplicar regras de quota ou permissões.

O servidor MCP não armazena dados de usuário a longo prazo: apenas propaga a identidade até a API, que permanece a autoridade sobre perfil e registros.

Cliente interno e normalização

Todas as requisições de saída passam por um único cliente interno que:

  • normaliza domínios (example.com, sem ponto final, em minúsculas);
  • valida tipos de registro (A, AAAA, TXT, etc.);
  • restringe o escopo das ferramentas (seletores DKIM, presets de resolvedores);
  • aplica timeouts ajustados por ferramenta (dns_lookup mais curto que email_auth_audit ou dns_propagation).

O servidor MCP nunca faz chamadas HTTP brutas fora desse cliente: isso facilita manutenção e segurança.

Classificação de erros

Os erros são classificados em três famílias:

  • input: parâmetros inválidos ou ausentes (domínio malformado, tipo de registro não suportado, falta de token, etc.);
  • business: problema de negócio (timeout de DNS, resolvedor inacessível, upstream de e-mail lento, etc.);
  • internal: falha interna (bug, configuração ausente, API em erro 5xx).

No /stream (JSON-RPC), os erros aparecem como códigos padrão (-32602, -32001, -32603) com detalhes em error.data. Nos resultados das ferramentas, um erro de negócio pode surgir como isError: true com um structuredContent dedicado.

Notas de campo: timeouts, 424 e outras surpresas

A teoria do MCP é elegante; a prática trouxe bugs e surpresas. Aqui estão os principais problemas que encontramos e como os resolvemos.

1. O timeout silencioso ao adicionar o servidor

Primeiro passo: declarar o servidor MCP no ChatGPT. No início, a URL configurada apontava para:

  • um servidor ouvindo apenas em 127.0.0.1, ou
  • um endpoint HTTP que não implementava MCP de verdade.

Resultado:

  • nenhum log initialize aparecia do lado MCP;
  • o ChatGPT acabava exibindo um simples timeout: "unable to connect to the MCP server".

Causas:

  • servidores ouvindo apenas em 127.0.0.1 em vez de 0.0.0.0 atrás de um reverse proxy;
  • uso de localhost/IPs privados na configuração (inacessíveis pela nuvem do ChatGPT).

Lição: antes do protocolo, confira o básico: DNS público, HTTPS, escutar em um endereço alcançável e ver logs no MCP assim que você tenta conectar.

2. SSE sem handshake completo: a conexão que fica aberta... e depois cai

Quando a URL MCP já era alcançável, os primeiros logs mostravam:

  • um POST /stream retornado em 405 (method not allowed);
  • um fallback para GET /stream com Accept: text/event-stream;
  • uma conexão SSE aceita e fechada depois de ~2 minutos.

No papel, "funcionava" (havia uma conexão SSE), mas o ChatGPT nunca conseguia inicializar a sessão MCP. Motivo:

  • o fluxo SSE enviava pings, mas não o handshake esperado (sem evento endpoint, sem informação sobre a URL JSON-RPC).

Solução: simplificar o transporte com uma entrada principal clara:

  • POST /stream para tudo o que é JSON-RPC (initialize, tools/list, tools/call);
  • GET /stream SSE como canal secundário de introspecção, não essencial para o ChatGPT.

Convergir para um único ponto de entrada JSON-RPC robusto eliminou a maioria dos timeouts misteriosos.

3. initialize incompleto: quando o cliente espera mais metadados

Versão seguinte: a conexão se estabelecia, mas o cliente MCP quebrava em initialize. Os logs mostravam:

  • um initialize de entrada correto (com protocolVersion e clientInfo);
  • uma resposta que já continha a lista de ferramentas e às vezes um capabilities.tools mal tipado.

Problemas identificados:

  • ausência de protocolVersion em result;
  • ausência de serverInfo;
  • capabilities.tools retornado como booleano (true) em vez de objeto ({listChanged:false});
  • inclusão de campos não padrão (lista de tools, endpoints custom) na resposta initialize.

Solução:

  • normalizar a resposta initialize:
    • eco de protocolVersion;
    • capabilities.tools = { listChanged: false };
    • serverInfo mínimo (name, version);
  • mover a lista de ferramentas para tools/list.

Com o contrato respeitado, o cliente pôde encadear notifications/initialized e depois tools/list sem erros.

4. Responder a uma notification: uma boa forma de derrubar o cliente

Outra surpresa: notifications/initialized é uma notificação JSON-RPC:

  • sem id;
  • o cliente não espera resposta.

Uma versão inicial do servidor respondia mesmo assim com:

  • uma pseudo-resposta JSON-RPC contendo id: null.

Para um cliente estrito, isso parece uma resposta a uma requisição que não existe, gerando erros como "unhandled errors in a TaskGroup".

Solução: aplicar a regra simples:

  • se a requisição não tem id (notification):
    • registre em log;
    • atualize o estado interno se necessário;
    • mas não envie nada no fluxo.

Esse detalhe foi suficiente para eliminar uma classe inteira de erros do lado do cliente.

5. tools/list e inputSchema vs input_schema

Nos primeiros testes, o ChatGPT chegava até tools/list, mas falhava ao construir as ferramentas internas. A causa:

  • a resposta tools/list usava a propriedade input_schema em snake_case, em vez de inputSchema em camelCase.

Mesmo com o esquema JSON correto, um cliente tipado espera estritamente inputSchema. Com input_schema, o campo era considerado ausente: impossível gerar formulários ou validar argumentos.

Solução: renomear sistematicamente a chave para inputSchema nas definições de ferramentas.

6. tools/call: a ferramenta desconhecida que não é

Passo seguinte: estabilizado tools/list, as chamadas tentavam tools/call... e recebiam sistematicamente:

  • um JSON-RPC method not found;
  • um código interno ERR_UNKNOWN_TOOL.

O servidor ainda buscava um método correspondente a dns_lookup ou dns_propagation, quando o protocolo MCP impõe um único método tools/call com um parâmetro name.

Solução:

  • adicionar um handler dedicado tools/call;
  • roteá-lo para as ferramentas corretas conforme params.name;
  • validar os parâmetros com o inputSchema correspondente;
  • retornar um CallToolResult estruturado.

A partir daí, as ferramentas do CaptainDNS finalmente começaram a ser executadas via MCP.

7. content type "json" vs structuredContent: detalhes que quebram integrações

Em uma versão intermediária, o servidor MCP retornava os resultados assim:

  • result.content[0].type = "json";
  • result.content[0].json = { ... }.

É conveniente do lado do servidor, mas não corresponde a um tipo de bloco padrão do protocolo (que define principalmente text, image, resource, etc.). Alguns clientes tolerantes interpretam, outros não.

No ChatGPT, isso podia aparecer como erros internos encapsulados como:

  • http_error 424;
  • unhandled errors in a TaskGroup (1 sub-exception).

Solução:

  • mover o payload estruturado para structuredContent;
  • manter content para um resumo em texto opcional (fácil de mostrar ao usuário);
  • usar isError para sinalizar claramente se o resultado de negócio é sucesso ou falha.

Com esse esquema, desapareceram os 424 ligados ao mapeamento de conteúdo.

FAQ: timeouts, 424 e boas práticas MCP

Perguntas frequentes sobre o MCP do CaptainDNS

Quais pré-requisitos verificar se falhar ao adicionar o servidor MCP?

Comece pelo básico:

  • A URL aponta para um endpoint HTTPS público, não para localhost ou um IP privado.
  • POST /stream responde (sem 405) e você vê um log initialize no servidor.
  • O servidor escuta em 0.0.0.0 atrás do reverse proxy, não apenas em 127.0.0.1.
  • SSE (GET /stream) é opcional: não trave nisso se o JSON-RPC funciona.

Se initialize nunca aparece nos logs, é problema de rede (DNS, TLS, firewall), não de protocolo.

Como tratar um `http_error 424` ou `unhandled errors in a TaskGroup`?

Isso costuma sinalizar uma resposta fora do contrato MCP:

  • coloque a payload estruturada em structuredContent e deixe content para um resumo em texto;
  • não responda a notificações (notifications/initialized não tem id);
  • retorne um único CallToolResult com isError explícito em vez de blocos customizados type: \"json\".

Se os logs mostram um tools/call bem-sucedido mas o cliente falha, confira primeiro esses pontos.

Por que rotear todas as ferramentas via `tools/call`?

O MCP exige um único método de negócio (tools/call) com params.name:

  • no servidor, implemente tools/call e roteie para dns_lookup, dns_propagation, email_auth_audit, validando com o inputSchema correspondente;
  • no cliente, garanta que params.name corresponde exatamente a um name exposto por tools/list.

Métodos por ferramenta ou nomes incorretos levam a method not found ou ERR_UNKNOWN_TOOL.

Como diagnosticar um timeout ou uma latência incomum?

Os timeouts costumam vir do caminho de rede, não do protocolo:

  • revise os timeouts por ferramenta no MCP (dns_lookup mais curto que dns_propagation);
  • reduza um sweep de resolvedores para testar e depois amplie;
  • veja os logs do backend (DNS lentos, upstream mail) e a presença do token de serviço.

Um timeout sem initialize registrado continua sendo problema de acessibilidade de rede.

Basta HTTP JSON-RPC ou preciso de SSE?

Por padrão, mantenha um único ponto de entrada JSON-RPC (POST /stream):

  • é o caminho mais estável para o ChatGPT e clientes modernos;
  • o discovery e as chamadas das ferramentas passam por ali.

Adicione SSE (GET /stream) apenas se precisar de introspecção; nesse caso, garanta que o fluxo fornece o endpoint e pings regulares.

Glossário MCP e CaptainDNS

MCP (Model Context Protocol)

Protocolo aberto que padroniza como um modelo de IA conversa com ferramentas externas: bancos de dados, APIs, serviços como o CaptainDNS. Define conceitos como initialize, tools/list, tools/call e formatos de resposta tipados (CallToolResult, ContentBlock, structuredContent).

Host

Aplicação que incorpora um modelo de IA e um cliente MCP: ChatGPT, um IDE, um agente interno. É o host que decide chamar uma ferramenta do CaptainDNS via MCP em resposta às instruções do usuário.

Servidor MCP

Serviço que expõe ferramentas MCP utilizáveis pelos hosts. No nosso caso: captaindns-mcp-server, um serviço Go que conhece a superfície da API do CaptainDNS e a traduz para o formato MCP.

JSON-RPC 2.0

Protocolo leve baseado em JSON, usado pelo MCP para descrever requisições (method, params, id) e respostas (result ou error). O MCP o usa de forma delimitada (initialize, tools/list, tools/call, notifications).

tools/list

Método MCP que retorna a lista de ferramentas disponíveis em um servidor MCP, com seus inputSchema e metadados. É o ponto de partida para o modelo saber o que pode fazer com o CaptainDNS.

tools/call

Método MCP que um host usa para executar uma ferramenta específica. O servidor MCP lê params.name, valida params.arguments, chama a API backend e devolve um CallToolResult que representa o resultado estruturado (ou o erro de negócio).

structuredContent

Campo opcional do CallToolResult onde um servidor MCP pode colocar dados estruturados (JSON) gerados pela execução de uma ferramenta. O CaptainDNS coloca ali, por exemplo, respostas DNS, pontuação de e-mail, detalhes SPF/DKIM/DMARC/BIMI.

TaskGroup

Conceito de runtimes assíncronos (Python, etc.): um grupo de tarefas executadas em paralelo. Uma mensagem como "unhandled errors in a TaskGroup" indica que ocorreu uma exceção não tratada em uma ou mais dessas tarefas, geralmente por uma incompatibilidade sutil de formato ou por um bug na cadeia.

Artigos relacionados

CaptainDNS · 21 de novembro de 2025

Um MCP para o CaptainDNS?

Antes de ligar o CaptainDNS a IAs, é preciso entender o que é o Model Context Protocol (MCP) e o que ele realmente permite. Um ABC do MCP e os primeiros passos para o CaptainDNS.

  • #MCP
  • #IA
  • #DNS
  • #E-mail
  • #Arquitetura