Ein ChatGPT-Widget für die E-Mail-Authentifizierungsprüfung mit CaptainDNS integrieren

Von CaptainDNS
Veröffentlicht am 7. Dezember 2025

  • #MCP
  • #ChatGPT
  • #Apps SDK
  • #DNS
  • #E-Mail
  • #Architektur
Diagramm, das zeigt, wie ChatGPT den CaptainDNS MCP-Server aufruft und ein Widget für die E-Mail-Authentifizierungsprüfung rendert
TL;DR
  • Ziel: eine visuelle SPF/DKIM/DMARC/BIMI-Prüfung in ChatGPT anzeigen, betrieben vom CaptainDNS-MCP-Server.
  • Praktisch müssen drei Dinge zusammenpassen: MCP-Tool-Descriptor, Tool-Response (structuredContent + _meta) und eine HTML-Resource text/html+skybridge.
  • Haupt-Hürden: wo openai/outputTemplate hingehört, die Form von resources/read korrigieren und das Timing von window.openai.toolOutput im Widget abfangen.
  • Ergebnis: ein email_auth_audit-Widget, das Score, Zusammenfassung und MX/SPF/DKIM/DMARC/BIMI-Checks direkt im ChatGPT-Dialog rendert, ohne das Reasoning zu verlieren.

1. Kontext: warum ein ChatGPT-Widget für CaptainDNS?

CaptainDNS bietet bereits eine API zur Prüfung der E-Mail-Authentifizierung: Wir berechnen einen "ready to send"-Score auf Basis von MX, SPF, DKIM, DMARC und BIMI für eine Domain.

Mit dem CaptainDNS-MCP-Server konnten wir dieses Audit schon über MCP Inspector und dann über eine ChatGPT-App auslösen. UX-seitig fehlte jedoch etwas:

  • der Nutzer sah einen Textblock,
  • aber keine synthetische Ansicht: Score, Status pro Mechanismus, Zusammenfassung.

Das neue Apps SDK UI von OpenAI erlaubt es, einem Tool ein HTML-Template zuzuordnen, um eine Komponente im Dialog zu rendern. Die Idee war simpel:

Wenn ChatGPT email_auth_audit aufruft, soll ein CaptainDNS-Widget die Prüfung zusammenfassen und der Assistent die DNS-Korrekturen ausformulieren.

Die Realität war etwas subtiler.

Dieser Beitrag beschreibt, was ChatGPT genau erwartet, welche Fallstricke wir hatten und wie wir schließlich das folgende Rendering erhielten:

(siehe den Abschnitt "Ergebnis: das Widget in Aktion" für den Screenshot, als Bild einzubinden).

2. Gesamtarchitektur: Backend, MCP, Apps SDK

Auf CaptainDNS-Seite ist die Basisarchitektur:

  • Frontend: Next.js auf Vercel (captaindns.com).
  • Backend-API: Go auf Fly.io, die die Fachlogik abbildet (DNS-Audit, Scoring usw.).
  • MCP-Server: ebenfalls Go auf Fly.io, spricht JSON-RPC mit MCP Inspector / ChatGPT und delegiert ans Backend.

Für das Widget kommt ein neuer Akteur hinzu: die ChatGPT-App, die den CaptainDNS-MCP konsumiert und ein HTML-Resource text/html+skybridge rendern kann.

Vereinfachter Ablauf für email_auth_audit:

  1. Der Nutzer fragt ChatGPT: "Kannst du das Mail-Setup von captaindns.com prüfen?"
  2. Das Modell entscheidet, das MCP-Tool email_auth_audit aufzurufen.
  3. Der MCP-Server ruft die Backend-API, erhält das Audit-JSON und mappt es auf structuredContent + _meta.
  4. ChatGPT rendert:
    • einen Textblock basierend auf content + structuredContent,
    • ein Widget, indem ui://widget/email-auth-widget.html geladen wird, referenziert über openai/outputTemplate.

Diesen Ablauf kann man sich mit einem Diagramm so vorstellen:

MCP + Apps SDK Flow für das Widget email_auth_audit

3. Was ChatGPT wirklich von einem MCP-Tool „Widget“ erwartet

Die erste Herausforderung war zu verstehen, wo man was deklariert. Auf Apps-SDK-Seite gibt es drei klar getrennte Ebenen:

  1. Tool-Descriptor (tools/list)
  • Hier stehen die OpenAI-Metadaten:
    • _meta["openai/outputTemplate"] = "ui://widget/email-auth-widget.html",
    • _meta["openai/toolInvocation/invoking"] / invoked (die kleinen Meldungen über dem Widget).
  • Parallel kann man annotations für eigene Zwecke behalten (Tags, OAuth2-Scopes usw.).
  1. HTML-Resource (resources/list + resources/read)
  • URI: ui://widget/email-auth-widget.html.
  • mimeType: strikt text/html+skybridge.
  • Der Inhalt ist ein klassisches HTML mit einem <script type="module">, das window.openai liest.
  1. Tool-Response (tools/call)
  • structuredContent: die Rohdaten, mit denen das Modell argumentiert.
  • _meta: die ergebnisspezifischen Metadaten, im Widget sichtbar.
  • content: ein Text-Fallback für den Dialog.

Für das email_auth_audit-Widget haben wir folgende Konvention gewählt:

  • structuredContent = komplettes Audit-JSON (Domain, Score, Checks, Breakdown, Notes...).
  • _meta.emailAuthUi = die "UI-taugliche" Version: Score, Zusammenfassung, Rating (good/ok/bad), normalisierte Checks.
  • _meta["openai/outputTemplate"] + openai/toolInvocation/* = sowohl in:
    • dem Tool-Descriptor,
    • als auch in der Tool-Response (zur Sicherheit – einige Apps-SDK-Guides empfehlen beides).

Diese Trennung erlaubt es:

  • dem Modell, die Daten ohne UI-Sicht zu lesen,
  • dem Widget, ein bereits "renderfertiges" Payload abzuholen.

4. Das Tool email_auth_audit auf MCP-Seite verdrahten

Auf MCP-Seite tut der Handler email_auth_audit drei Dinge:

  1. Backend aufrufen mit der Domain und ein Audit-JSON holen, z. B.:
{
  "domain": "captaindns.com",
  "score": 57,
  "max_score": 100,
  "percentage": 57,
  "authentication_summary": "Teilweise authentifiziert: SPF/DKIM/DMARC verstärken.",
  "checks": [ ... ],
  "score_breakdown": { ... },
  "findings": { ... },
  "notes": [ ... ],
  "ui_meta": { ... }
}
  1. Auf structuredContent remappen

Hier verwenden wir das JSON fast unverändert weiter:

"structuredContent": {
  "domain": "captaindns.com",
  "ready_to_send": true,
  "degraded": true,
  "grade": "mittel",
  "score": 57,
  "max_score": 100,
  "percentage": 57,
  "authentication_score": 57,
  "authentication_summary": "Teilweise authentifiziert: SPF/DKIM/DMARC verstärken.",
  "checks": [ ... ],
  "score_breakdown": { ... },
  "findings": { ... },
  "notes": [ ... ]
}
  1. _meta + content bauen
"_meta": {
  "emailAuthUi": {
    "score": 57,
    "summary": "Teilweise authentifiziert: SPF/DKIM/DMARC verstärken.",
    "rating": "bad",
    "checks": [ ... ]
  },
  "openai/outputTemplate": "ui://widget/email-auth-widget.html",
  "openai/toolInvocation/invoking": "E-Mail-Authentifizierungs-Audit läuft...",
  "openai/toolInvocation/invoked": "E-Mail-Authentifizierungs-Audit abgeschlossen."
},
"content": [
  {
    "type": "text",
    "text": "Authentifizierungs-Score: 57/100 — Teilweise authentifiziert: SPF/DKIM/DMARC verstärken."
  }
]

Der "Reasoning"-Teil von ChatGPT stützt sich auf structuredContent und content. Das Widget nutzt structuredContent und _meta.emailAuthUi.

5. Das HTML-Template als MCP-Resource exponieren

Als Nächstes musste das Template auf MCP-Seite deklariert werden.

5.1. resources/list: die Resources deklarieren

Der MCP stellt drei Resources für dieses Widget bereit:

  • ui://widget/email-auth-widget.html (HTML + CSS + JS),
  • ui://widget/apps-sdk-bridge.js (optional, Apps-SDK-Bridge),
  • ggf. weitere Assets.

resources/list liefert ein Array von Deskriptoren mit uri, name, description, mimeType.

5.2. resources/read: die invalid_union-Falle

Die erste Version von resources/read gab ein JSON in dieser Form zurück:

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

Ergebnis im MCP Inspector: ein hübscher invalid_union-Fehler auf contents[0].uri. Der MCP-Client erwartet genau:

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

Nachdem die Typen korrigiert waren (uri und text als Strings), konnte MCP Inspector das HTML anzeigen und ChatGPT das Template ohne Murren laden.

6. Im Widget: window.openai verstehen

Letzter Schritt – und nicht der kleinste: die Widget-Implementierung.

6.1. Was ChatGPT dem Template liefert

In einem Apps-SDK-Template injiziert ChatGPT ein globales Objekt window.openai, dessen Felder sich im Laufe der Zeit ändern:

  • beim Laden kann window.openai.toolOutput null oder leer sein;
  • wenn das Tool ausgeführt wird, sendet ChatGPT ein openai:set_globals-Event und aktualisiert:
    • globals.toolOutput,
    • globals.toolResponseMetadata.

Zu Beginn nahm unser Script an, dass window.openai.toolOutput sofort ein Objekt vom Typ structuredContent enthält. Das war falsch, daher ein Render mit:

  • Unbekannte Domain,
  • Score nicht verfügbar,
  • Keine Checks verfügbar.

6.2. Die Form von toolOutput normalisieren

Wir haben zuerst einen Helper geschrieben, der mehrere mögliche Formen akzeptiert:

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

Dann tun wir in normalize statt:

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

folgendes:

const structured = extractStructured(toolOutputSource);

6.3. Timing mit openai:set_globals handhaben

Anschließend haben wir das Widget an das openai:set_globals-Event angebunden:

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

// Erster, optimistischer Render
renderFromContext();

// Reaktiver Render, wenn ChatGPT Tool-Ergebnisse pusht
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 }
);

Mit diesen beiden Punkten zeigte das Widget:

  • captaindns.com oben links,
  • 57/100 (57%) oben rechts,
  • die Zusammenfassung sowie die Zeilen MX/SPF/DKIM/DMARC/BIMI mit ihren Status.

7. Ergebnis: das Widget in Aktion

Die finale Ansicht wirkt wie eine CaptainDNS-Karte direkt im Dialog:

  • Titel: captaindns.com.
  • Score: 57/100 (57%) mit Statusfarbe (bad in unserem Fall).
  • Zusammenfassung: "Teilweise authentifiziert: SPF/DKIM/DMARC verstärken."
  • Tabelle:
    • MX: OK – 1 smtp.google.com.
    • SPF: OK – v=spf1 include:_spf.google.com -all
    • DKIM: Info – "Kein Eintrag gefunden"
    • DMARC: OK – v=DMARC1; p=reject; rua=mailto:dmarc@captaindns.com;
    • BIMI: Warnung – "Kein Eintrag gefunden"

Screenshot des email_auth_audit-Widgets in ChatGPT mit Score und MX/SPF/DKIM/DMARC/BIMI-Checks für captaindns.com

8. Stolpersteine und Checklist

Zur Erinnerung die Checklist, die wir jetzt für jedes neue Apps-SDK-Widget am CaptainDNS-MCP nutzen:

  1. Tool-Descriptor (tools/list):
  • _meta["openai/outputTemplate"] zeigt auf eine HTML-Resource text/html+skybridge.
  • _meta["openai/toolInvocation/invoking"] / invoked sind gesetzt.
  1. HTML-Resource (resources/read):
  • contents[0].uri ist ein String (exakt die Resource-URI).
  • contents[0].mimeType === "text/html+skybridge".
  • contents[0].text enthält valides HTML.
  1. Tool-Response (tools/call):
  • structuredContent trägt die Fachdaten.
  • _meta enthält:
    • die UI-Metadaten (hier emailAuthUi),
    • die openai/*-Keys, falls man die Deklaration doppeln will.
    • content enthält einen lesbaren Satz für den Dialog.
  1. HTML-Widget:
  • liest window.openai.toolOutput / toolResponseMetadata,
  • normalisiert die Form von toolOutput,
  • lauscht auf openai:set_globals,
  • lässt kein Debug-Rauschen im finalen Render.

Mit diesen Sicherungen wird das Hinzufügen neuer Widgets (z. B. für autoritative DNS-Gesundheit oder Propagation) deutlich reibungsloser.

9. Wie geht es weiter?

Dieses erste email_auth_audit-Widget dient als Referenz für kommende CaptainDNS-UI-Komponenten in ChatGPT:

  • ein Widget für die autoritative DNS-Gesundheit (domain_dns_check),
  • ein Multi-Resolver-DNS-Propagation-Widget,
  • oder sogar umfangreichere Dashboards, die mehrere Tools kombinieren.

Der wichtigste Punkt aus diesem REX ist eigentlich einfach:

das Apps-SDK-Rendering ist nicht "magisch"; es beruht auf einem sehr präzisen Vertrag zwischen Tool-Descriptor, Tool-Response und den HTML-Resources.

Wenn man diesen Vertrag beherrscht, kann man ziemlich rohe MCP-Tools in wesentlich lesbarere Nutzererlebnisse direkt im ChatGPT-Dialog verwandeln, ohne die Business-Logik im Frontend zu duplizieren.

Ähnliche Artikel

CaptainDNS · 27. November 2025

Diagramm der CaptainDNS-MCP-Architektur zwischen ChatGPT, dem MCP-Server und der Backend-API

Hinter den Kulissen des CaptainDNS-MCP

Wie wir CaptainDNS über MCP an AIs angebunden haben: Architektur, HTTP+SSE-Transport, JSON-RPC, 424-Fehler, Timeouts und unsere Learnings.

  • #MCP
  • #Architektur
  • #DNS
  • #E-Mail
  • #KI-Integrationen

CaptainDNS · 21. November 2025

Diagramm: Ein KI-Host spricht über einen standardisierten MCP-Connector mit CaptainDNS

Ein MCP für CaptainDNS?

Bevor CaptainDNS an IAs angebunden wird, muss klar sein, was das Model Context Protocol (MCP) ist und was es wirklich ermöglicht. Ein MCP-ABC und erste Ansätze für CaptainDNS.

  • #MCP
  • #KI
  • #DNS
  • #E-Mail
  • #Architektur

CaptainDNS · 4. Dezember 2025

Architekturdiagramm mit Auth0, dem CaptainDNS-MCP-Server, der Backend-API und MCP-Clients

Auth0 + MCP CaptainDNS: unser vollständiger Erfahrungsbericht

Wie wir Auth0 an unseren MCP-Server von CaptainDNS angeschlossen haben: eigene Audiences, PRM, Resource Parameter Compatibility Profile, JWT-Validierung, Identitätsweitergabe bis profiles und api_requests, mit optionaler Auth heute und geschützten Tools für später.

  • #Auth0
  • #MCP
  • #OAuth2
  • #DNS
  • #Architektur