Nos bastidores do MCP do CaptainDNS
Por CaptainDNS
Publicado em 27 de novembro de 2025
- #MCP
- #Arquitetura
- #DNS
- #Integrações IA
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
/streamalinhado 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/apipermanece 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.
- expõe ferramentas MCP (
- API do CaptainDNS (
services/api):- endpoints
/resolve,/resolve/propagation,mail/domain-check; - banco de dados e resolvedores;
- logs, scoring, perfil do usuário.
- endpoints
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: especialmentetools: { 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 exemplodns_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 comocaptaindns: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(enumA,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 oinputSchema.
O servidor MCP:
- valida e normaliza os argumentos (domínios, tipos DNS, etc.);
- chama o endpoint backend correto (
/resolve,/resolve/propagation,/mail/domain-check) via um cliente interno; - reempacota a resposta em um
CallToolResultcom:structuredContent: a resposta estruturada do CaptainDNS (respostas DNS, pontuação de e-mail, detalhes SPF/DKIM/DMARC/BIMI, etc.);- opcionalmente
contentcom um resumo em texto para a IA; isError:falsese tudo funcionou,truese 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.
Ponto de entrada: POST /stream
O ponto de entrada principal do servidor MCP é um endpoint HTTP:
POST /streamcom um corpo JSON-RPC 2.0.
Os clientes modernos usam para:
- abrir a sessão;
- enviar
initialize; - encadear
tools/listetools/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 /streamcomAccept: 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_lookupmais curto queemail_auth_auditoudns_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
initializeaparecia 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.1em vez de0.0.0.0atrá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 /streamretornado em405(method not allowed); - um fallback para
GET /streamcomAccept: 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 /streampara tudo o que é JSON-RPC (initialize, tools/list, tools/call);GET /streamSSE 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
initializede entrada correto (comprotocolVersioneclientInfo); - uma resposta que já continha a lista de ferramentas e às vezes um
capabilities.toolsmal tipado.
Problemas identificados:
- ausência de
protocolVersionemresult; - ausência de
serverInfo; capabilities.toolsretornado 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 };serverInfomínimo (name,version);
- eco de
- 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/listusava a propriedadeinput_schemaem snake_case, em vez deinputSchemaem 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
inputSchemacorrespondente; - retornar um
CallToolResultestruturado.
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
contentpara um resumo em texto opcional (fácil de mostrar ao usuário); - usar
isErrorpara 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
localhostou um IP privado. POST /streamresponde (sem405) e você vê um loginitializeno servidor.- O servidor escuta em
0.0.0.0atrás do reverse proxy, não apenas em127.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
structuredContente deixecontentpara um resumo em texto; - não responda a notificações (
notifications/initializednão temid); - retorne um único
CallToolResultcomisErrorexplícito em vez de blocos customizadostype: \"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/calle roteie paradns_lookup,dns_propagation,email_auth_audit, validando com oinputSchemacorrespondente; - no cliente, garanta que
params.namecorresponde exatamente a umnameexposto portools/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_lookupmais curto quedns_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.