For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Hauptnavigation

Deinen MCP-Server um eine UI erweitern

Optionale UI-Ressourcen aus ausgewählten MCP-Tools zurückgeben.

Übersicht

Eine eigene UI ist optional. Ergänze sie, wenn ein Anwendungsfall deines Plug-ins erfordert, dass Menschen strukturierte Informationen prüfen, vergleichen, bearbeiten, bestätigen oder darin navigieren. Gestalte die MCP-Tools so, dass sie auch ohne Komponente nützlich bleiben und ChatGPT und Codex den Ablauf ohne UI abschließen können.

Der MCP-Server gibt UI-Ressourcen für ausgewählte Tools zurück. Komponenten laufen in einem iframe in ChatGPT, kommunizieren über die MCP Apps Bridge (JSON-RPC über postMessage) mit dem Host und werden neben der Unterhaltung dargestellt. Der offene Standard MCP Apps ermöglicht es, dieselbe UI auf verschiedenen kompatiblen Hosts auszuführen.

Mit MCP Apps beginnen

ChatGPT implementiert den offenen Standard MCP Apps für UIs, die ein MCP-Server zurückgibt. MCP Apps definiert, wie dein Server Tools mit UI-Ressourcen verknüpft und wie der iframe mit seinem Host kommuniziert.

Für neue UIs:

  1. Deklariere die UI-Ressource mit _meta.ui.resourceUri.
  2. Verwende die JSON-RPC-Bridge ui/* über postMessage für die Initialisierung, Benachrichtigungen, Tool-Aufrufe, Nachrichten und für das Modell sichtbaren Kontext.
  3. Gestalte Tools so, dass sie auch ohne UI nützlich bleiben und das Modell den Ablauf in Clients abschließen kann, die keine Komponenten rendern.

Mit dieser auf Standards basierenden Grundlage lässt sich dieselbe UI in ChatGPT und anderen kompatiblen MCP Apps Hosts ausführen.

Wenn du den Standard implementieren möchtest, nutze die Spezifikation für MCP Apps.

ChatGPT-Erweiterungen ergänzen

Sobald der Ablauf mit MCP Apps funktioniert, verwende window.openai nur für Funktionen, die die gemeinsame Spezifikation nicht abdeckt. Diese optionalen Erweiterungen können die Nutzung in ChatGPT verbessern, ohne Teil der portablen UI-Grundlage zu werden.

Gemeinsame Felder und Methoden bevorzugen

Verwende das Feld oder die Methode von MCP Apps, wenn die gemeinsame Spezifikation die Funktion abdeckt:

ZielStandard MCP AppsChatGPT-Kompatibilitätsalias
Ein Tool mit einer UI-Ressource verknüpfen_meta.ui.resourceUri_meta["openai/outputTemplate"]
Tool-Eingaben empfangenui/initialize + ui/notifications/tool-inputwindow.openai.toolInput
Tool-Ergebnisse empfangenui/notifications/tool-resultwindow.openai.toolOutput
Ein Tool aus der UI aufrufentools/callwindow.openai.callTool
Eine Folgenachricht sendenui/messagewindow.openai.sendFollowUpMessage

Die Kompatibilitätsaliase bleiben für bestehende Integrationen verfügbar. Neue UIs sollten die gemeinsamen Felder und Bridge-Methoden in der mittleren Spalte verwenden.

Beispiele dafür sind:

  • Instant Checkout mit window.openai.requestCheckout.
  • Dateiverarbeitung in ChatGPT mit window.openai.uploadFile, window.openai.selectFiles und window.openai.getFileDownloadUrl.
  • Vom Host gesteuerte modale Dialoge mit window.openai.requestModal.
  • Speichern des Widget-Zustands mit window.openai.widgetState und window.openai.setWidgetState.

Prüfe bei jeder Erweiterung, ob die benötigte Funktion verfügbar ist, und biete nach Möglichkeit eine Alternative an:

const openai = typeof window !== "undefined" ? window.openai : undefined;

if (openai?.requestModal) {
  await openai.requestModal({
    /* ... */
  });
} else {
  // Fallback behavior for hosts without this extension.
}

Mache Verzweigungen im Code nicht vom Namen des Hosts oder Produkts abhängig. Prüfe, ob die Funktion verfügbar ist, die deine UI benötigt.

Signaturen und Beispiele für Erweiterungen findest du in der Referenz zur Komponenten-Bridge window.openai.

Optionale Komponentenbibliothek von OpenAI

Die Komponentenbibliothek @openai/apps-sdk-ui bietet vorgefertigte Schaltflächen, Karten, Eingabeelemente und grundlegende Layout-Elemente, die auf den Container von ChatGPT abgestimmt sind. Nutze sie, wenn du ein einheitliches Erscheinungsbild möchtest, ohne Basiskomponenten neu zu entwickeln.

Du kannst dir auch das Repository mit UI-Beispielen auf GitHub ansehen.

Eine Darstellungsform wählen

Beginne mit einer Inline-UI und fordere nur dann mehr Platz an, wenn der Ablauf ihn erfordert. Wähle die kleinste Darstellungsform, mit der sich das Ergebnis verstehen oder die Aufgabe abschließen lässt.

Inline-Karte

Verwende eine Inline-Karte für ein klar umrissenes Ergebnis, eine Bestätigung oder eine kleine Auswahl an Aktionen. Halte sie in sich geschlossen und vermeide tief verschachtelte Navigation.

Beispiele für Inline-Karten

Verwende ein Inline-Karussell, wenn Nutzende eine kleine Auswahl ähnlicher, visuell reichhaltiger Optionen überblicken und daraus wählen sollen.

Beispiel für ein Inline-Karussell

Vollbild

Verwende die Vollbildansicht für umfangreiche Aufgaben, die mehr Platz benötigen, etwa Karten, Bearbeitungsflächen oder das Durchsehen detaillierter Inhalte. Gestalte die UI so, dass sie mit dem Editor von ChatGPT zusammenarbeitet, der auch in der Vollbildansicht verfügbar bleibt.

Beispiel für eine UI in der Vollbildansicht

Bild-in-Bild

Verwende Bild-in-Bild für eine laufende Aktivität, die sichtbar bleiben soll, während die Unterhaltung weitergeht, etwa eine Live-Sitzung, ein Spiel oder ein Video.

Beispiel für eine Bild-in-Bild-UI

Ausführliche Hinweise zu Layout, Interaktion, visueller Gestaltung und Barrierefreiheit findest du in den UI-Richtlinien.

Datenverarbeitung vom UI-Rendering trennen

Entkoppeltes Entwurfsmuster

Wenn du jedem Tool-Aufruf eine Widget-Vorlage beifügst, rendert ChatGPT deinen iframe möglicherweise zu häufig neu. Besser ist es, Tools zur Datenverarbeitung von Tools zum Rendern zu trennen:

  • Daten-Tools rufen Daten ab, berechnen oder verändern sie und geben ausschließlich Tool-Ergebnisse zurück.
  • Rendering-Tools nehmen die endgültigen Daten entgegen und geben die Widget-Vorlage zurück.

So kann das Modell die abgerufenen Daten zunächst mithilfe seiner Intelligenz verarbeiten, bevor es entscheidet, eine UI anzuzeigen. Damit steigt die Wahrscheinlichkeit deutlich, dass es das konkret geäußerte Ziel der nutzenden Person erreicht.

Dieses Entwurfsmuster ist Teil der Architektur von MCP Apps.

In der Praxis verwenden viele UI-Integrationen diese Aufteilung:

  • Tools zum Suchen und Abrufen (Daten zuerst): Geben IDs und Metadaten zurück, ohne eine Widget-Vorlage beizufügen.
  • Rendering-Tools (zum Beispiel render_listings_widget): Nehmen eine vorbereitete Liste von IDs entgegen und rendern das Widget.

Nur das Rendering-Tool sollte _meta.ui.resourceUri enthalten.

Entkoppelter Aufrufablauf

Empfohlener Aufrufablauf:

  1. Das Modell ruft das Daten-Tool auf (zum Beispiel roll_dice).
  2. Das Modell erhält structuredContent vom Daten-Tool.
  3. Das Modell ruft das Render-Tool mit diesen Daten auf.
  4. Das Widget wird einmal mit dem endgültigen, vom Modell geprüften Kontext gerendert.

Beispiel: Folgefragen zu Immobilien

Angenommen, dein Plug-in zeigt Immobilienangebote als Karten und eine Landkarte an, aber dein serverseitiges Tool search unterstützt nur allgemeine Filter (Stadt, Preis, Schlafzimmer, Badezimmer) und kann nicht nach Schuleinzugsgebiet filtern.

Bei einer Frage wie „Welche dieser Immobilien liegen im Einzugsgebiet der Richmond Primary School?“ hilft die Entkopplung:

  1. search führt eine breit angelegte Suche aus und gibt die IDs potenziell passender Immobilienangebote samt Metadaten zurück.
  2. Das Modell grenzt diese Auswahl anhand der Folgefrage weiter ein.
  3. Das Modell ruft render_listings_widget nur mit den gefilterten IDs auf.
  4. Das Widget rendert die endgültige gefilterte Auswahl.

Bewährte Methoden:

  • Gestalte Daten-Tools wiederverwendbar. Gib vollständige Daten in structuredContent zurück, damit sich Tool-Aufrufe verketten lassen.
  • Beschränke Render-Tools auf die Darstellung. Vermische die Geschäftslogik nicht mit dem Render-Handler.
  • Gib die Abhängigkeit in der Beschreibung des Render-Tools an (zum Beispiel: „Rufe immer zuerst roll_dice auf“).
  • Lasse erneute Ausführungen nur gezielt zu. Ermögliche der UI, Daten-Tools bei lokalen Interaktionen wie „Erneut würfeln“ direkt aufzurufen, ohne das Widget erneut zu mounten.

Beispiel für Entkopplung

Beispiel (entkoppelte Würfel-Tools):

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod/v3";

const TEMPLATE_URI = "ui://widget/dice.html";

const server = new McpServer(
  { name: "Decoupled dice", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// The widget only renders the latest tool result.
// Re-roll calls the data tool directly to avoid remounting the widget.
const widgetHtml = `
  <div style="font-family: system-ui; padding: 8px;">
    <div style="font-size: 20px; margin-bottom: 6px;">
      Result: <span id="out">—</span>
    </div>
    <button id="reroll">Re-roll</button>
  </div>

  <script>
    const outputEl = document.getElementById("out");
    const rerollButton = document.getElementById("reroll");
    const pendingRequests = new Map();
    let nextRequestId = 1;
    let latestToolInput;
    let latestToolOutput;

    function render(result) {
      outputEl.textContent = String(result?.value ?? "—");
    }

    function request(method, params) {
      const id = nextRequestId++;
      window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
      return new Promise((resolve, reject) => {
        pendingRequests.set(id, { resolve, reject });
      });
    }

    window.addEventListener(
      "message",
      (event) => {
        if (event.source !== window.parent) return;
        const message = event.data;
        if (!message || message.jsonrpc !== "2.0") return;

        if (message.id !== undefined && pendingRequests.has(message.id)) {
          const pending = pendingRequests.get(message.id);
          pendingRequests.delete(message.id);
          if (message.error) pending.reject(message.error);
          else pending.resolve(message.result);
          return;
        }

        if (message.method === "ui/notifications/tool-input") {
          latestToolInput = message.params;
        }

        if (message.method === "ui/notifications/tool-result") {
          latestToolOutput = message.params?.structuredContent;
          render(latestToolOutput);
        }
      },
      { passive: true }
    );

    rerollButton.onclick = async () => {
      const sides = latestToolOutput?.sides ?? latestToolInput?.sides ?? 6;
      const next = await request("tools/call", {
        name: "roll_dice",
        arguments: { sides },
      });
      if (next?.structuredContent) {
        render(next.structuredContent);
      }
    };
  </script>
`.trim();

server.registerResource("dice-widget", TEMPLATE_URI, {}, async () => ({
  contents: [
    {
      uri: TEMPLATE_URI,
      mimeType: "text/html;profile=mcp-app",
      text: widgetHtml,
      _meta: { ui: { prefersBorder: true } },
    },
  ],
}));

// 1) Data tool: no output template, returns chainable structuredContent.
server.registerTool(
  "roll_dice",
  {
    title: "Roll dice",
    description: "Roll an N-sided die and return { sides, value }.",
    inputSchema: { sides: z.number().int().min(2) },
    outputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    _meta: {
      "openai/toolInvocation/invoking": "Rolling…",
      "openai/toolInvocation/invoked": "Rolled.",
    },
  },
  async ({ sides }) => {
    const value = 1 + Math.floor(Math.random() * sides);
    return {
      structuredContent: { sides, value },
      content: [{ type: "text", text: `Rolled ${value} on ${sides} sides.` }],
    };
  }
);

// 2) Render tool: owns the template and requires data from roll_dice.
server.registerTool(
  "render_dice_widget",
  {
    title: "Render dice widget",
    description:
      "Render the dice widget from roll data. First call roll_dice, then pass its sides and value to this tool.",
    inputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    outputSchema: {
      sides: z.number().int().min(2),
      value: z.number().int().min(1),
    },
    _meta: {
      ui: { resourceUri: TEMPLATE_URI },
      "openai/toolInvocation/invoking": "Rendering…",
      "openai/toolInvocation/invoked": "Rendered.",
    },
  },
  async ({ sides, value }) => ({
    structuredContent: { sides, value },
    content: [
      {
        type: "text",
        text: `Showing a ${sides}-sided roll: ${value}.`,
      },
    ],
  })
);

export default server;

Zustand verwalten

Die UI eines MCP-Servers arbeitet mit drei Arten von Zustand:

Art des ZustandsVerantwortliches SystemLebensdauerBeispiele
Geschäftsdaten (maßgeblich)MCP-Server oder externer DienstLanglebigAufgaben, Tickets, Dokumente
UI-Zustand (flüchtig)UI-InstanzSolange die UI-Instanz aktiv istAusgewählte Zeile, aufgeklappter Bereich, Sortierreihenfolge
Sitzungsübergreifender Zustand (dauerhaft)Speicher unter deiner KontrolleSitzungs- und unterhaltungsübergreifendGespeicherte Filter, Ansichtsmodus, Workspace

Belasse jeden Wert in dem System, das dafür zuständig ist. Die UI sollte maßgebliche Daten aus Tool-Ergebnissen rendern und diese um einen temporären Zustand für die Darstellung ergänzen.

MCP server or external service

├── Authoritative business data


UI

├── Ephemeral presentation state

└── Rendered view = business data + UI state

Geschäftsdaten auf dem Server halten

Geschäftsdaten sind die maßgebliche Datenquelle. Speichere sie nicht ausschließlich in der UI. Wenn jemand eine Aktion ausführt:

  1. Die UI ruft ein MCP-Tool auf.
  2. Der Server validiert die Anfrage und aktualisiert die Daten.
  3. Der Server gibt den aktualisierten, maßgeblichen Snapshot zurück.
  4. Die UI rendert den Snapshot und behält dabei den Darstellungszustand bei, soweit er kompatibel ist.

Gib genügend strukturierte Inhalte zurück, damit sowohl das Modell als auch die UI den neuen Zustand verstehen. So bleibt die Unterhaltung auch dann nützlich, wenn die UI nicht geladen werden kann.

Temporären UI-Zustand in der UI halten

Verwende die Zustandsverwaltung deines Frameworks für Werte, die nur die Darstellung beeinflussen, etwa ein ausgewähltes Element, einen geöffneten Bereich oder einen Filterentwurf. Jede gerenderte UI-Instanz hat ihren eigenen Zustand.

Wenn das Modell über eine Auswahl oder eine vorbereitete Änderung informiert sein muss, sende diese Informationen über ui/update-model-context. Das ist der portable Mechanismus von MCP Apps, um den für das Modell sichtbaren Kontext zu aktualisieren.

ChatGPT bietet außerdem optionale Persistenz für einzelne Widgets:

  • Lies den aktuellen Snapshot aus window.openai.widgetState.
  • Schreibe mit window.openai.setWidgetState(state) einen neuen Snapshot.

setWidgetState ist synchron. Rufe die Funktion nach jeder wesentlichen Änderung des UI-Zustands auf. Es gibt nichts, worauf du mit await warten müsstest.

import { useState } from "react";

export function TaskList({ tasks }) {
  const [state, setState] = useState(
    window.openai?.widgetState ?? { selectedId: null }
  );

  function selectTask(selectedId) {
    const nextState = { ...state, selectedId };
    setState(nextState);
    window.openai?.setWidgetState?.(nextState);
  }

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <button
            type="button"
            aria-pressed={state.selectedId === task.id}
            onClick={() => selectTask(task.id)}
          >
            {task.title}
          </button>
        </li>
      ))}
    </ul>
  );
}

Der Widget-Zustand gehört zu einer einzelnen gerenderten UI-Instanz. Verwende ihn weder als maßgebliche Quelle für Geschäftsdaten noch als dauerhaften Speicher.

Bilder für das Modell sichtbar machen

Verwende für eine UI, die mit Bildern arbeitet, das strukturierte Format für den Widget-Zustand:

  • modelContent: Text oder JSON, den oder das das Modell sehen soll.
  • privateContent: Zustand nur für die UI, den das Modell nicht sehen soll.
  • imageIds: Datei-IDs, die das Modell in späteren Gesprächsrunden erhalten soll.
window.openai.setWidgetState({
  modelContent: "Review the currently selected images.",
  privateContent: {
    currentView: "image-viewer",
    filters: ["crop", "sharpen"],
  },
  imageIds: ["file_123", "file_456"],
});

Nimm nur IDs von Dateien auf, die mit window.openai.uploadFile hochgeladen oder mit window.openai.selectFiles ausgewählt wurden, über Dateiparameter in Tool-Eingaben eingegangen sind oder als Dateiverweise in Tool-Ergebnissen zurückgegeben wurden.

Sitzungsübergreifenden Zustand auf deinem Server speichern

Speichere Einstellungen und Daten, die über Unterhaltungen, Geräte oder Sitzungen hinweg erhalten bleiben müssen, in einem Speicher unter deiner Kontrolle. Authentifiziere die nutzende Person, damit der MCP-Server jede Anfrage dem richtigen Konto zuordnen kann.

Wenn du dauerhaften Speicher hinzufügst:

  • Halte die Latenz niedrig genug für eine interaktive UI.
  • Schütze private Daten durch serverseitige Autorisierung.
  • Berücksichtige Anforderungen an Datenresidenz und Compliance.
  • Wende Ratenlimits auf Anfragen durch Wiederholungsversuche oder gleichzeitig aktive UI-Instanzen an.
  • Versioniere gespeicherte Objekte, damit du sie migrieren kannst, ohne bestehende Unterhaltungen zu beeinträchtigen.

Verwende localStorage nicht für zentrale Zustandsdaten. Die UI läuft in einem isolierten iframe, und der Browserspeicher bietet keine zuverlässige geräte- oder sitzungsübergreifende Datenschicht.

Das Grundgerüst des Komponentenprojekts erstellen

Jetzt kennst du die MCP Apps Bridge und die optionalen ChatGPT-Erweiterungen. Als Nächstes erstellst du das Grundgerüst deines Komponentenprojekts.

Es empfiehlt sich, den Komponentencode von der Serverlogik zu trennen. Eine gängige Struktur sieht so aus:

plugin-ui/
  server/            # MCP server (Python or Node)
  web/               # Component bundle source
    package.json
    tsconfig.json
    src/component.tsx
    dist/component.js   # Build output

Erstelle das Projekt und installiere die Abhängigkeiten (Node 18+ empfohlen):

cd plugin-ui/web
npm init -y
npm install react@^18 react-dom@^18
npm install -D typescript esbuild

Wenn deine Komponente Bibliotheken für Drag-and-drop, Diagramme oder andere Funktionen benötigt, füge sie jetzt hinzu. Beschränke die Abhängigkeiten auf das Nötige, um die Bundle-Größe zu reduzieren.

Die React-Komponente schreiben

Deine Einstiegsdatei sollte eine Komponente in ein root-Element einhängen und sie anhand des neuesten Tool-Ergebnisses rendern, das über die MCP Apps Bridge übermittelt wird (zum Beispiel ui/notifications/tool-result).

Auf der Beispielseite findest du UI-Beispiele, etwa die Pizzaz-Liste mit Pizzerien.

Die UI-Beispiele enthalten Beispielkomponenten. Nutze sie als Vorlagen für die Gestaltung deiner eigenen UI:

  • Pizzaz List: Nach Rang sortierte Kartenliste mit Favoriten und Aktionsschaltflächen.
    Screenshot der Komponente Pizzaz List
  • Pizzaz Carousel: Horizontaler, mit Embla umgesetzter Scrollbereich, der Layouts mit vielen Medieninhalten demonstriert.
    Screenshot der Komponente Pizzaz Carousel
  • Pizzaz Map: Mapbox-Integration mit Vollbildinspektor und Zustandssynchronisierung mit dem Host.
    Screenshot der Komponente Pizzaz Map
  • Pizzaz Album: Gestapelte Galerieansicht, um einen einzelnen Ort im Detail zu erkunden.
    Screenshot der Komponente Pizzaz Album
  • Pizzaz Video: Skriptgesteuerter Player mit Overlays und Vollbildsteuerung.

Jedes Beispiel zeigt, wie du Assets bündelst, Host-APIs anbindest und den Zustand für tatsächliche Unterhaltungen strukturierst. Kopiere das Beispiel, das deinem Anwendungsfall am nächsten kommt, und passe die Datenschicht an deine Tool-Antworten an.

Hilfs-Hooks für React

Eine kleine Hilfsfunktion zum Abonnieren von ui/notifications/tool-result:

type ToolResult = { structuredContent?: unknown } | null;

export function useToolResult() {
  const [toolResult, setToolResult] = useState<ToolResult>(null);

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (event.source !== window.parent) return;
      const message = event.data;
      if (!message || message.jsonrpc !== "2.0") return;
      if (message.method !== "ui/notifications/tool-result") return;
      setToolResult(message.params ?? null);
    };

    window.addEventListener("message", onMessage, { passive: true });
    return () => window.removeEventListener("message", onMessage);
  }, []);

  return toolResult;
}

Rendere die UI anhand von toolResult?.structuredContent und behandle diese Daten als nicht vertrauenswürdige Eingabe.

Widget-Lokalisierung

Der Host überträgt das Gebietsschema nach document.documentElement.lang. Verwende dieses Gebietsschema, um Übersetzungen zu laden sowie Datumsangaben und Zahlen zu formatieren. Ein gängiges Muster mit react-intl:

import { IntlProvider } from "react-intl";
import en from "./locales/en-US.json";
import es from "./locales/es-ES.json";

const messages: Record<string, Record<string, string>> = {
  "en-US": en,
  "es-ES": es,
};

export function PluginUI() {
  const locale = document.documentElement.lang || "en-US";
  return (
    <IntlProvider
      locale={locale}
      messages={messages[locale] ?? messages["en-US"]}
    >
      {/* Render UI with <FormattedMessage> or useIntl() */}
    </IntlProvider>
  );
}

Ein Bundle für das iframe erstellen

Wenn deine React-Komponente fertig ist, kannst du sie zu einem einzigen JavaScript-Modul bündeln, das der Server inline einbetten kann:

// package.json
{
  "scripts": {
    "build": "esbuild src/component.tsx --bundle --format=esm --outfile=dist/component.js"
  }
}

Führe npm run build aus, um dist/component.js zu erzeugen. Wenn esbuild fehlende Abhängigkeiten meldet, prüfe, ob du npm install im Verzeichnis web/ ausgeführt hast und ob deine Imports mit den Namen der installierten Pakete übereinstimmen (zum Beispiel @react-dnd/html5-server-side im Vergleich zu react-dnd-html5-server-side).

Die Komponente in die Serverantwort einbetten

Stelle die Komponente als MCP-Ressource mit dem MIME-Typ für MCP Apps UI (text/html;profile=mcp-app) bereit. Wenn du @modelcontextprotocol/ext-apps/server verwendest, nutze vorzugsweise RESOURCE_MIME_TYPE, statt die Zeichenfolge direkt einzubetten:

import {
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { readFileSync } from "node:fs";

const component = readFileSync("web/dist/component.js", "utf8");

registerAppResource(
  server,
  "project-board",
  "ui://project-board/v1.html",
  {},
  async () => ({
    contents: [
      {
        uri: "ui://project-board/v1.html",
        mimeType: RESOURCE_MIME_TYPE,
        text: `<div id="root"></div><script type="module">${component}</script>`,
        _meta: {
          ui: {
            prefersBorder: true,
            domain: "https://example.com",
            csp: {
              connectDomains: ["https://api.example.com"],
              resourceDomains: ["https://static.example.com"],
            },
          },
        },
      },
    ],
  })
);

Verknüpfe den Ressourcen-URI nur mit den Tools, die die Komponente rendern sollen. Verwende für eine breitere Kompatibilität mit MCP Apps _meta.ui.resourceUri. ChatGPT unterstützt auch _meta["openai/outputTemplate"] als Kompatibilitätsalias.

Behandle den Ressourcen-URI als Cache-Schlüssel. Wenn du eine nicht abwärtskompatible Änderung an HTML, JavaScript oder CSS vornimmst, veröffentliche einen neuen URI und aktualisiere jedes Tool, das darauf verweist.

Content Security Policy (CSP)

Gib genau die Domains an, zu denen die Komponente eine Verbindung herstellt oder von denen sie Ressourcen lädt:

  • connectDomains für API-Anfragen.
  • resourceDomains für Skripte, Styles, Bilder und andere Assets.
  • frameDomains nur dann, wenn die Komponente iframes bestimmter Origins einbetten muss.

Verschachtelte Frames sind standardmäßig blockiert. Halte jede Zulassungsliste so eng wie möglich. Bei der Überprüfung des Plug-ins wird die deklarierte Richtlinie mit dem Verhalten der UI abgeglichen.

Du kannst einen vorhandenen Editor oder eine Admin-Oberfläche von der eigenen registrierbaren Domain deines MCP-Servers einbetten. Beispielsweise kann ein Server unter https://api.example.com/mcp https://app.example.com in frameDomains deklarieren. Gib bei der Einreichung die erforderliche Begründung an und beachte die iframe-Richtlinie, einschließlich ihrer Einschränkungen für Shared Hosting und ihrer Review-Anforderungen.

Für den Produktivbetrieb werden UI-Vorlagen für Komponenten empfohlen.

Während der Entwicklung kannst du das Komponenten-Bundle bei jeder Änderung deines React-Codes neu erstellen und den Server per Hot Reload aktualisieren.

Checkout in deiner UI anbieten

Wenn du Nutzenden ermöglichen möchtest, Käufe über die UI deines Plug-ins abzuschließen, zeige mit der Komponente vor der Bestätigung Produkte, Preise, Bedingungen und Zahlungsoptionen an. Die zugrunde liegenden Katalog- und Bestelltools sollten auch ohne UI nutzbar bleiben. Wähle dann einen externen Checkout-Ablauf oder, sofern verfügbar, eine eingebettete Zahlungsoption.

Standardmäßig externen Checkout verwenden

Ein externer Checkout ist der empfohlene und allgemein verfügbare Ansatz. Verlinke aus der Komponente auf einen vom Händler gehosteten Checkout-Ablauf auf deiner eigenen Domain. Dort kümmerst du dich um:

  • Preise und Zahlungsabwicklung.
  • Steuern, Rabatte und Gebühren.
  • Versand und Auftragsabwicklung.
  • Rückerstattungen, Support und Compliance.

Die aktuelle Genehmigung beschränkt sich auf Plug-ins für den Kauf physischer Waren. Biete keine anderen Handelskategorien an, es sei denn, OpenAI hat sie ausdrücklich für dein Plug-in freigeschaltet.

Gespeicherte Zahlungsmethoden verwenden

Bei zulässigen Käufen physischer Waren kann eine optionale UI deiner Kundschaft ermöglichen, eine zuvor bei deinem Dienst gespeicherte Zahlungsmethode auszuwählen. Dieser Ablauf kann zulässige gespeicherte Methoden anzeigen, aber keine neuen Zahlungsdaten erfassen. Dein MCP-Server verarbeitet den Kauf und gibt das verbindliche Bestellergebnis zurück.

Den ChatGPT-Zahlungsdialog verwenden

Der eingebettete Checkout mit dem ChatGPT-Zahlungsdialog befindet sich für ausgewählte Marktplätze in einer privaten Betaphase und steht nicht allen Entwickelnden oder Nutzenden zur Verfügung.

Bei freigeschalteten Integrationen öffnet window.openai.requestCheckout den ChatGPT-Zahlungsdialog:

const order = await window.openai.requestCheckout(checkoutSession);

Der Checkout-Ablauf besteht aus vier Teilen:

  1. Ein MCP-Tool gibt eine Checkout-Sitzung in structuredContent zurück.
  2. Die Komponente zeigt die einzelnen Positionen, Gesamtbeträge, Bedingungen und Optionen zur Bestellabwicklung an.
  3. Die Komponente ruft requestCheckout(checkoutSession) auf, nachdem sich die nutzende Person für die Zahlung entschieden hat.
  4. ChatGPT sendet das ausgewählte Zahlungstoken an das Tool complete_checkout des MCP-Servers. Dieses belastet die Zahlungsmethode und gibt die abgeschlossene Bestellung zurück.

Die Checkout-Sitzung muss Folgendes enthalten:

  • Eine eindeutige Sitzungs-ID.
  • Einzelne Positionen und Mengen.
  • Gesamtbeträge als ganze Zahlen in der kleinsten Währungseinheit.
  • Metadaten zum Zahlungsanbieter und Händler.
  • Erforderliche Links zu rechtlichen Informationen, Datenschutz, Rückerstattungen und Support.

Nutze den Server als maßgebliche Quelle für Preise und Bestellstatus. Überprüfe das Zahlungstoken, gestalte den Vorgang idempotent, speichere die Bestellung dauerhaft und gib einen verbindlichen Beleg zurück. Vertraue niemals auf Gesamtbeträge, die ausschließlich in der Komponente berechnet wurden.

Verwende payment_mode: "test", um den gesamten Ablauf zu testen, ohne echtes Geld zu bewegen. Behandle Abbrüche, abgelehnte Zahlungen und Fehler des Zahlungsanbieters in der Komponente.

Alle Felder der Checkout-Sitzung, das Verhalten des Zahlungsanbieters, die Ergebnisstruktur von complete_checkout und die Anforderungen an delegierte Zahlungen findest du in der Checkout-API-Referenz.