Dans les coulisses du MCP CaptainDNS

Par CaptainDNS
Publié le 27 novembre 2025

  • #MCP
  • #Architecture
  • #DNS
  • #E-mail
  • #Intégrations IA
TL;DR

TL;DR - 🔨 Avant de brancher CaptainDNS sur ChatGPT via MCP, il a fallu définir une architecture claire : un serveur MCP dédié, placé entre les hosts (ChatGPT, outils internes) et l'API CaptainDNS existante.

  • Le serveur MCP ne contient aucune logique DNS ou e-mail : il délègue tout au backend CaptainDNS via une API interne sécurisée.
  • Le transport MCP s'appuie sur HTTP + JSON-RPC, avec un point d'entrée unique /stream compatible avec le modèle HTTP+SSE moderne.
  • Les outils exposés (dns_lookup, dns_propagation, email_auth_audit) sont décrits de manière typée pour que l'IA puisse les découvrir et les appeler seule.
  • Les premiers essais ont révélé des timeouts, des erreurs 424 et des subtilités du protocole (notifications, tools/list, tools/call) qui ont obligé à durcir le contrat.
  • Ce retour d'expérience détaille comment nous avons structuré l'architecture, sécurisé les échanges et débogué la chaîne de bout en bout.

Pourquoi un serveur MCP dédié pour CaptainDNS ?

D'un point de vue produit, CaptainDNS reste un SaaS classique : une interface web Next.js (frontend) et une API Go (services/api) qui gère le coeur métier : DNS, e-mail, résolveurs, journalisation, scoring.

Ajouter un MCP ne signifie pas exposer directement l'API au modèle d'IA : nous avons choisi d'intercaler un serveur MCP dédié (services/mcp-server) qui joue le rôle d'adaptateur.

En pratique :

  • services/api reste la seule source de vérité métier (DNS, propagation, e-mail).
  • services/mcp-server est un client fort de cette API :
    • il se présente avec un token de service pour prouver qu'il vient bien de l'infrastructure CaptainDNS ;
    • il propage un éventuel token utilisateur Auth0 pour respecter les quotas, la journalisation et les permissions.
  • Les hosts (ChatGPT, outils internes, agents) ne voient que le serveur MCP et parlent MCP/JSON-RPC, jamais l'API brute.

Cette séparation permet :

  • de découpler l'évolution de l'API interne de l'évolution du contrat MCP ;
  • d'ajouter des garde-fous spécifiques au monde IA (rate limiting, contrôles de formats, timeouts agressifs) ; et,
  • de garder une architecture propre.

Vue d'ensemble de l'architecture

À un niveau haut, l'architecture ressemble à ceci :

  • Hosts MCP : ChatGPT (Connectors), outils internes, autres clients MCP.
  • Serveur MCP CaptainDNS :
    • expose les outils MCP (dns_lookup, dns_propagation, email_auth_audit, etc.) via JSON-RPC ;
    • valide et normalise les entrées (domaines, types d'enregistrement, sélecteurs DKIM) ;
    • applique timeouts, quotas, classification des erreurs.
  • API CaptainDNS (services/api) :
    • endpoints /resolve, /resolve/propagation, mail/domain-check ;
    • base de données et résolveurs ;
    • logs, scoring, profil utilisateur.

Le serveur MCP n'est donc qu'un pont protocolaire : il traduit des appels MCP (tools/list, tools/call) en appels HTTP internes, puis reconditionne la réponse en un format exploitable par le client MCP et le modèle.

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

Le coeur du protocole repose sur JSON-RPC 2.0 et seulement trois méthodes principales côté serveur :

  • initialize : négociation de la version du protocole et des capacités.
  • tools/list : découverte de la liste des outils disponibles.
  • tools/call : exécution d'un outil nommé avec des arguments typés.

initialize : dire qui l'on est et ce que l'on sait faire

Lorsqu'un host (ChatGPT, par exemple) ouvre une session avec le serveur MCP CaptainDNS, il commence par envoyer :

  • method: "initialize" ;
  • params.protocolVersion: une version de protocole ("2025-06-18" par exemple) ;
  • params.clientInfo: nom et version du client (openai-mcp, 1.0.0, etc.).

Le serveur MCP répond avec :

  • protocolVersion: la version qu'il accepte (souvent la même que celle du client) ;
  • capabilities: notamment tools: { listChanged: false } pour indiquer qu'il gère une liste d'outils stable ;
  • serverInfo: nom ("captaindns-mcp-server") et version ("0.1.0") du serveur.

À ce stade, aucune requête métier n'est encore partie vers l'API CaptainDNS : on se contente de se mettre d'accord sur la version du protocole et les capacités générales.

tools/list : annoncer les outils CaptainDNS

Une fois initialize validé, le client envoie un tools/list. Le MCP CaptainDNS répond avec un tableau de définitions d'outils, chacune incluant :

  • name : identifiant de l'outil, par exemple dns_lookup, dns_propagation, email_auth_audit ;
  • description : ce que fait l'outil, en langage naturel ;
  • inputSchema : un schéma JSON décrivant précisément les arguments attendus ;
  • annotations : métadonnées (tags, scopes recommandés comme captaindns:dns:read).

Côté CaptainDNS, les outils sont volontairement read-only : ils interrogent DNS ou la configuration e-mail, mais ne modifient rien.

Exemples de paramètres typés :

  • dns_lookup :
    • domain (obligatoire) ;
    • record_type (enum A, AAAA, TXT, MX, etc.) ;
    • resolver_preset (optionnel) ;
    • trace (booléen) pour demander une trace itérative.
  • dns_propagation :
    • même logique, mais appliquée à un balayage de plusieurs résolveurs.
  • email_auth_audit :
    • domain (obligatoire) ;
    • rp_domain (optionnel, pour les rapports/politiques) ;
    • dkim_selectors (liste optionnelle de sélecteurs à sonder).

tools/call : exécuter un outil métier

Lorsque le modèle décide d'utiliser un outil, il ne fait pas un dns_lookup direct : il envoie un tools/call avec :

  • params.name: le nom de l'outil ("dns_lookup", "dns_propagation", "email_auth_audit") ;
  • params.arguments: un objet JSON conforme à l'inputSchema.

Le rôle du serveur MCP :

  1. Valider et normaliser les arguments (domaines, types DNS, etc.).
  2. Appeler le bon endpoint backend (/resolve, /resolve/propagation, /mail/domain-check) via un client interne.
  3. Reconditionner la réponse dans un CallToolResult avec :
    • structuredContent: la réponse structurée CaptainDNS (par exemple la liste des réponses DNS, le score e-mail, les détails SPF/DKIM/DMARC/BIMI) ;
    • éventuellement content avec un résumé texte pour l'IA ;
    • isError: false si tout a fonctionné, true si l'outil a bien été appelé, mais que le résultat métier est une erreur (par exemple un timeout DNS).

Les erreurs "structurelles" (tool inconnu, paramètres invalides, JSON mal formé) restent des erreurs JSON-RPC classiques (-32601, -32602, etc.), ce qui permet au client MCP de distinguer clairement les problèmes de protocole des problèmes métier.

Transport MCP : HTTP + JSON-RPC et SSE

Pour cette première version, nous avons choisi de supporter le transport moderne basé sur HTTP + JSON-RPC, avec la possibilité de gérer du SSE en complément.

Schéma du transport MCP CaptainDNS

Point d'entrée : POST /stream

L'entrée principale du serveur MCP est un endpoint HTTP :

  • POST /stream avec un corps JSON-RPC 2.0.

C'est ce que les clients modernes utilisent :

  • ouverture de session ;
  • envoi de initialize ;
  • enchaînement de tools/list puis tools/call.

Le serveur MCP :

  • lit la requête JSON-RPC (jsonrpc, id, method, params) ;
  • route vers le handler approprié (initialize, tools/list, tools/call) ;
  • renvoie une réponse JSON-RPC structurée :
    • result (succès) ;
    • ou error (échec protocolaire).

SSE : compatibilité et introspection

Pour des clients ou des scénarios plus anciens, le serveur expose aussi :

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

Ce flux SSE :

  • annonce les métadonnées de base (endpoint HTTP pour les requêtes) ;
  • peut diffuser une vue simplifiée de la liste des outils ;
  • envoie des pings réguliers pour garder les connexions ouvertes sous contrôle.

En pratique, l'intégration avec ChatGPT s'appuie principalement sur le POST /stream JSON-RPC. Les pannes initiales (timeouts, 424, etc.) venaient surtout d'un handshake SSE incomplet lors des premières versions, corrigé en convergeant vers un seul flux JSON-RPC bien défini.

Interactions serveur MCP ↔ API CaptainDNS

Pour chaque tools/call, le serveur MCP joue le rôle de client Authentifié de l'API CaptainDNS.

Authentification et identité

Le MCP envoie systématiquement :

  • un token de service pour que l'API puisse reconnaître une origine de confiance interne ;
  • un token utilisateur, lorsque le host MCP en fournit un, afin que l'API puisse :
    • rattacher les requêtes à un profil (pour les journaux et l'historique) ;
    • appliquer des règles de quotas ou de permissions.

Le serveur MCP ne stocke pas de données utilisateur à long terme : il propage simplement l'identité jusqu'à l'API, qui reste l'autorité sur le profil et les journaux.

Client interne et normalisation

Toutes les requêtes sortantes passent par un client interne unique, qui :

  • normalise les domaines (example.com, sans point final, en minuscules) ;
  • valide les types d'enregistrement (A, AAAA, TXT, etc.) ;
  • restreint le champ d'action des outils (sélecteurs DKIM, presets de résolveurs) ;
  • applique des timeouts adaptés à chaque outil (dns_lookup plus court que email_auth_audit ou dns_propagation).

Le serveur MCP ne fait jamais d'appel HTTP brut en dehors de ce client : cela facilite la maintenance et la sécurité.

Classification des erreurs

Les erreurs sont classées en trois familles :

  • input : paramètres invalides ou manquants (domaine mal formé, type d'enregistrement non supporté, manque de token, etc.) ;
  • business : problème métier (timeout DNS, résolveur injoignable, upstream d'e-mail lent, etc.) ;
  • internal : dysfonctionnement interne (bug, configuration manquante, API en erreur 5xx).

Côté /stream (JSON-RPC), les erreurs sont reflétées sous forme de codes standard (-32602, -32001, -32603) avec une enveloppe détaillée dans error.data. Côté résultat d'outil, un échec métier peut se matérialiser par isError: true et une structuredContent dédiée.

Retour d'expérience : timeouts, 424 et autres surprises

La théorie MCP est élégante, mais la mise en pratique a été ponctuée de bugs et de surprises. Voici un condensé des problèmes rencontrés et de la manière dont ils ont été résolus.

1. Le timeout silencieux lors de l'ajout du serveur

Première étape : déclarer le serveur MCP dans ChatGPT. Au début, l'URL configurée pointait vers :

  • un serveur écoutant sur 127.0.0.1, ou
  • un endpoint HTTP qui n'implémentait pas correctement la partie MCP.

Résultat :

  • aucun log initialize n'apparaissait côté MCP ;
  • l'interface ChatGPT finissait par afficher un simple timeout "impossible de se connecter au serveur MCP".

Les causes principales :

  • serveurs écoutant seulement sur 127.0.0.1 au lieu de 0.0.0.0 derrière un reverse proxy ;
  • utilisation de chemins localhost/IP privées côté configuration (inaccessibles depuis le cloud ChatGPT).

Leçon : avant même de parler protocole, il faut vérifier les basiques : DNS public, HTTPS, écoute sur une adresse accessible, et traces visibles côté MCP dès la tentative de connexion.

2. SSE sans handshake complet : la connexion qui reste ouverte... puis meurt

Une fois l'URL MCP accessible, les premiers logs montraient :

  • un POST /stream renvoyé en 405 (méthode non gérée) ;
  • un fallback en GET /stream avec Accept: text/event-stream ;
  • une connexion SSE acceptée, puis fermée au bout de ~2 minutes.

Sur le papier, cela semblait "fonctionner" (la connexion SSE existait), mais ChatGPT n'arrivait jamais à initialiser la session MCP. La raison :

  • le flux SSE envoyait des pings, mais pas le handshake attendu (pas d'event endpoint, pas d'information sur l'URL d'invocation JSON-RPC).

Résolution : simplifier le transport en adoptant une entrée principale claire :

  • POST /stream pour tout ce qui est JSON-RPC (initialize, tools/list, tools/call) ;
  • GET /stream SSE comme canal d'introspection secondaire, mais non indispensable à ChatGPT.

En pratique, le fait de converger vers un seul point d'entrée JSON-RPC robuste a supprimé la plupart des timeouts mystérieux.

3. initialize incomplet : quand le client s'attend à plus de métadonnées

Version suivante : la connexion s'établit, mais le client MCP plante dès initialize. Les logs montraient :

  • un initialize entrant correct (avec protocolVersion et clientInfo) ;
  • une réponse qui contenait déjà la liste des outils et parfois un champ capabilities.tools mal typé.

Problèmes identifiés :

  • absence de protocolVersion dans le result ;
  • absence de serverInfo ;
  • capabilities.tools renvoyé comme booléen (true) au lieu d'un objet ({listChanged:false}) ;
  • inclusion de champs non standards (liste des tools, endpoints custom) dans la réponse initialize.

Résolution :

  • normaliser la réponse initialize :
    • protocolVersion écho ;
    • capabilities.tools = { listChanged: false } ;
    • serverInfo minimal (name, version) ;
  • déplacer la liste des outils dans tools/list.

Une fois ce contrat respecté, le client pouvait enchaîner notifications/initialized puis tools/list sans erreur.

4. Répondre à une notification : une bonne façon de faire planter le client

Autre surprise : notifications/initialized est une notification JSON-RPC :

  • pas d'id ;
  • le client n'attend aucune réponse.

Une première version du serveur répondait malgré tout avec :

  • une pseudo-réponse JSON-RPC contenant id: null.

Pour un client strict, cela ressemble à une réponse à une requête qui n'existe pas, d'où des erreurs du type "unhandled errors in a TaskGroup".

Résolution : appliquer la règle simple :

  • si la requête n'a pas d'id (notification) :
    • on la logue ;
    • on met éventuellement à jour un état interne ;
    • mais on ne renvoie rien sur le flux.

Ce petit détail a suffi à faire disparaître toute une classe d'erreurs côté client.

5. tools/list et inputSchema vs input_schema

Lors des premiers tests, ChatGPT arrivait jusqu'à tools/list, mais échouait ensuite au moment de construire les outils internes. La cause :

  • la réponse tools/list utilisait une propriété input_schema en snake_case, au lieu de inputSchema en camelCase.

Même si le contenu du schéma JSON était correct, un client typed s'attend strictement à inputSchema. Avec input_schema, le champ était considéré comme absent : impossible de générer des formulaires d'entrée ou de valider les arguments.

Résolution : renommer systématiquement la clef en inputSchema dans les définitions d'outils.

6. tools/call : l'outil inconnu qui n'en est pas un

Autre étape : une fois tools/list stabilisé, les appels d'outils tentaient un tools/call... et se faisaient systématiquement répondre par :

  • un JSON-RPC method not found ;
  • un code interne ERR_UNKNOWN_TOOL.

Le serveur cherchait encore une méthode correspondant directement à dns_lookup ou dns_propagation, alors que le protocole MCP impose une méthode unique tools/call avec un paramètre name.

Résolution :

  • ajouter un handler dédié tools/call ;
  • router ce handler vers les bons outils en fonction de params.name ;
  • valider les paramètres avec l'inputSchema correspondant ;
  • renvoyer un CallToolResult structuré.

À partir de là, les outils CaptainDNS ont enfin commencé à être réellement exécutés via MCP.

7. content type "json" vs structuredContent : les détails qui font planter les intégrations

Dans une version intermédiaire, le serveur MCP renvoyait les résultats sous la forme :

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

C'est une convention pratique côté serveur, mais elle ne correspond à aucun type de bloc standard dans le protocole (qui définit surtout text, image, resource, etc.). Certains clients tolérants savent l'interpréter, d'autres non.

Côté ChatGPT, cela a pu se traduire par des erreurs internes encapsulées en :

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

Résolution :

  • déplacer la payload structurée dans structuredContent ;
  • garder content pour un résumé texte optionnel (facile à afficher à l'utilisateur) ;
  • utiliser isError pour signaler clairement si le résultat métier est un succès ou un échec.

Une fois ce schéma adopté, les erreurs 424 liées à des problèmes de mapping de contenu ont disparu.

FAQ : timeouts, 424 et bonnes pratiques MCP

Questions fréquentes sur le MCP CaptainDNS

Quels prérequis vérifier si l'ajout du serveur MCP échoue ?

Commence par les basiques :

  • L'URL configurée pointe vers un endpoint HTTPS public, pas vers localhost ou une IP privée.
  • POST /stream doit répondre (pas de 405), et tu dois voir un log initialize côté MCP.
  • Le serveur écoute sur 0.0.0.0 derrière le reverse proxy, pas seulement 127.0.0.1.
  • SSE (GET /stream) est optionnel : ne bloque pas dessus si le JSON-RPC fonctionne.

Si initialize n'apparaît jamais dans les logs, le souci est réseau (DNS, TLS, firewall), pas protocolaire.

Comment traiter un `http_error 424` ou un `unhandled errors in a TaskGroup` ?

Ces messages signalent souvent une réponse hors contrat MCP :

  • place la payload structurée dans structuredContent et garde content pour un résumé texte ;
  • ne réponds jamais à une notification (notifications/initialized n'a pas d'id) ;
  • renvoie un seul CallToolResult avec isError explicite plutôt que des blocs type: \"json\".

Si les logs montrent un tools/call réussi mais que le client échoue, vérifie ces points en premier.

Pourquoi router tous les outils via `tools/call` ?

Le protocole MCP impose une seule méthode métier (tools/call) avec params.name :

  • côté serveur, implémente tools/call et route vers dns_lookup, dns_propagation, email_auth_audit en validant avec l'inputSchema correspondant ;
  • côté client, assure-toi que params.name correspond exactement à un name exposé par tools/list.

Une méthode dédiée par outil ou un mauvais nom de tool aboutissent à method not found ou ERR_UNKNOWN_TOOL.

Comment diagnostiquer un timeout ou une latence inhabituelle ?

Les timeouts viennent souvent de la chaîne réseau plutôt que du protocole :

  • vérifie les timeouts par outil côté MCP (dns_lookup plus court que dns_propagation) ;
  • réduis un balayage de résolveurs trop large pour tester, puis élargis ;
  • contrôle les logs backend (DNS lents, upstream mail) et la présence du service token.

Un timeout sans initialize logué reste un problème d'accessibilité réseau.

HTTP JSON-RPC suffit-il ou faut-il du SSE ?

Par défaut, reste sur un seul point d'entrée JSON-RPC (POST /stream) :

  • c'est le chemin le plus stable pour ChatGPT et les clients modernes ;
  • la liste des outils et les appels passent tous par là.

Ajoute SSE (GET /stream) uniquement si tu as besoin d'introspection ; assure alors que le flux fournit l'endpoint et des pings réguliers.

Glossaire MCP et CaptainDNS

MCP (Model Context Protocol)

Protocole ouvert qui standardise la façon dont un modèle d'IA discute avec des outils externes : bases de données, APIs, services comme CaptainDNS. Il définit des concepts comme initialize, tools/list, tools/call, et des formats de réponses typés (CallToolResult, ContentBlock, structuredContent).

Host (hôte)

Application qui embarque un modèle d'IA et un client MCP : ChatGPT, un IDE, un agent interne. C'est l'hôte qui décide d'appeler un outil CaptainDNS via MCP en réponse aux instructions de l'utilisateur.

Serveur MCP

Service qui expose des outils MCP utilisables par les hosts. Dans notre cas : captaindns-mcp-server, un service Go qui connaît la surface d'API CaptainDNS et la traduit au format MCP.

JSON-RPC 2.0

Protocole léger basé sur JSON, utilisé par MCP pour décrire les requêtes (method, params, id) et les réponses (result ou error). MCP en fait un usage cadré (initialize, tools/list, tools/call, notifications).

tools/list

Méthode MCP qui renvoie la liste des outils disponibles sur un serveur MCP, avec leurs schémas d'entrée (inputSchema) et leurs métadonnées. C'est le point de départ pour qu'un modèle sache ce qu'il peut faire avec CaptainDNS.

tools/call

Méthode MCP qu'un host utilise pour exécuter un outil précis. Le serveur MCP lit params.name, valide params.arguments, appelle l'API backend et renvoie un CallToolResult représentant le résultat structuré (ou l'erreur métier).

structuredContent

Champ optionnel du CallToolResult dans lequel un serveur MCP peut placer des données structurées (JSON) résultant de l'exécution d'un outil. CaptainDNS y place, par exemple, les réponses DNS, les scores e-mail, les détails SPF/DKIM/DMARC/BIMI.

TaskGroup

Concept issu des runtimes asynchrones (Python, etc.) : un groupe de tâches exécutées en parallèle. Un message comme "unhandled errors in a TaskGroup" indique qu'une exception non gérée s'est produite dans une ou plusieurs de ces tâches, souvent à cause d'une incompatibilité subtile de format ou d'un bug dans la chaîne de traitement.

Articles similaires

CaptainDNS · 21 novembre 2025

Un MCP pour CaptainDNS ?

Avant de brancher CaptainDNS sur des IA, il faut comprendre ce qu'est le Model Context Protocol (MCP) et ce qu'il permet réellement. Petit ABC du MCP, puis premières pistes pour CaptainDNS.

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