Intégrer un widget ChatGPT pour l'audit d'authentification des e-mails avec CaptainDNS

Par CaptainDNS
Publié le 7 décembre 2025

  • #MCP
  • #ChatGPT
  • #Apps SDK
  • #DNS
  • #Email
  • #Architecture
Diagramme montrant ChatGPT appelant le serveur MCP CaptainDNS et rendant un widget d'audit d'authentification email
TL;DR
  • Objectif : afficher un composant visuel d'audit SPF/DKIM/DMARC/BIMI dans ChatGPT, piloté par le serveur MCP de CaptainDNS.
  • En pratique, il faut aligner trois choses : le descriptor du tool MCP, la réponse du tool (structuredContent + _meta) et une resource HTML text/html+skybridge.
  • Les difficultés principales : comprendre où placer openai/outputTemplate, corriger le format de resources/read et gérer le timing de window.openai.toolOutput dans le widget.
  • Résultat : un widget email_auth_audit qui rend la note, le résumé et les checks MX/SPF/DKIM/DMARC/BIMI directement dans la conversation ChatGPT... sans perdre la partie "raisonnement" de l'assistant.

1. Contexte : pourquoi un widget ChatGPT pour CaptainDNS ?

CaptainDNS propose déjà une API d'audit d'authentification email : on calcule un score de préparation à l'envoi à partir de MX, SPF, DKIM, DMARC et BIMI pour un domaine donné.

Avec l'arrivée du serveur MCP CaptainDNS, nous pouvions déjà appeler cet audit depuis MCP Inspector, puis depuis une App ChatGPT. Côté UX, il manquait cependant quelque chose :

  • l'utilisateur voyait un bloc de texte,
  • mais pas de vue synthétique : score, statut par mécanisme, résumé.

Le nouveau Apps SDK UI d'OpenAI permet justement de brancher un template HTML sur un tool pour rendre un composant dans la conversation. L'idée était donc simple :

Quand ChatGPT appelle email_auth_audit, afficher un widget CaptainDNS qui résume l'audit, puis laisser l'assistant détailler les corrections DNS à appliquer.

La réalité a été un peu plus subtile.

Ce billet raconte ce que ChatGPT attend exactement, les pièges rencontrés, et comment nous avons fini par obtenir le rendu suivant :

(voir section "Résultat : le widget en action" pour la capture d'écran, à intégrer sous forme d'image).

2. Architecture globale : backend, MCP, Apps SDK

Côté CaptainDNS, l'architecture de base est la suivante :

  • Frontend : Next.js sur Vercel (captaindns.com).
  • Backend API : Go sur Fly.io, qui fait tout le métier (audit DNS, scoring, etc.).
  • Serveur MCP : Go sur Fly.io également, qui parle JSON-RPC avec MCP Inspector / ChatGPT et délègue au backend.

Pour le widget, on ajoute un nouvel acteur : l'App ChatGPT qui consomme le MCP CaptainDNS et sait rendre une resource HTML text/html+skybridge.

Le flux simplifié pour email_auth_audit :

  1. L'utilisateur demande à ChatGPT : "Peux-tu auditer la messagerie de captaindns.com ?"
  2. Le modèle décide d'appeler le tool MCP email_auth_audit.
  3. Le serveur MCP appelle l'API backend, reçoit le JSON d'audit et le "re-map" en structuredContent + _meta.
  4. ChatGPT rend :
    • un bloc texte basé sur content + structuredContent,
    • un widget en chargeant ui://widget/email-auth-widget.html référencé par openai/outputTemplate.

Vous pouvez visualiser ce flux avec un diagramme du type :

Flux MCP + Apps SDK pour le widget email_auth_audit

3. Ce que ChatGPT attend vraiment d'un tool MCP "widget"

La première difficulté a été de comprendre déclarer quoi. Côté Apps SDK, il y a trois couches bien distinctes :

  1. Descriptor du tool (tools/list)
  • C'est là qu'on déclare les métadonnées OpenAI :
    • _meta["openai/outputTemplate"] = "ui://widget/email-auth-widget.html",
    • _meta["openai/toolInvocation/invoking"] / invoked (les petits messages au-dessus du widget).
  • On peut garder en parallèle des annotations pour nos propres besoins (tags, scopes OAuth2, etc.).
  1. Resource HTML (resources/list + resources/read)
  • URI : ui://widget/email-auth-widget.html.
  • mimeType : impérativement text/html+skybridge.
  • Le contenu est un fichier HTML classique, avec un <script type="module"> qui lit window.openai.
  1. Réponse de tool (tools/call)
  • structuredContent : les données brutes que le modèle va utiliser pour raisonner.
  • _meta : les métadonnées spécifiques au résultat, visibles depuis le widget.
  • content : une version texte "fallback" pour la conversation.

Pour le widget email_auth_audit, nous avons adopté la convention suivante :

  • structuredContent = JSON complet de l'audit (domaine, score, checks, breakdown, notes...).
  • _meta.emailAuthUi = version "UI-friendly" : score, résumé, rating (good/ok/bad), checks normalisés.
  • _meta["openai/outputTemplate"] + openai/toolInvocation/* = présents à la fois dans :
    • le descriptor du tool,
    • et la réponse du tool (par sécurité, certains guides Apps SDK recommandent les deux).

Cette séparation permet :

  • au modèle de lire les données sans se soucier de l'UI,
  • au widget de récupérer un payload déjà "prêt à afficher".

4. Câbler le tool email_auth_audit côté MCP

Côté MCP, le handler email_auth_audit fait trois choses :

  1. Appeler le backend avec le domaine et récupérer un JSON d'audit du type :
{
  "domain": "captaindns.com",
  "score": 57,
  "max_score": 100,
  "percentage": 57,
  "authentication_summary": "Authentification partielle : renforcez SPF/DKIM/DMARC.",
  "checks": [ ... ],
  "score_breakdown": { ... },
  "findings": { ... },
  "notes": [ ... ],
  "ui_meta": { ... }
}
  1. Remapper vers structuredContent

Ici, nous reprenons quasiment le JSON tel quel :

"structuredContent": {
  "domain": "captaindns.com",
  "ready_to_send": true,
  "degraded": true,
  "grade": "moyen",
  "score": 57,
  "max_score": 100,
  "percentage": 57,
  "authentication_score": 57,
  "authentication_summary": "Authentification partielle : renforcez SPF/DKIM/DMARC.",
  "checks": [ ... ],
  "score_breakdown": { ... },
  "findings": { ... },
  "notes": [ ... ]
}
  1. Construire _meta + content
"_meta": {
  "emailAuthUi": {
    "score": 57,
    "summary": "Authentification partielle : renforcez SPF/DKIM/DMARC.",
    "rating": "bad",
    "checks": [ ... ]
  },
  "openai/outputTemplate": "ui://widget/email-auth-widget.html",
  "openai/toolInvocation/invoking": "Analyse de l'authentification email en cours...",
  "openai/toolInvocation/invoked": "Analyse d'authentification email terminée."
},
"content": [
  {
    "type": "text",
    "text": "Score d'authentification: 57/100 — Authentification partielle : renforcez SPF/DKIM/DMARC."
  }
]

La partie "raisonnement" de ChatGPT repose sur structuredContent et content. Le widget, lui, exploitera structuredContent et _meta.emailAuthUi.

5. Exposer le template HTML comme resource MCP

Ensuite, nous avons dû déclarer le template côté MCP.

5.1. resources/list : déclarer les resources

Le MCP expose trois resources pour ce widget :

  • ui://widget/email-auth-widget.html (HTML + CSS + JS),
  • ui://widget/apps-sdk-bridge.js (bridge Apps SDK optionnel),
  • éventuellement d'autres assets.

resources/list renvoie un tableau de descripteurs avec uri, name, description, mimeType.

5.2. resources/read : le piège du invalid_union

La première version de resources/read renvoyait un JSON du type :

{
  "contents": [
    {
      "uri": { "value": "ui://widget/email-auth-widget.html" }, // mauvais
      "mimeType": "text/html+skybridge",
      "text": "<!doctype html>..."
    }
  ]
}

Résultat dans MCP Inspector : une belle erreur invalid_union sur contents[0].uri. Le client MCP s'attend très exactement à :

{
  "contents": [
    {
      "uri": "ui://widget/email-auth-widget.html",
      "mimeType": "text/html+skybridge",
      "text": "<!doctype html>..."
    }
  ]
}

Une fois les types corrigés (uri et text en string), MCP Inspector a pu afficher le HTML, et ChatGPT a pu charger le template sans broncher.

6. Dans le widget : comprendre window.openai

Dernière étape – et non des moindres : l'implémentation du widget lui-même.

6.1. Ce que fournit ChatGPT au template

Dans un template Apps SDK, ChatGPT injecte un objet global window.openai dont les champs évoluent avec le temps :

  • au chargement, window.openai.toolOutput peut être null ou vide ;
  • quand le tool est exécuté, ChatGPT émet un évènement openai:set_globals et met à jour :
    • globals.toolOutput,
    • globals.toolResponseMetadata.

Au début, notre script supposait que window.openai.toolOutput contenait immédiatement un objet du type structuredContent. C'était faux, d'où un rendu avec :

  • Domaine inconnu,
  • Score indisponible,
  • Aucun check disponible.

6.2. Normaliser la forme de toolOutput

Nous avons commencé par écrire un helper qui accepte plusieurs formes possibles :

function extractStructured(toolOutputSource) {
  if (!toolOutputSource) return {};
  // Cas 1 : structuredContent direct
  if (toolOutputSource.domain || toolOutputSource.score || toolOutputSource.checks) {
    return toolOutputSource;
  }
  // Cas 2 : { structuredContent: {...} }
  if (toolOutputSource.structuredContent) {
    return toolOutputSource.structuredContent;
  }
  // Cas 3 : { result: { structuredContent: {...} } }
  if (toolOutputSource.result?.structuredContent) {
    return toolOutputSource.result.structuredContent;
  }
  return toolOutputSource;
}

Puis, dans normalize, au lieu de faire :

const structured =
  toolOutputSource?.structuredContent ?? toolOutputSource ?? {};

on fait :

const structured = extractStructured(toolOutputSource);

6.3. Gérer le timing avec openai:set_globals

Ensuite, nous avons branché le widget sur l'évènement openai:set_globals :

function renderFromContext() {
  const ctx = window.openai || {};
  const toolOutput = ctx.toolOutput || ctx.toolInvocationResult || {};
  const meta = ctx.toolResponseMetadata || {};
  const data = normalize(toolOutput, meta);
  render(data);
}

// Premier rendu optimiste
renderFromContext();

// Rendu réactif quand ChatGPT pousse les résultats du tool
window.addEventListener(
  "openai:set_globals",
  (event) => {
    const globals = event.detail?.globals || {};
    const toolOutput = globals.toolOutput ?? window.openai?.toolOutput ?? {};
    const meta = globals.toolResponseMetadata ?? window.openai?.toolResponseMetadata ?? {};
    const data = normalize(toolOutput, meta);
    render(data);
  },
  { passive: true }
);

Une fois ces deux points en place, le widget s'est mis à afficher :

  • captaindns.com en haut à gauche,
  • 57/100 (57%) en haut à droite,
  • le résumé, et les lignes MX/SPF/DKIM/DMARC/BIMI avec leurs statuts.

7. Résultat : le widget en action

La vue finale ressemble à une carte CaptainDNS rendue directement dans la conversation :

  • Titre : captaindns.com.
  • Score : 57/100 (57%), avec une couleur d'état (bad dans notre cas).
  • Résumé : "Authentification partielle : renforcez SPF/DKIM/DMARC."
  • Tableau :
    • MX : OK – 1 smtp.google.com.
    • SPF : OK – v=spf1 include:_spf.google.com -all
    • DKIM : Info – "Aucun enregistrement trouvé"
    • DMARC : OK – v=DMARC1; p=reject; rua=mailto:dmarc@captaindns.com;
    • BIMI : Avertissement – "Aucun enregistrement trouvé"

Capture d'écran du widget email_auth_audit dans ChatGPT, montrant le score et les checks MX/SPF/DKIM/DMARC/BIMI pour captaindns.com

8. Points d'attention et checklist

Pour résumer, voici la checklist que nous utilisons désormais pour tout nouveau widget Apps SDK branché sur le MCP CaptainDNS :

  1. Descriptor du tool (tools/list) :
  • _meta["openai/outputTemplate"] pointe vers une resource HTML text/html+skybridge.
  • _meta["openai/toolInvocation/invoking"] / invoked sont renseignés.
  1. Resource HTML (resources/read) :
  • contents[0].uri est une string (exactement l'URI de la resource).
  • contents[0].mimeType === "text/html+skybridge".
  • contents[0].text contient un HTML valide.
  1. Réponse de tool (tools/call) :
  • structuredContent contient les données métier.
  • _meta contient :
    • les métadonnées UI (emailAuthUi dans notre cas),
    • les clés openai/* si l'on veut doubler la déclaration.
    • content contient une phrase lisible dans la conversation.
  1. Widget HTML :
  • lit window.openai.toolOutput / toolResponseMetadata,
  • normalise la forme de toolOutput,
  • écoute openai:set_globals,
  • ne laisse aucun debug polluer le rendu final.

Avec ces garde-fous, l'ajout de nouveaux widgets (par exemple pour l'audit DNS autoritatif ou la propagation) devient beaucoup plus fluide.

9. Et après ?

Ce premier widget email_auth_audit nous sert de référence pour les prochains composants UI CaptainDNS dans ChatGPT :

  • un widget de santé DNS autoritative (domain_dns_check),
  • un widget de propagation DNS multi-résolveurs,
  • voire des dashboards plus complets, croisant les résultats de plusieurs tools.

Le point le plus important de ce REX est finalement simple :

le rendu Apps SDK n'est pas "magique" ; il repose sur un contrat très précis entre le descriptor du tool, la réponse du tool et les resources HTML.

Une fois ce contrat maîtrisé, on peut transformer des outils MCP assez bruts en expériences utilisateur beaucoup plus lisibles directement dans la conversation ChatGPT, sans dupliquer la logique métier côté frontend.

Articles similaires

CaptainDNS · 4 décembre 2025

Schéma d'architecture montrant Auth0, le serveur MCP CaptainDNS, l'API backend et les clients MCP

Auth0 + MCP CaptainDNS : notre retour d'expérience complet

Comment nous avons connecté Auth0 à notre serveur MCP CaptainDNS : audiences dédiées, PRM, Resource Parameter Compatibility Profile, validation JWT, propagation de l'identité jusqu'aux profils et api_requests, et mise en place d'une auth optionnelle mais prête pour des tools protégés.

  • #Auth0
  • #MCP
  • #OAuth2
  • #DNS
  • #Architecture

CaptainDNS · 27 novembre 2025

Schéma de l'architecture MCP CaptainDNS entre ChatGPT, le serveur MCP et l'API backend

Dans les coulisses du MCP CaptainDNS

Comment nous avons branché CaptainDNS sur des IA via MCP : architecture, transport HTTP+SSE, JSON-RPC, erreurs 424 et timeouts, et ce que nous avons appris en chemin.

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

CaptainDNS · 21 novembre 2025

Un schéma illustrant une IA qui discute avec CaptainDNS à travers un connecteur MCP standardisé

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