Entre bastidores del MCP de CaptainDNS

Por CaptainDNS
Publicado el 27 de noviembre de 2025

  • #MCP
  • #Arquitectura
  • #DNS
  • #Correo
  • #Integraciones IA
TL;DR

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 /stream alineado 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/api sigue siendo la única fuente de verdad (DNS, propagación, correo).
  • services/mcp-server es 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.
  • API de CaptainDNS (services/api):
    • endpoints /resolve, /resolve/propagation, mail/domain-check;
    • base de datos y resolutores;
    • logs, scoring, perfil usuario.

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 particular tools: { 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 ejemplo dns_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 como captaindns: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 (enum A, 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 al inputSchema.

El servidor MCP:

  1. Valida y normaliza los argumentos (dominios, tipos DNS, etc.).
  2. Llama al endpoint backend adecuado (/resolve, /resolve/propagation, /mail/domain-check) mediante un cliente interno.
  3. Reempaqueta la respuesta en un CallToolResult con:
    • structuredContent: la respuesta estructurada de CaptainDNS (respuestas DNS, puntuación de correo, detalles SPF/DKIM/DMARC/BIMI, etc.);
    • opcionalmente content con un resumen de texto para la IA;
    • isError: false si todo funcionó, true si 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.

Diagrama del transporte MCP de CaptainDNS

Punto de entrada: POST /stream

La entrada principal del servidor MCP es un endpoint HTTP:

  • POST /stream con un cuerpo JSON-RPC 2.0.

Los clientes modernos lo usan para:

  • abrir la sesión;
  • enviar initialize;
  • encadenar tools/list y tools/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 /stream con Accept: 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_lookup más corto que email_auth_audit o dns_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 initialize del lado MCP;
  • ChatGPT terminaba mostrando un simple timeout: "imposible conectar con el servidor MCP".

Causas:

  • servidores escuchando solo en 127.0.0.1 en lugar de 0.0.0.0 detrá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 /stream respondido en 405 (método no permitido);
  • un fallback a GET /stream con Accept: 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 /stream para todo lo JSON-RPC (initialize, tools/list, tools/call);
  • GET /stream SSE 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 initialize entrante correcto (con protocolVersion y clientInfo);
  • una respuesta que ya contenía la lista de herramientas y a veces un capabilities.tools mal tipado.

Problemas detectados:

  • falta protocolVersion en result;
  • falta serverInfo;
  • capabilities.tools devuelto 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 };
    • serverInfo mínimo (name, version);
  • 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/list usaba la propiedad input_schema en snake_case en lugar de inputSchema en 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 inputSchema correspondiente;
  • devolver un CallToolResult estructurado.

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 content para un resumen de texto opcional (fácil de mostrar al usuario);
  • usar isError para 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 localhost ni a una IP privada.
  • POST /stream responde (sin 405) y ves un log initialize en el servidor.
  • El servidor escucha en 0.0.0.0 detrás del reverse proxy, no solo en 127.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 structuredContent y deja content para un resumen de texto;
  • no respondas a las notificaciones (notifications/initialized no tiene id);
  • devuelve un único CallToolResult con isError explícito en lugar de bloques personalizados type: \"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/call y enruta a dns_lookup, dns_propagation, email_auth_audit, validando con el inputSchema correspondiente;
  • en el cliente, asegúrate de que params.name coincide exactamente con un name expuesto por tools/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_lookup más corto que dns_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.

Artículos relacionados

CaptainDNS · 21 de noviembre de 2025

¿Un MCP para CaptainDNS?

Antes de conectar CaptainDNS a las IAs, hay que entender qué es el Model Context Protocol (MCP) y lo que permite de verdad. Un ABC del MCP y las primeras pistas para CaptainDNS.

  • #MCP
  • #IA
  • #DNS
  • #Correo
  • #Arquitectura