Integrare un widget ChatGPT per l'audit di autenticazione email con CaptainDNS

Di CaptainDNS
Pubblicato il 7 dicembre 2025

  • #MCP
  • #ChatGPT
  • #Apps SDK
  • #DNS
  • #Email
  • #Architettura
Diagramma che mostra ChatGPT che chiama il server MCP CaptainDNS e renderizza un widget di audit di autenticazione email
TL;DR
  • Obiettivo: mostrare in ChatGPT un componente visivo di audit SPF/DKIM/DMARC/BIMI alimentato dal server MCP di CaptainDNS.
  • In pratica bisogna allineare tre elementi: il descriptor del tool MCP, la risposta del tool (structuredContent + _meta) e una risorsa HTML text/html+skybridge.
  • Principali difficoltà: decidere dove posizionare openai/outputTemplate, correggere la forma di resources/read e gestire il timing di window.openai.toolOutput nel widget.
  • Risultato: un widget email_auth_audit che rende punteggio, riepilogo e verifiche MX/SPF/DKIM/DMARC/BIMI direttamente nella conversazione ChatGPT senza perdere il reasoning dell'assistente.

1. Contesto: perché un widget ChatGPT per CaptainDNS?

CaptainDNS offre già un'API di audit dell'autenticazione email: calcoliamo un punteggio di readiness all'invio basato su MX, SPF, DKIM, DMARC e BIMI per un dominio.

Con il server MCP CaptainDNS potevamo già lanciare questo audit da MCP Inspector e poi da un'App ChatGPT. Sul piano UX mancava qualcosa:

  • l'utente vedeva un blocco di testo,
  • ma non una vista sintetica: punteggio, stato per meccanismo, riepilogo.

Il nuovo Apps SDK UI di OpenAI permette proprio di agganciare un template HTML a un tool per renderizzare un componente nella conversazione. L'idea era semplice:

Quando ChatGPT chiama email_auth_audit, mostrare un widget CaptainDNS che riassume l'audit, e lasciare che l'assistente dettaglia le correzioni DNS.

La realtà è stata un po' più sottile.

Questo articolo racconta cosa si aspetta esattamente ChatGPT, gli scogli incontrati e come siamo arrivati al seguente rendering:

(vedi la sezione "Risultato: il widget in azione" per lo screenshot, da integrare come immagine).

2. Architettura complessiva: backend, MCP, Apps SDK

Dal lato CaptainDNS, l'architettura di base è:

  • Frontend: Next.js su Vercel (captaindns.com).
  • API backend: Go su Fly.io, che gestisce la logica (audit DNS, scoring, ecc.).
  • Server MCP: Go su Fly.io anch'esso, parla JSON-RPC con MCP Inspector / ChatGPT e delega al backend.

Per il widget aggiungiamo un nuovo attore: l'App ChatGPT che consuma l'MCP CaptainDNS e sa renderizzare una risorsa HTML text/html+skybridge.

Flusso semplificato per email_auth_audit:

  1. L'utente chiede a ChatGPT: "Puoi auditare la posta di captaindns.com?"
  2. Il modello decide di chiamare il tool MCP email_auth_audit.
  3. Il server MCP chiama l'API backend, riceve il JSON di audit e lo rimappa in structuredContent + _meta.
  4. ChatGPT renderizza:
    • un blocco di testo basato su content + structuredContent,
    • un widget caricando ui://widget/email-auth-widget.html referenziato da openai/outputTemplate.

Puoi visualizzare questo flusso con un diagramma del tipo:

Flusso MCP + Apps SDK per il widget email_auth_audit

3. Cosa si aspetta davvero ChatGPT da un tool MCP “widget”

La prima difficoltà è stata capire dove dichiarare cosa. Nel Apps SDK ci sono tre livelli distinti:

  1. Descriptor del tool (tools/list)
  • Qui si dichiarano i metadati OpenAI:
    • _meta["openai/outputTemplate"] = "ui://widget/email-auth-widget.html",
    • _meta["openai/toolInvocation/invoking"] / invoked (i piccoli messaggi sopra il widget).
  • In parallelo puoi mantenere annotations per esigenze proprie (tag, scope OAuth2, ecc.).
  1. Resource HTML (resources/list + resources/read)
  • URI: ui://widget/email-auth-widget.html.
  • mimeType: obbligatoriamente text/html+skybridge.
  • Il contenuto è un HTML classico con uno <script type="module"> che legge window.openai.
  1. Risposta del tool (tools/call)
  • structuredContent: i dati grezzi che il modello userà per ragionare.
  • _meta: i metadati specifici del risultato, visibili dal widget.
  • content: una versione testuale "fallback" per la conversazione.

Per il widget email_auth_audit abbiamo adottato la convenzione seguente:

  • structuredContent = JSON completo dell'audit (dominio, punteggio, checks, breakdown, note...).
  • _meta.emailAuthUi = la versione "pronta per la UI": punteggio, riepilogo, rating (good/ok/bad), checks normalizzati.
  • _meta["openai/outputTemplate"] + openai/toolInvocation/* = presenti sia nel:
    • descriptor del tool,
    • sia nella risposta del tool (per sicurezza: alcune guide dell'Apps SDK raccomandano entrambi).

Questa separazione permette:

  • al modello di leggere i dati senza preoccuparsi della UI,
  • al widget di recuperare un payload già "pronto da rendere".

4. Cablaggio del tool email_auth_audit lato MCP

L'handler email_auth_audit sul MCP fa tre cose:

  1. Chiamare il backend con il dominio e recuperare un JSON di audit del tipo:
{
  "domain": "captaindns.com",
  "score": 57,
  "max_score": 100,
  "percentage": 57,
  "authentication_summary": "Autenticazione parziale: rafforza SPF/DKIM/DMARC.",
  "checks": [ ... ],
  "score_breakdown": { ... },
  "findings": { ... },
  "notes": [ ... ],
  "ui_meta": { ... }
}
  1. Rimappare verso structuredContent

Qui riutilizziamo quasi il JSON così com'è:

"structuredContent": {
  "domain": "captaindns.com",
  "ready_to_send": true,
  "degraded": true,
  "grade": "medio",
  "score": 57,
  "max_score": 100,
  "percentage": 57,
  "authentication_score": 57,
  "authentication_summary": "Autenticazione parziale: rafforza SPF/DKIM/DMARC.",
  "checks": [ ... ],
  "score_breakdown": { ... },
  "findings": { ... },
  "notes": [ ... ]
}
  1. Costruire _meta + content
"_meta": {
  "emailAuthUi": {
    "score": 57,
    "summary": "Autenticazione parziale: rafforza SPF/DKIM/DMARC.",
    "rating": "bad",
    "checks": [ ... ]
  },
  "openai/outputTemplate": "ui://widget/email-auth-widget.html",
  "openai/toolInvocation/invoking": "Audit di autenticazione email in corso...",
  "openai/toolInvocation/invoked": "Audit di autenticazione email completato."
},
"content": [
  {
    "type": "text",
    "text": "Punteggio di autenticazione: 57/100 — Autenticazione parziale: rafforza SPF/DKIM/DMARC."
  }
]

La parte di "reasoning" di ChatGPT si appoggia a structuredContent e content. Il widget usa structuredContent e _meta.emailAuthUi.

5. Esporre il template HTML come resource MCP

Poi abbiamo dovuto dichiarare il template lato MCP.

5.1. resources/list: dichiarare i resources

Il MCP espone tre resources per questo widget:

  • ui://widget/email-auth-widget.html (HTML + CSS + JS),
  • ui://widget/apps-sdk-bridge.js (bridge Apps SDK opzionale),
  • eventuali altri asset.

resources/list restituisce un array di descrittori con uri, name, description, mimeType.

5.2. resources/read: la trappola invalid_union

La prima versione di resources/read restituiva un JSON di forma:

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

Risultato in MCP Inspector: un bell'errore invalid_union su contents[0].uri. Il client MCP si aspetta esattamente:

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

Una volta corretti i tipi (uri e text come stringhe), MCP Inspector ha potuto mostrare l'HTML e ChatGPT ha caricato il template senza problemi.

6. Dentro il widget: capire window.openai

Ultimo step, e non banale: l'implementazione del widget.

6.1. Cosa fornisce ChatGPT al template

In un template Apps SDK, ChatGPT inietta un oggetto globale window.openai i cui campi evolvono nel tempo:

  • al caricamento, window.openai.toolOutput può essere null o vuoto;
  • quando il tool viene eseguito, ChatGPT emette un evento openai:set_globals e aggiorna:
    • globals.toolOutput,
    • globals.toolResponseMetadata.

All'inizio il nostro script supponeva che window.openai.toolOutput contenesse subito un oggetto di tipo structuredContent. Non era così, da cui un render con:

  • Dominio sconosciuto,
  • Punteggio non disponibile,
  • Nessun check disponibile.

6.2. Normalizzare la forma di toolOutput

Abbiamo iniziato scrivendo un helper che accetta diverse forme possibili:

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

Poi, in normalize, invece di fare:

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

facciamo:

const structured = extractStructured(toolOutputSource);

6.3. Gestire il timing con openai:set_globals

Abbiamo poi collegato il widget all'evento 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);
}

// Primo render ottimistico
renderFromContext();

// Render reattivo quando ChatGPT invia i risultati del 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 }
);

Con questi due punti in ordine, il widget ha iniziato a mostrare:

  • captaindns.com in alto a sinistra,
  • 57/100 (57%) in alto a destra,
  • il riepilogo, e le righe MX/SPF/DKIM/DMARC/BIMI con i rispettivi stati.

7. Risultato: il widget in azione

La vista finale sembra una card CaptainDNS renderizzata direttamente nella conversazione:

  • Titolo: captaindns.com.
  • Punteggio: 57/100 (57%), con un colore di stato (bad nel nostro caso).
  • Riepilogo: "Autenticazione parziale: rafforza SPF/DKIM/DMARC."
  • Tabella:
    • MX: OK – 1 smtp.google.com.
    • SPF: OK – v=spf1 include:_spf.google.com -all
    • DKIM: Info – "Nessun record trovato"
    • DMARC: OK – v=DMARC1; p=reject; rua=mailto:dmarc@captaindns.com;
    • BIMI: Avviso – "Nessun record trovato"

Screenshot del widget email_auth_audit in ChatGPT che mostra il punteggio e i check MX/SPF/DKIM/DMARC/BIMI per captaindns.com

8. Punti di attenzione e checklist

In sintesi, ecco la checklist che usiamo ora per ogni nuovo widget Apps SDK collegato all'MCP di CaptainDNS:

  1. Descriptor del tool (tools/list):
  • _meta["openai/outputTemplate"] punta a una risorsa HTML text/html+skybridge.
  • _meta["openai/toolInvocation/invoking"] / invoked sono valorizzati.
  1. Resource HTML (resources/read):
  • contents[0].uri è una stringa (esattamente l'URI della risorsa).
  • contents[0].mimeType === "text/html+skybridge".
  • contents[0].text contiene HTML valido.
  1. Risposta del tool (tools/call):
  • structuredContent contiene i dati di business.
  • _meta contiene:
    • i metadati UI (emailAuthUi qui),
    • le chiavi openai/* se si vuole duplicare la dichiarazione.
    • content contiene una frase leggibile per la conversazione.
  1. Widget HTML:
  • legge window.openai.toolOutput / toolResponseMetadata,
  • normalizza la forma di toolOutput,
  • ascolta openai:set_globals,
  • non lascia debug nel render finale.

Con queste salvaguardie, aggiungere nuovi widget (ad esempio per la salute DNS autoritativa o la propagazione) diventa molto più fluido.

9. E dopo?

Questo primo widget email_auth_audit ci serve da riferimento per i prossimi componenti UI CaptainDNS in ChatGPT:

  • un widget di salute DNS autoritativa (domain_dns_check),
  • un widget di propagazione DNS multi-resolver,
  • o persino dashboard più ricchi che incrociano i risultati di più tool.

Il punto più importante di questo REX è in realtà semplice:

il rendering Apps SDK non è "magico"; si basa su un contratto molto preciso tra descriptor del tool, risposta del tool e risorse HTML.

Una volta padroneggiato quel contratto, puoi trasformare tool MCP piuttosto grezzi in esperienze utente molto più leggibili direttamente nella conversazione ChatGPT, senza duplicare la logica di business nel frontend.

Articoli simili

CaptainDNS · 27 novembre 2025

Diagramma dell'architettura MCP di CaptainDNS tra ChatGPT, il server MCP e la API backend

Dietro le quinte dell'MCP di CaptainDNS

Come abbiamo collegato CaptainDNS alle AI via MCP: architettura, trasporto HTTP+SSE, JSON-RPC, errori 424 e timeout, e cosa abbiamo imparato lungo il percorso.

  • #MCP
  • #Architettura
  • #DNS
  • #Email
  • #Integrazioni IA

CaptainDNS · 4 dicembre 2025

Diagramma di architettura con Auth0, il server MCP CaptainDNS, la API backend e i client MCP

Auth0 + MCP CaptainDNS: il nostro REX completo

Come abbiamo collegato Auth0 al server MCP di CaptainDNS: audiences dedicate, PRM, Resource Parameter Compatibility Profile, validazione JWT, propagazione dell'identità fino a profiles e api_requests, con auth opzionale oggi e strumenti protetti pronti per dopo.

  • #Auth0
  • #MCP
  • #OAuth2
  • #DNS
  • #Architettura

CaptainDNS · 21 novembre 2025

Schema: un host IA che parla con CaptainDNS tramite un connettore MCP standard

Un MCP per CaptainDNS?

Prima di collegare CaptainDNS alle IA, bisogna capire cos'è il Model Context Protocol (MCP) e cosa permette davvero. Un piccolo ABC del MCP e le prime piste per CaptainDNS.

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