Ü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:
- Deklariere die UI-Ressource mit
_meta.ui.resourceUri. - Verwende die JSON-RPC-Bridge
ui/*überpostMessagefür die Initialisierung, Benachrichtigungen, Tool-Aufrufe, Nachrichten und für das Modell sichtbaren Kontext. - 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:
| Ziel | Standard MCP Apps | ChatGPT-Kompatibilitätsalias |
|---|---|---|
| Ein Tool mit einer UI-Ressource verknüpfen | _meta.ui.resourceUri | _meta["openai/outputTemplate"] |
| Tool-Eingaben empfangen | ui/initialize + ui/notifications/tool-input | window.openai.toolInput |
| Tool-Ergebnisse empfangen | ui/notifications/tool-result | window.openai.toolOutput |
| Ein Tool aus der UI aufrufen | tools/call | window.openai.callTool |
| Eine Folgenachricht senden | ui/message | window.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.selectFilesundwindow.openai.getFileDownloadUrl. - Vom Host gesteuerte modale Dialoge mit
window.openai.requestModal. - Speichern des Widget-Zustands mit
window.openai.widgetStateundwindow.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.

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

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.

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.

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:
- Das Modell ruft das Daten-Tool auf (zum Beispiel
roll_dice). - Das Modell erhält
structuredContentvom Daten-Tool. - Das Modell ruft das Render-Tool mit diesen Daten auf.
- 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:
searchführt eine breit angelegte Suche aus und gibt die IDs potenziell passender Immobilienangebote samt Metadaten zurück.- Das Modell grenzt diese Auswahl anhand der Folgefrage weiter ein.
- Das Modell ruft
render_listings_widgetnur mit den gefilterten IDs auf. - Das Widget rendert die endgültige gefilterte Auswahl.
Bewährte Methoden:
- Gestalte Daten-Tools wiederverwendbar. Gib vollständige Daten in
structuredContentzurü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_diceauf“). - 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 Zustands | Verantwortliches System | Lebensdauer | Beispiele |
|---|---|---|---|
| Geschäftsdaten (maßgeblich) | MCP-Server oder externer Dienst | Langlebig | Aufgaben, Tickets, Dokumente |
| UI-Zustand (flüchtig) | UI-Instanz | Solange die UI-Instanz aktiv ist | Ausgewählte Zeile, aufgeklappter Bereich, Sortierreihenfolge |
| Sitzungsübergreifender Zustand (dauerhaft) | Speicher unter deiner Kontrolle | Sitzungs- und unterhaltungsübergreifend | Gespeicherte 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:
- Die UI ruft ein MCP-Tool auf.
- Der Server validiert die Anfrage und aktualisiert die Daten.
- Der Server gibt den aktualisierten, maßgeblichen Snapshot zurück.
- 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 Pizzaz-Komponentengalerie erkunden
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.

- Pizzaz Carousel: Horizontaler, mit Embla umgesetzter Scrollbereich, der Layouts mit vielen Medieninhalten demonstriert.

- Pizzaz Map: Mapbox-Integration mit Vollbildinspektor und Zustandssynchronisierung mit dem Host.

- Pizzaz Album: Gestapelte Galerieansicht, um einen einzelnen Ort im Detail zu erkunden.

- 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:
connectDomainsfür API-Anfragen.resourceDomainsfür Skripte, Styles, Bilder und andere Assets.frameDomainsnur 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:
- Ein MCP-Tool gibt eine Checkout-Sitzung in
structuredContentzurück. - Die Komponente zeigt die einzelnen Positionen, Gesamtbeträge, Bedingungen und Optionen zur Bestellabwicklung an.
- Die Komponente ruft
requestCheckout(checkoutSession)auf, nachdem sich die nutzende Person für die Zahlung entschieden hat. - ChatGPT sendet das ausgewählte Zahlungstoken an das Tool
complete_checkoutdes 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.