Entre bastidores del MCP de CaptainDNS
Por CaptainDNS
Publicado el 27 de noviembre de 2025
- #MCP
- #Arquitectura
- #DNS
- #Correo
- #Integraciones IA
TL;DR - 🔨 Antes de enchufar CaptainDNS a ChatGPT vía MCP, hacía falta una arquitectura clara: un servidor MCP dedicado, situado entre los hosts (ChatGPT, herramientas internas) y la API existente de CaptainDNS.
- El servidor MCP no contiene lógica DNS ni de correo: delega todo en el backend de CaptainDNS mediante una API interna segura.
- El transporte MCP se apoya en HTTP + JSON-RPC, con un único punto de entrada
/streamalineado con el modelo HTTP+SSE moderno. - Las herramientas expuestas (
dns_lookup,dns_propagation,email_auth_audit) están tipadas para que la IA pueda descubrirlas y llamarlas sola. - Las primeras pruebas sacaron a la luz timeouts, errores 424 y sutilezas del protocolo (notifications, tools/list, tools/call) que obligaron a endurecer el contrato.
- Este artículo detalla cómo estructuramos la arquitectura, aseguramos los intercambios y depuramos la cadena de extremo a extremo.
¿Por qué un servidor MCP dedicado para CaptainDNS?
Desde el punto de vista producto, CaptainDNS sigue siendo un SaaS clásico: una interfaz web Next.js (frontend) y una API Go (services/api) que lleva la lógica de negocio (DNS, correo, resolutores, registro, scoring).
Añadir MCP no significa exponer la API directamente al modelo: elegimos intercalar un servidor MCP dedicado (services/mcp-server) que actúa como adaptador.
En la práctica:
services/apisigue siendo la única fuente de verdad (DNS, propagación, correo).services/mcp-serveres un cliente fuerte de esa API:- se presenta con un token de servicio para probar que viene de la infraestructura CaptainDNS;
- propaga un token Auth0 de usuario cuando existe para respetar cuotas, registro y permisos.
- Los hosts (ChatGPT, herramientas internas, agentes) solo ven el servidor MCP y hablan MCP/JSON-RPC, nunca la API bruta.
Esta separación permite:
- desacoplar la evolución de la API de la del contrato MCP;
- añadir cortafuegos específicos para el mundo IA (rate limiting, controles de formato, timeouts agresivos); y
- mantener una arquitectura limpia.
Visión general de la arquitectura
A alto nivel, la arquitectura se ve así:
- Hosts MCP: ChatGPT (Connectors), herramientas internas, otros clientes MCP.
- Servidor MCP de CaptainDNS:
- expone las herramientas MCP (
dns_lookup,dns_propagation,email_auth_audit, etc.) vía JSON-RPC; - valida y normaliza las entradas (dominios, tipos de registro, selectores DKIM);
- aplica timeouts, cuotas y clasificación de errores.
- expone las herramientas MCP (
- API de CaptainDNS (
services/api):- endpoints
/resolve,/resolve/propagation,mail/domain-check; - base de datos y resolutores;
- logs, scoring, perfil usuario.
- endpoints
El servidor MCP es un puente de protocolo: traduce llamadas MCP (tools/list, tools/call) en llamadas HTTP internas y vuelve a empaquetar la respuesta en un formato utilizable por el cliente MCP y el modelo.
Contrato MCP: initialize, tools/list, tools/call
El protocolo se apoya en JSON-RPC 2.0 y tres métodos principales en el servidor:
initialize: negociación de versión de protocolo y capacidades.tools/list: descubrimiento de las herramientas disponibles.tools/call: ejecución de una herramienta nombrada con argumentos tipados.
initialize: decir quién eres y qué soportas
Cuando un host (ChatGPT, por ejemplo) abre sesión con el servidor MCP de CaptainDNS, empieza enviando:
method: "initialize";params.protocolVersion: una versión de protocolo (por ejemplo"2025-06-18");params.clientInfo: nombre y versión del cliente (openai-mcp,1.0.0, etc.).
El servidor MCP responde con:
protocolVersion: la versión que acepta (a menudo la misma que el cliente);capabilities: en particulartools: { listChanged: false }para indicar una lista de herramientas estable;serverInfo: nombre ("captaindns-mcp-server") y versión ("0.1.0") del servidor.
En este punto aún no ha salido ninguna llamada de negocio hacia la API de CaptainDNS: solo se pacta la versión del protocolo y las capacidades generales.
tools/list: anunciar las herramientas de CaptainDNS
Una vez validado initialize, el cliente envía tools/list. El MCP de CaptainDNS responde con un array de definiciones de herramientas, cada una con:
name: identificador de la herramienta, por ejemplodns_lookup,dns_propagation,email_auth_audit;description: lo que hace la herramienta, en lenguaje natural;inputSchema: un esquema JSON que describe con precisión los argumentos esperados;annotations: metadatos (etiquetas, scopes recomendados comocaptaindns:dns:read).
Las herramientas de CaptainDNS son intencionadamente solo lectura: consultan DNS o la configuración de correo, pero no modifican nada.
Ejemplos de parámetros tipados:
dns_lookup:domain(obligatorio);record_type(enumA,AAAA,TXT,MX, etc.);resolver_preset(opcional);trace(booleano) para pedir una traza iterativa.
dns_propagation:- misma idea, aplicada a un barrido de varios resolutores.
email_auth_audit:domain(obligatorio);rp_domain(opcional, para reports/policies);dkim_selectors(lista opcional de selectores a sondear).
tools/call: ejecutar una herramienta de negocio
Cuando el modelo decide usar una herramienta, no hace un dns_lookup directo: envía un tools/call con:
params.name: nombre de la herramienta ("dns_lookup","dns_propagation","email_auth_audit");params.arguments: un objeto JSON conforme alinputSchema.
El servidor MCP:
- Valida y normaliza los argumentos (dominios, tipos DNS, etc.).
- Llama al endpoint backend adecuado (
/resolve,/resolve/propagation,/mail/domain-check) mediante un cliente interno. - Reempaqueta la respuesta en un
CallToolResultcon:structuredContent: la respuesta estructurada de CaptainDNS (respuestas DNS, puntuación de correo, detalles SPF/DKIM/DMARC/BIMI, etc.);- opcionalmente
contentcon un resumen de texto para la IA; isError:falsesi todo funcionó,truesi la herramienta se ejecutó pero devolvió un error de negocio (p. ej. timeout DNS).
Los errores estructurales (herramienta desconocida, parámetros inválidos, JSON mal formado) siguen siendo errores JSON-RPC clásicos (-32601, -32602, etc.), lo que permite al cliente MCP distinguir claramente problemas de protocolo de problemas de negocio.
Transporte MCP: HTTP + JSON-RPC y SSE
Para esta primera versión optamos por el transporte moderno basado en HTTP + JSON-RPC, con soporte SSE opcional.
Punto de entrada: POST /stream
La entrada principal del servidor MCP es un endpoint HTTP:
POST /streamcon un cuerpo JSON-RPC 2.0.
Los clientes modernos lo usan para:
- abrir la sesión;
- enviar
initialize; - encadenar
tools/listytools/call.
El servidor MCP:
- lee la petición JSON-RPC (
jsonrpc,id,method,params); - enruta al handler adecuado (
initialize,tools/list,tools/call); - devuelve una respuesta JSON-RPC estructurada:
result(éxito);- o
error(fallo de protocolo).
SSE: compatibilidad e introspección
Para clientes más antiguos o casos específicos, el servidor expone también:
GET /streamconAccept: text/event-stream;
Este flujo SSE:
- anuncia las metadatos básicas (endpoint HTTP para las peticiones);
- puede difundir una lista simplificada de herramientas;
- envía pings regulares para mantener las conexiones bajo control.
En la práctica, la integración con ChatGPT se apoya sobre todo en el POST /stream JSON-RPC. Los primeros fallos (timeouts, 424, etc.) venían de un handshake SSE incompleto; converger hacia un único flujo JSON-RPC bien definido los eliminó.
Interacciones servidor MCP ↔ API de CaptainDNS
Para cada tools/call, el servidor MCP actúa como cliente autenticado de la API de CaptainDNS.
Autenticación e identidad
El MCP envía siempre:
- un token de servicio para que la API reconozca un origen interno de confianza;
- un token de usuario cuando el host MCP lo proporciona, para que la API pueda:
- vincular las peticiones a un perfil (para logs e historial);
- aplicar reglas de cuotas o permisos.
El servidor MCP no almacena datos de usuario a largo plazo: solo propaga la identidad hasta la API, que sigue siendo la autoridad sobre perfil y registros.
Cliente interno y normalización
Todas las peticiones salientes pasan por un único cliente interno que:
- normaliza los dominios (
example.com, sin punto final, en minúsculas); - valida los tipos de registro (
A,AAAA,TXT, etc.); - limita el alcance de las herramientas (selectores DKIM, presets de resolutores);
- aplica timeouts ajustados a cada herramienta (
dns_lookupmás corto queemail_auth_auditodns_propagation).
El servidor MCP nunca hace llamadas HTTP en bruto fuera de este cliente: facilita el mantenimiento y la seguridad.
Clasificación de errores
Los errores se clasifican en tres familias:
input: parámetros inválidos o ausentes (dominio mal formado, tipo de registro no soportado, falta de token, etc.);business: problema de negocio (timeout DNS, resolutor inalcanzable, upstream de correo lento, etc.);internal: fallo interno (bug, configuración ausente, API en error 5xx).
En /stream (JSON-RPC), los errores se reflejan como códigos estándar (-32602, -32001, -32603) con detalles en error.data. En resultados de herramienta, un fallo de negocio puede materializarse con isError: true y un structuredContent específico.
Notas de campo: timeouts, 424 y otras sorpresas
La teoría MCP es elegante; la práctica vino con bugs y sorpresas. Aquí van los principales problemas que encontramos y cómo los solucionamos.
1. El timeout silencioso al añadir el servidor
Primer paso: declarar el servidor MCP en ChatGPT. Al principio la URL configurada apuntaba a:
- un servidor que solo escuchaba en
127.0.0.1, o - un endpoint HTTP que no implementaba realmente MCP.
Resultado:
- no aparecía ningún log
initializedel lado MCP; - ChatGPT terminaba mostrando un simple timeout: "imposible conectar con el servidor MCP".
Causas:
- servidores escuchando solo en
127.0.0.1en lugar de0.0.0.0detrás de un reverse proxy; - uso de
localhost/IPs privadas en la configuración (inaccesibles desde la nube de ChatGPT).
Lección: antes de hablar de protocolo, comprueba lo básico: DNS público, HTTPS, escucha en una dirección alcanzable y trazas visibles en el MCP en cuanto intentas conectar.
2. SSE sin handshake completo: la conexión que se queda abierta... y luego muere
Cuando la URL MCP ya era alcanzable, los primeros logs mostraban:
- un
POST /streamrespondido en405(método no permitido); - un fallback a
GET /streamconAccept: text/event-stream; - una conexión SSE aceptada y cerrada a los ~2 minutos.
Sobre el papel "funcionaba" (existía una conexión SSE), pero ChatGPT nunca conseguía inicializar la sesión MCP. La razón:
- el flujo SSE enviaba pings, pero no el handshake esperado (sin evento
endpoint, sin información sobre la URL JSON-RPC).
Solución: simplificar el transporte con una entrada principal clara:
POST /streampara todo lo JSON-RPC (initialize, tools/list, tools/call);GET /streamSSE como canal secundario de introspección, no esencial para ChatGPT.
Convergir hacia un único punto de entrada JSON-RPC robusto eliminó la mayoría de timeouts misteriosos.
3. initialize incompleto: cuando el cliente espera más metadatos
Versión siguiente: la conexión se establecía, pero el cliente MCP se caía en initialize. Los logs mostraban:
- un
initializeentrante correcto (conprotocolVersionyclientInfo); - una respuesta que ya contenía la lista de herramientas y a veces un
capabilities.toolsmal tipado.
Problemas detectados:
- falta
protocolVersionenresult; - falta
serverInfo; capabilities.toolsdevuelto como booleano (true) en lugar de objeto ({listChanged:false});- inclusión de campos no estándar (lista de tools, endpoints custom) en la respuesta
initialize.
Solución:
- normalizar la respuesta
initialize:- eco de
protocolVersion; capabilities.tools = { listChanged: false };serverInfomínimo (name,version);
- eco de
- mover la lista de herramientas a
tools/list.
Una vez respetado el contrato, el cliente pudo encadenar notifications/initialized y luego tools/list sin errores.
4. Responder a una notification: una buena forma de romper el cliente
Otra sorpresa: notifications/initialized es una notificación JSON-RPC:
- sin
id; - el cliente no espera respuesta.
Una versión temprana del servidor respondía aun así con:
- una pseudo respuesta JSON-RPC con
id: null.
Para un cliente estricto eso parece una respuesta a una petición inexistente, de ahí errores tipo "unhandled errors in a TaskGroup".
Solución: aplicar la regla simple:
- si la petición no tiene
id(notification):- se registra en logs;
- se actualiza estado interno si hace falta;
- pero no se devuelve nada en el flujo.
Ese detalle bastó para hacer desaparecer toda una clase de errores del lado cliente.
5. tools/list e inputSchema vs input_schema
En las primeras pruebas, ChatGPT llegaba hasta tools/list, pero fallaba al construir las herramientas internas. Motivo:
- la respuesta
tools/listusaba la propiedadinput_schemaen snake_case en lugar deinputSchemaen camelCase.
Aunque el esquema JSON era correcto, un cliente tipado espera estrictamente inputSchema. Con input_schema, el campo se consideraba ausente: imposible generar formularios o validar argumentos.
Solución: renombrar sistemáticamente la clave a inputSchema en las definiciones de herramientas.
6. tools/call: la herramienta desconocida que no lo es
Siguiente paso: una vez estabilizado tools/list, las llamadas intentaban tools/call... y recibían sistemáticamente:
- un JSON-RPC
method not found; - un código interno
ERR_UNKNOWN_TOOL.
El servidor seguía buscando un método correspondiente a dns_lookup o dns_propagation, cuando el protocolo MCP impone un único método tools/call con un parámetro name.
Solución:
- añadir un handler dedicado
tools/call; - enrutarlo a las herramientas correctas según
params.name; - validar los parámetros con el
inputSchemacorrespondiente; - devolver un
CallToolResultestructurado.
A partir de ahí, las herramientas de CaptainDNS empezaron por fin a ejecutarse vía MCP.
7. content type "json" vs structuredContent: los detalles que rompen integraciones
En una versión intermedia, el servidor MCP devolvía resultados así:
result.content[0].type = "json";result.content[0].json = { ... }.
Es práctico del lado servidor, pero no corresponde a un tipo de bloque estándar en el protocolo (que define sobre todo text, image, resource, etc.). Algunos clientes tolerantes saben interpretarlo; otros no.
En ChatGPT esto podía traducirse en errores internos encapsulados como:
http_error 424;unhandled errors in a TaskGroup (1 sub-exception).
Solución:
- mover la carga estructurada a
structuredContent; - reservar
contentpara un resumen de texto opcional (fácil de mostrar al usuario); - usar
isErrorpara señalar claramente si el resultado de negocio es éxito o fallo.
Con ese esquema, desaparecieron los 424 provocados por el mapeo de contenido.
FAQ: timeouts, 424 y buenas prácticas MCP
Preguntas frecuentes sobre el MCP de CaptainDNS
¿Qué requisitos previos revisar si falla al añadir el servidor MCP?
Empieza por lo básico:
- La URL apunta a un endpoint HTTPS público, no a
localhostni a una IP privada. POST /streamresponde (sin405) y ves un loginitializeen el servidor.- El servidor escucha en
0.0.0.0detrás del reverse proxy, no solo en127.0.0.1. - SSE (
GET /stream) es opcional: no te bloquees si JSON-RPC funciona.
Si initialize nunca aparece en los logs, es un problema de red (DNS, TLS, firewall), no de protocolo.
¿Cómo tratar un `http_error 424` o un `unhandled errors in a TaskGroup`?
Suele indicar una respuesta fuera del contrato MCP:
- coloca la payload estructurada en
structuredContenty dejacontentpara un resumen de texto; - no respondas a las notificaciones (
notifications/initializedno tieneid); - devuelve un único
CallToolResultconisErrorexplícito en lugar de bloques personalizadostype: \"json\".
Si los logs muestran un tools/call exitoso pero el cliente falla, revisa primero estos puntos.
¿Por qué enrutar todas las herramientas por `tools/call`?
El MCP exige un único método de negocio (tools/call) con params.name:
- en el servidor, implementa
tools/cally enruta adns_lookup,dns_propagation,email_auth_audit, validando con elinputSchemacorrespondiente; - en el cliente, asegúrate de que
params.namecoincide exactamente con unnameexpuesto portools/list.
Métodos por herramienta o nombres incorrectos acaban en method not found o ERR_UNKNOWN_TOOL.
¿Cómo diagnosticar un timeout o una latencia inusual?
Los timeouts suelen venir de la ruta de red más que del protocolo:
- revisa los timeouts por herramienta en el MCP (
dns_lookupmás corto quedns_propagation); - reduce un barrido de resolutores para probar y luego amplía;
- mira los logs del backend (DNS lentos, upstream mail) y la presencia del token de servicio.
Un timeout sin initialize registrado sigue siendo un problema de accesibilidad de red.
¿Basta HTTP JSON-RPC o hace falta SSE?
Por defecto, mantén un solo punto de entrada JSON-RPC (POST /stream):
- es la vía más estable para ChatGPT y clientes modernos;
- el descubrimiento y las llamadas de herramientas pasan por ahí.
Añade SSE (GET /stream) solo si necesitas introspección; asegúrate entonces de que el flujo expone el endpoint y envía pings regulares.
Glosario MCP y CaptainDNS
MCP (Model Context Protocol)
Protocolo abierto que estandariza cómo un modelo de IA habla con herramientas externas: bases de datos, APIs, servicios como CaptainDNS. Define conceptos como initialize, tools/list, tools/call, y formatos de respuesta tipados (CallToolResult, ContentBlock, structuredContent).
Host
Aplicación que integra un modelo de IA y un cliente MCP: ChatGPT, un IDE, un agente interno. Es el host quien decide llamar a una herramienta de CaptainDNS vía MCP en respuesta a las instrucciones del usuario.
Servidor MCP
Servicio que expone herramientas MCP utilizables por los hosts. En nuestro caso: captaindns-mcp-server, un servicio Go que conoce la superficie de la API de CaptainDNS y la traduce al formato MCP.
JSON-RPC 2.0
Protocolo ligero basado en JSON que usa MCP para describir peticiones (method, params, id) y respuestas (result o error). MCP lo usa de forma acotada (initialize, tools/list, tools/call, notifications).
tools/list
Método MCP que devuelve la lista de herramientas disponibles en un servidor MCP, con su inputSchema y metadatos. Es el punto de partida para que un modelo sepa qué puede hacer con CaptainDNS.
tools/call
Método MCP que un host utiliza para ejecutar una herramienta concreta. El servidor MCP lee params.name, valida params.arguments, llama a la API backend y devuelve un CallToolResult que representa el resultado estructurado (o el error de negocio).
structuredContent
Campo opcional del CallToolResult donde un servidor MCP puede colocar datos estructurados (JSON) resultantes de la ejecución de una herramienta. CaptainDNS coloca ahí, por ejemplo, las respuestas DNS, puntuaciones de correo y detalles SPF/DKIM/DMARC/BIMI.
TaskGroup
Concepto de runtimes asíncronos (Python, etc.): un grupo de tareas ejecutadas en paralelo. Un mensaje como "unhandled errors in a TaskGroup" indica que se produjo una excepción no gestionada en una o varias de esas tareas, a menudo por una incompatibilidad sutil de formato o un bug en la cadena.