1. Einleitung
Das Projekt ChatGPT App HelloWorld ist keine „magische Blackbox von CodeGym, an der man besser nichts anfasst“. Es ist ein ganz normales Next.js‑Projekt, in dem lediglich gleichzeitig Folgendes lebt:
- ein Frontend, das innerhalb von ChatGPT gerendert wird,
- ein MCP‑Server, der auf Tool‑Aufrufe reagiert,
- Konfigurationen, die das Ganze mit ChatGPT zusammenführen.
Wenn man nicht weiß, wo was liegt, passieren typischerweise drei klassische Szenarien:
- Ein Entwickler schreibt versehentlich window in eine Serverdatei, kassiert einen Crash und beginnt, den gesamten Stack zu hassen.
- Er versucht, im UI einen Button hinzuzufügen, editiert aber die falsche page.tsx (z. B. die App‑Wurzel statt des Widgets) und sieht in ChatGPT keine Änderungen.
- Er legt den OPENAI_API_KEY versehentlich im Client‑Teil ab – und der Schlüssel landet im Browser.
Deshalb ist das heutige Ziel – die Karte zu zeichnen: Wo ist das UI, wo MCP, wo die Konfigs – und wohin Sie gehen, wenn Sie Folgendes wollen:
- das Erscheinungsbild des Widgets ändern;
- ein neues Tool hinzufügen;
- eine Plattform‑Einstellung anpassen (CORS, assetPrefix usw.).
2. Architektur des Projekts auf hoher Ebene
Das Next.js‑Projekt ChatGPT App HelloWorld verwendet den App Router und ist um den Ordner app/ organisiert. Darin leben im selben Seitenbaum:
- das UI des Widgets, das innerhalb von ChatGPT gerendert wird,
- ein MCP‑Endpoint, der Tool‑Aufrufe verarbeitet.
Typischer Baum (vereinfacht; die Ordnernamen in Ihrer Vorlage können abweichen, das Muster ist jedoch dasselbe):
my-chatgpt-app/
├─ app/
│ ├─ api/ // REST API
│ │ └─ time/ // GET /api/time gibt die Serverzeit zurück
│ │ └─ route.ts
│ ├─ hooks/ // Satz von Hooks aus dem offiziellen Apps SDK
│ │ ├─ use-call-tool.ts
│ │ ├─ use-display-mode.ts
│ │ └─ use-open-external.ts
│ ├─ mcp/ // MCP‑Server: Hier klopft ChatGPT an, wenn es Tools aufruft
│ │ └─ route.ts
│ ├─ globals.css // Globales globals.css der gesamten App
│ ├─ layout.tsx // Root‑Layout der gesamten App
│ └─ page.tsx // Seite des Widgets innerhalb von ChatGPT
├─ public/ // Statische Dateien: Icons, Manifest usw.
├─ next.config.ts // Next.js‑Konfiguration und App‑spezifische Einstellungen (assetPrefix u. a.)
├─ proxy.ts // CORS/Headers für den Betrieb im iframe (ehemals middleware.ts)
├─ package.json // Projektabhängigkeiten
├─ tsconfig.json // TypeScript‑Konfiguration
└─ .env.local // Geheimnisse: OPENAI_API_KEY u. a.
Wenn es mehrere Widgets gibt, legt man sie üblicherweise nicht in app/page.tsx, sondern in app/widget/page.tsx ab. Die Logik bleibt jedoch gleich: Es gibt dennoch eine Seite (Widget) und einen Endpoint, der die Rolle des MCP‑Servers spielt.
Hilfreich ist folgende Denkweise: Ihr Repository ist ein „zweigesichtiger Janus“:
- das eine „Gesicht“ – der Pfad /mcp, den ChatGPT nimmt, wenn ein Tool aufgerufen werden soll;
- das andere „Gesicht“ – der Pfad /widget (oder /), der im iframe geladen wird, sobald das Modell Ihr UI anzeigen möchte.
Um Verwechslungen zu vermeiden, merken wir uns drei Dateigruppen:
- UI‑Schicht – alles, was mit React/Next‑Seiten zusammenhängt (app/widget, Komponenten, Styles).
- MCP‑Schicht – app/mcp/route.ts und Dateien, die dort verwendet werden.
- Klebstoff‑Schicht und Konfigs – next.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.
Weiter unten gehen wir jede dieser Schichten durch.
3. Wo das Widget lebt: der Ordner app/widget und/oder app/page.tsx
Beginnen wir mit dem, was Sie am häufigsten anfassen werden – dem Widget, also dem UI, das innerhalb von ChatGPT sichtbar ist.
In den meisten aktuellen Projekten gibt es entweder:
- den Pfad app/widget/page.tsx – das Widget lebt unter dem separaten Präfix /widget,
- oder eine Wurzel‑app/page.tsx – das Widget entspricht der Startseite.
Hauptmerkmale der Widget‑Datei:
- ganz oben steht 'use client', da die Komponente im Browser läuft, mit window und dem Apps SDK kommuniziert;
- es ist eine ganz normale React‑Komponente, die Markup rendert und (ein wenig später im Kurs) mit window.openai kommuniziert.
Ein einfachstes Beispiel eines Lern‑Widgets (etwas sehr Ähnliches sehen Sie wahrscheinlich bereits in Ihrem Projekt):
// app/widget/page.tsx
'use client';
import React from 'react';
export default function WidgetPage() {
return (
<main className="p-4">
<h1 className="text-xl font-semibold">
HelloWorld — ChatGPT App
</h1>
<p className="text-sm text-gray-500">
Hier werden wir das UI unseres Widgets bauen.
</p>
</main>
);
}
Liegt Ihr Widget direkt in app/page.tsx, ist der Code nahezu identisch – nur ohne den Zwischenordner widget.
Achten Sie auf ein paar Punkte.
Erstens ist die Direktive 'use client' obligatorisch: Das Widget liest/schreibt in window.openai, hört auf Events usw. – das ist nur in einer Client‑Komponente möglich. Entfernen Sie sie, versucht Next, die Seite als Server‑Komponente zu behandeln – und Sie sehen Fehler wie „window is not defined“.
Zweitens ist es eine ganz und gar unmagische React‑Komponente. Sie können:
- sie in Unterkomponenten in components/ zerlegen,
- Tailwind oder ein anderes CSS‑System verwenden,
- Kontexte, Hooks usw. einbinden.
Drittens werden Sie später genau hier:
- window.openai.toolInput und window.openai.toolOutput lesen, um reale Daten zu rendern,
- den widgetState speichern – über window.openai.setWidgetState,
- openExternal, callTool und weitere Methoden der Runtime aufrufen.
Für den Moment reicht: Wenn Sie die visuelle Oberfläche ändern möchten – führt der Weg fast sicher nach app/widget/page.tsx oder app/page.tsx.
4. Root‑Layout: app/layout.tsx als „Rahmen“ für die gesamte Anwendung
Die nächste wichtige Datei ist app/layout.tsx. Sie:
- definiert die HTML‑Struktur (<html>, <body>),
- bindet globale Styles ein (globals.css),
- initialisiert oft das „Bootstrap“ für das Apps SDK (einen Wrapper, der window.openai abhört und Daten in React durchreicht).
Vereinfachtes Beispiel:
// app/layout.tsx
import './globals.css';
import type { ReactNode } from 'react';
import { OpenAIAppProvider } from '@/lib/openai-app-provider';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<NextChatSDKBootstrap baseUrl={baseURL} />
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased h-full overflow-hidden`}>
{children}
</body>
</html>
);
}
Der Name NextChatSDKBootstrap ist hier exemplarisch; in Ihrer Vorlage kann es OpenAIAppProvider oder eine andere Komponente sein. Ihre Aufgabe ist meist dieselbe: die Verbindung zwischen dem React‑Baum und der Apps‑SDK‑Runtime herzustellen, globale Daten (theme, displayMode, toolInput usw.) zu abonnieren und sie an die Kinder zu verteilen.
Wichtige praktische Folgerung: Wenn Sie einen globalen Kontext, Styles oder eine UI‑Bibliothek (z. B. shadcn/ui) einbinden möchten – der richtige Ort ist fast immer app/layout.tsx (oder ein Layout innerhalb von app/widget für Einstellungen und Komponenten, die speziell fürs Widget sind).
Analyse von NextChatSDKBootstrap
NextChatSDKBootstrap habe ich im offiziellen Template von Vercel gesehen. Falls Sie es nicht wussten: Das sind die Leute, die Next erfunden und groß gemacht haben. Auf ihrer Seite gibt es einen guten Beitrag über ChatGPT‑App mit Next. Außerdem gibt es ein Starter Template. Auch wenn es an ein paar Stellen leicht veraltet ist, stehen die Chancen gut, dass sie es aktuell halten werden.
Wir heben fünf zentrale Dinge hervor, die NextChatSDKBootstrap uns bringt:
- 1. Behebt Hydrationsprobleme
Der Punkt ist: ChatGPT lädt zunächst das HTML Ihres Widgets auf seinen Server, bereinigt und patcht es. In der Folge meckert der Hydrationsmechanismus und streut Warnungen in die Konsole. Das kann Ihr Review behindern. - 2. Patcht die Browser‑Historie
Ihr Widget wird in einem iframe von einer speziellen Domain in ChatGPT geladen. Wenn Sie Ihre eigene Domain verwenden, durchbrechen Sie die Sandbox. Daher wird in der Browser‑Historie nur der Pfad ohne Domain gespeichert. - 3. Überschreibt die Funktion fetch()
Ihr gesamtes fetch() auf relative Adressen ohne Domain funktioniert im Widget nicht, da die Domain des iframe eine andere ist. Deshalb ersetzen wir fetch() durch eine Variante, die Anfragen ohne Domain an die richtige URL sendet. Ist eine Domain angegeben, bleibt alles unverändert. - 4. Klicks auf Links funktionieren
Wenn Links innerhalb des iframe geöffnet werden, gefällt das ChatGPT nicht. Deshalb wurde Code hinzugefügt, der Linkklicks abfängt und sie in einem externen Fenster via openExternal() öffnet. - 5. Setzen von head base (DEPRECATED)
Dieser Code hat außerdem <base> in den <head> gesetzt – das funktioniert nicht mehr. Die Sandbox setzt jeden gesetzten base zurück, daher empfehle ich, überall absolute Links zu verwenden: Skripte, Ressourcen, Schriften, API usw.
5. MCP‑Server: app/mcp/route.ts
Jetzt kommen wir zur zweiten Hälfte des „zweigesichtigen Janus“ – dem Server, der per MCP mit ChatGPT spricht.
Die Datei app/mcp/route.ts ist ein gewöhnlicher Route Handler des App Routers, der:
- HTTP‑Anfragen von ChatGPT annimmt (meist POST mit JSON‑Payload im MCP‑Format),
- sie an den MCP‑Server übergibt (auf Basis von @modelcontextprotocol/sdk oder einer dünnen Hülle),
- und die JSON‑Antwort im MCP‑Format zurückgibt.
Es gibt zwei Varianten: direkt auf dem nackten MCP SDK oder mit einigen Klassen von Next/Vercel, um Ecken abzurunden.
Hier eine Variante mit purem TS‑MCP‑SDK:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// 1. MCP‑Server erstellen
const server = new McpServer({
name: "simple-mcp-server",
version: "1.0.0",
});
// 2. MCP‑Resources registrieren
// 3. MCP‑Tools registrieren
// 4. HTTP‑Transport
const transport = new HttpServerTransport({
port: 3001,
path: "/mcp",
});
// 5. Server starten
await server.connect(transport);
Angenehmer ist es, ein paar fertige Klassen zu verwenden:
// app/mcp/route.ts
import { NextRequest } from 'next/server';
import { createMcpHandler } from "mcp-handler";
const handler = createMcpHandler(async (server) => {
const gateway = new McpGateway(server);
await gateway.initialize();
gateway.registerResources();
gateway.registerTools();
});
export const GET = handler;
export const POST = handler;
Hier ist McpGateway eine Wrapper‑Klasse um McpServer, die Sie irgendwo (z. B. in lib/mcp/server.ts) mithilfe des SDK erstellen. In unserem Fall passt alles in app/mcp/route.ts. Schauen wir uns vollständig an, was in dieser Datei steckt.
type ContentWidget
Am Anfang der Datei ist der Typ ContentWidget beschrieben. Er enthält alle Widget‑Daten und wird an zwei Stellen verwendet: bei der Registrierung des Widgets als MCP‑Resource und wenn ein MCP‑Tool Metadaten zurückgibt, in denen es angibt, welches Widget zur Anzeige der von ihm gelieferten Daten verwendet werden soll.
type ContentWidget = {
id: string; // Eindeutiger Name/Key
title: string; // Title
description: string; // Description
templateUri: string; // Eindeutige URI des Widgets; kann beliebig sein. Hat keine Auswirkung.
invoking: string; // Beschriftung über dem Widget, solange es lädt
invoked: string; // Beschriftung über dem Widget, sobald es geladen ist
html: string; // Der gesamte HTML‑Code des Widgets.
widgetDomain: string; // "Domain" des Widgets. Hat keine Auswirkung.
};
class McpGateway
Eine Wrapper‑Klasse um McpServer, die manches vereinfacht. Sie enthält sechs Methoden:
- initialize() – hier laden wir das HTML unseres Widgets
- registerResources() – registriert Widgets als MCP‑Resources
- registerTools() – registriert Funktionen als MCP‑Tools
- widgetMeta() – gibt die Widget‑Metadaten zurück
- getAppsSdkCompatibleHtml() – lädt den HTML‑Code des Widgets und patcht ihn leicht
- makeImgUrlsAbsolute() – patcht HTML: macht Bild‑URLs absolut
Gehen wir sie im Detail durch:
public async initialize()
Diese Methode lädt den HTML‑Code der Widgets aus dem Internet und füllt ein Objekt vom Typ ContentWidget.
{
id: "hello_world", // Eindeutiger Widget‑Key
templateUri: "ui://widget/hello_world.html", // Eindeutige URI des Widgets. "ui:" bedeutet nichts.
title: "HelloWorld Widget", // Name des Widgets
description: "Displays the HelloWorld widget", // Erklärung für das LLM, was das Widget macht
invoking: "Loading widget...", // Beschriftung während des Ladens
invoked: "Widget loaded", // Beschriftung nach dem Laden
html: htmlWidget, // HTML des Widgets
widgetDomain: baseURL, // "Domain" des Widgets. Derzeit ohne Auswirkung.
}
public registerResources()
Registriert Widgets als MCP‑Resources. Ruft server.registerResource() auf, dem vier Parameter übergeben werden:
- ID/Key der MCP‑Ressource
- Ressourcen‑URI (das ist für das MCP‑Protokoll; für das Widget faktisch ein Synonym für eine eindeutige Adresse)
- Metadaten der MCP‑Ressource
- Funktion, die die MCP‑Ressource zurückgibt
Metadaten des Widgets
{
title: widget.title, // Name der Ressource/des Widgets
description: widget.description, // Beschreibung der Ressource/des Widgets
mimeType: "text/html+skybridge", // Wichtig! Nur solcher HTML wird als Widget angezeigt
_meta: {
"openai/widgetDescription": widget.description, // Beschreibung des Widgets
"openai/widgetPrefersBorder": true, // Weisen ChatGPT an, das Widget mit Rahmen zu rendern
},
}
Widget als MCP‑Ressource
{
uri: uri.href, // Unsere URI (kommt aus dem Parameter uri)
mimeType: "text/html+skybridge", // Wichtig! Nur solcher HTML wird als Widget angezeigt
text: widget.html, // HTML des Widgets
_meta: {
"openai/widgetDescription": widget.description, // Beschreibung des Widgets
"openai/widgetPrefersBorder": true, // Weisen ChatGPT an, einen Rahmen zu zeichnen
"openai/widgetDomain": widget.widgetDomain, // "Domain" des Widgets. Derzeit ohne Auswirkung.
"openai/widgetCSP": { // Wichtig! Für das Widget zugelassene Domains:
connect_domains: [ // Domains für Verbindungen (fetch etc.)
baseURL,
"https://codegym.cc",
],
resource_domains: [ // Domains für Ressourcen (CSS/Fonts/Images)
baseURL,
"https://codegym.cc",
"https://cdn.tailwindcss.com",
"https://persistent.oaistatic.com",
"https://fonts.googleapis.com",
"https://fonts.gstatic.com"
]
}
},
}
Auf openai/widgetCSP kommen wir in Zukunft noch öfter zurück; hier möchte ich zwei Punkte hervorheben:
- connect_domains – Liste der Domains für:
- fetch()
- das Laden von Skripten
- openExternal()
- resource_domains – Liste der Domains für:
- Bilder
- CSS
- Schriften
Theoretisch können Sie 200 Domains eintragen – ob Sie mit so einer Liste das Review bestehen, ist allerdings fraglich.
Ich habe diese Parameter auch bei bereits veröffentlichten Apps untersucht und dort amplitude.com gefunden. Ebenfalls eine gute Nachricht. Gute Analytics schadet niemandem.
public registerTools()
Registriert Funktionen als MCP‑Tools. Ruft server.registerTool() auf, dem drei Parameter übergeben werden:
- ID/Key des MCP‑Tools
- Metadaten des MCP‑Tools
- Funktion, die das MCP‑Tool zurückgibt
Metadaten des Tools
Alle Parameter in dieser Liste sind wichtig. Details dazu folgen in den nächsten Vorlesungen.
{
title: widget.title, // Name des Tools
description: "Returns HelloWorld widget", // Wichtig! Beschreibung dessen, was das Tool tut
inputSchema: z.object({}).describe("No inputs"), // Parameterschema des Tools. Zod ist möglich
_meta: this.widgetMeta(widget), // Widget‑Metadaten: welches Widget angezeigt wird
annotations: {
destructiveHint: false, // Die Methode macht etwas Wichtiges – Bestätigung erforderlich
openWorldHint: false, // Die Methode ändert etwas bei Drittanbieterdiensten
readOnlyHint: true // Die Methode ändert nichts
},
}
Funktion, die etwas Wichtiges erledigt
async (input, extra) => {
// 1. Validierung der Parameter
// 2. Wir tun etwas Wichtiges
return {
content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Beschreibung des Ergebnisses für die KI
structuredContent: { // Wichtig! Das ist der JSON des Ergebnisses.
timestamp: new Date().toISOString() // Kann beliebige Daten enthalten.
},
_meta: this.widgetMeta(widget), // Widget‑Metadaten, das den JSON anzeigt
}; // Kann fehlen – dann gibt es kein Widget
}
private widgetMeta(widget: ContentWidget)
Gibt die Metadaten des Widgets zurück – daran erkennt ChatGPT, welches Widget zur Anzeige des JSON‑Ergebnisses verwendet werden soll.
{
"openai/outputTemplate": widget.templateUri, // Widget‑URI
"openai/toolInvocation/invoking": widget.invoking, // Beschriftung über dem Widget, solange es lädt
"openai/toolInvocation/invoked": widget.invoked, // Beschriftung über dem Widget, sobald es geladen ist
"openai/widgetAccessible": true, // MCP‑Tool kann aus dem Widget aufgerufen werden
"openai/resultCanProduceWidget": true, // MCP‑Tool liefert ein Widget zurück
}
Separat möchte ich eine einfache Sache besprechen: "openai/outputTemplate". Im MCP‑Protokoll gibt es drei Entitäten (mehr dazu in Modul 6):
- MCP Resources
- MCP Templates
- MCP Tools
"openai/outputTemplate" hat nichts mit MCP Templates zu tun. MCP Templates werden in ChatGPT‑Apps überhaupt nicht verwendet. Das Wort „template“ kommt hierher:
Widgets waren als Vorlage zur Darstellung von JSON gedacht. Ein MCP‑Tool liefert irgendeinen JSON, die KI zeigt ein Widget an, übergibt ihm den JSON über den Parameter ToolOutput, und das Widget stellt diesen JSON ansprechend dar. outputTemplate ist einfach ein Synonym für Widget.
Ich denke, das reicht hier. Ausführlicher behandeln wir diese Dinge in Modul 4: wie man Tools beschreibt, JSON Schema und Handler. Jetzt genügt das Verständnis: Wenn etwas mit Tools und Logik zu tun hat – suchen Sie in der Nähe von app/mcp/route.ts.
6. Konfiguration und „Klebstoff“: next.config.ts, middleware.ts, .env und Co.
Sehen wir uns nun den Hauptsatz an Dateien an, die nötig sind, damit Ihr Next.js‑Projekt korrekt innerhalb des iframe von ChatGPT funktioniert und für ChatGPT über einen HTTPS‑Tunnel erreichbar ist (ngrok, Cloudflare Tunnel usw.; über Tunnel sprechen wir noch separat).
next.config.ts
In dieser Datei werden neben den Standard‑Einstellungen von Next.js häufig konfiguriert:
- assetPrefix – damit statische Dateien (JS, CSS aus /_next/) nicht von der ChatGPT‑Domain, sondern von Ihrer Dev‑URL (Tunnel oder Vercel) geladen werden;
- beliebige spezifische Einstellungen, die das Template benötigt (z. B. experimentelle Flags für Next 16).
In der Praxis sieht das wie ein normaler Export von nextConfig mit den benötigten Feldern aus. Für diese Vorlesung ist eines wichtig: Wenn das Widget in ChatGPT CSS/JS nicht laden kann, ist sehr häufig assetPrefix die Ursache.
proxy.ts (ehemals middleware.ts)
Diese Datei schiebt eine Middleware‑Schicht zwischen die Anfrage aus ChatGPT und Ihre Routen. Im Template:
- setzt sie CORS‑Header, damit das iframe von ChatGPT überhaupt auf Ihren Server zugreifen darf;
- und richtet manchmal zusätzliche Header für React Server Components ein.
Alle Feinheiten müssen Sie jetzt nicht kennen. Merken Sie sich nur: Wenn ChatGPT über CORS klagt oder Sie seltsame DevTools‑Fehler wegen verweigertem Zugriff sehen, schauen Sie in proxy.ts.
.env
Die Datei .env (oder .env.local) ist der Ort für Geheimnisse und Umgebungsvariablen:
- OPENAI_API_KEY (falls der MCP‑Server selbst das OpenAI‑API anspricht),
- Adressen Ihrer internen APIs,
- Token externer Dienste usw.
Ein wichtiger Punkt: In Next.js landen Variablen, die mit NEXT_PUBLIC_ beginnen, automatisch im JS‑Bundle und werden im Browser verfügbar. Tun Sie das niemals mit OPENAI_API_KEY; Geheimnisse gehören nur in Server‑Variablen.
package.json und tsconfig.json
In package.json sehen Sie:
- Versionen von Next.js, React, Apps SDK, MCP SDK und andere Abhängigkeiten,
- Skripte wie dev, build, start sowie ggf. Hilfskommandos (Linter, Formatter usw.).
In tsconfig.json liegen die üblichen TypeScript‑Einstellungen:
- Pfade der Aliase (@/lib, @/components),
- Strikter Modus,
- Compile‑Targets.
Aus Sicht dieses Kurses ist vor allem wichtig zu verstehen, dass die Vorlage auf einem normalen TypeScript‑Stack basiert – Sie können ihn ganz regulär erweitern.
7. Schneller „Projekt‑Navigator“ für Entwickler
Fixieren wir, wohin Sie gehen, wenn Sie typische Dinge erledigen wollen. Ohne Listen – einfach als Mini‑Szenarien.
Wenn Sie Text/Buttons im Widget ändern wollen, öffnen Sie die UI‑Datei des Widgets: Entweder app/widget/page.tsx oder app/page.tsx – je nach Template. Dort editieren Sie JSX, fügen neue Komponenten hinzu und binden eine Design‑System‑Bibliothek ein. Genau hier verwenden Sie die Apps‑SDK‑Runtime (window.openai oder praktische Hooks), um Daten anzuzeigen.
Wenn Sie einen neuen Button brauchen, der serverseitig etwas ausführt, beginnen Sie ebenfalls mit der UI‑Datei. Der Button im Widget ruft beim Klick window.openai.callTool auf, und die Implementierung dieses Tools fügen Sie in der Konfiguration des MCP‑Servers hinzu – also im Code rund um app/mcp/route.ts. Die Verbindung UI ↔ Tool‑Logik behandeln wir in den Modulen 4 ff.
Wenn Sie ChatGPT neue Funktionalität beibringen möchten (z. B. „Reisesuche“ oder „Produktempfehlungen“), gehen Sie in die MCP‑Schicht (Dateien, die aus app/mcp/route.ts importiert werden). Dort registrieren Sie ein neues Tool mit JSON Schema, Beschreibung und Handler. Das Widget kann das Ergebnis anschließend über window.openai.toolOutput lesen und hübsch anzeigen.
Wenn statische Assets nicht geladen werden oder das Widget nur in ChatGPT seltsam aussieht, lokal aber okay, denken wir an die Klebstoff‑Schicht. Prüfen Sie zuerst next.config.ts (insbesondere assetPrefix) und middleware.ts/proxy.ts (CORS). Wenn Sie kürzlich den Tunnel oder die URL geändert oder auf Vercel deployt haben, sind korrekte Einstellungen kritisch.
Wenn Sie Probleme mit Schlüsseln oder der Umgebung vermuten, ist Ihr Dreigespann – .env.local, package.json (um zu sehen, welche Abhängigkeiten und Skripte wirklich genutzt werden) und die Logs des Dev‑Servers. Genau dieses Trio stellt sicher, dass der MCP Zugriff auf die nötigen Geheimnisse und Dienste hat.
8. Mini‑Praxis: die Dateistruktur händisch kennenlernen
Theorie ist gut – aber lassen Sie uns händisch festhalten, wo was liegt. Diese Schritte können Sie sofort im Editor/der IDE durchführen.
Öffnen Sie in Ihrem Projekt den Ordner app und finden Sie die Datei, die für das Widget verantwortlich ist. Verwendet die Vorlage app/page.tsx, sehen Sie dort einen Hinweis wie „HelloWorld — ChatGPT App“ oder einen Willkommenstext. Gibt es keinen eigenen Ordner für das Widget, öffnen Sie app/page.tsx und vergewissern Sie sich, dass dort 'use client' und etwas JSX‑Markup vorhanden ist.
Suchen Sie anschließend app/mcp/route.ts. Achten Sie darauf, welche Module importiert werden: Üblicherweise sehen Sie entweder die direkte Nutzung des MCP‑SDKs oder den Aufruf einer Hilfsfunktion aus lib/mcp/*. Schätzen Sie ein, wie „dünn“ diese Zwischenschicht gehalten ist – idealerweise gibt es dort kaum Business‑Logik; nur „JSON empfangen → an den Server weitergeben → JSON zurückgeben“.
Danach schauen Sie in next.config.ts und proxy.ts/middleware.ts. Sie müssen nicht alles verstehen, was dort steht – merken Sie sich nur:
- next.config.ts ist für die Next‑Konfiguration verantwortlich, einschließlich der Regeln für Build und Auslieferung von Assets;
- proxy.ts greift in HTTP‑Anfragen ein (Sie sehen dort fast sicher Arbeiten mit Headern).
Und zum Schluss öffnen Sie .env oder .env.local und stellen sicher, dass Ihre Schlüssel genau dort liegen – und nicht im Code. Wenn Sie irgendwo NEXT_PUBLIC_OPENAI_API_KEY sehen – ein hervorragender Anlass, das zu korrigieren, solange es nur um lokale Entwicklung geht.
9. Visuelles Schema: Wie ChatGPT mit Ihrer Vorlage interagiert
Damit das Bild vollständig wird, hilft ein einfacher Ablauf:
flowchart TD
U[Benutzer in ChatGPT] -->|schreibt eine Anfrage| M[ChatGPT‑Modell]
M -->|ruft ein Tool auf| MCP["Ihr MCP‑Endpoint
app/mcp/route.ts"]
MCP -->|"JSON‑Antwort des MCP (structuredContent, _meta, UI‑Link)"| M
M -->|entscheidet, UI anzuzeigen| WIDGET_URL["Widget‑URL
(/widget oder /)"]
WIDGET_URL -->|iframe| W[Ihr Widget
app/page.tsx]
W -->|liest window.openai.toolOutput
+ widgetState| U
Wichtig ist hier zu erkennen, dass der Initiator fast immer das ChatGPT‑Modell ist – nicht der Browser des Nutzers, wie bei einer klassischen Web‑App. Ihr app/mcp/route.ts und app/widget/page.tsx sind einfach zwei verschiedene „Türen“ in dasselbe Next.js‑Projekt: eine für den „Roboter“ (MCP), die andere fürs UI.
Wenn Sie diese Projektkarte (Widget → MCP‑Schicht → Konfigs) im Kopf behalten und die genannten Stolperfallen bewusst vermeiden, können Sie sich im weiteren Verlauf des Kurses auf Logik und UX Ihrer App konzentrieren – statt „die eine Datei, die alles kaputt macht“ zu suchen.
10. Typische Fehler im Umgang mit der Template‑Struktur
Fehler Nr. 1: Das Widget mit einer gewöhnlichen Website‑Seite verwechseln.
Manchmal sieht ein Entwickler sowohl app/page.tsx als auch app/widget/page.tsx, ändert die „falsche“ Datei und wundert sich, warum es in ChatGPT keine Änderungen gibt. Das Widget ist genau die Seite, die als outputTemplate/iframe für das MCP‑Tool verwendet wird. Ändern Sie eine andere Route, bekommt ChatGPT das gar nicht mit. Prüfen Sie immer das README der Vorlage und schauen Sie, welche URL als Widget angegeben ist.
Fehler Nr. 2: Client‑Code (window, document) in Serverdateien des MCP schreiben.
Die Datei app/mcp/route.ts und alles, was sie importiert, läuft auf dem Server. Jeder Versuch, dort window oder DOM‑APIs zu verwenden, führt zu Runtime‑Abstürzen. Möchten Sie etwas im UI tun, gehört es fast sicher in Dateien unter app/widget oder in andere Client‑Komponenten. Die MCP‑Schicht ist reines Backend: Requests, Datenbanken, externe APIs und das Formen der strukturierten Antwort.
Fehler Nr. 3: assetPrefix und CORS‑Einstellungen ignorieren.
Lokal auf localhost:3000 läuft alles wunderbar, aber geöffnet über einen Tunnel in ChatGPT – sind Styles weg, JS lädt nicht, die Konsole ist voller CORS‑Fehler. Oft liegt die Ursache daran, dass die Konfiguration in next.config.ts oder middleware.ts/proxy.ts die neue öffentliche URL nicht berücksichtigt oder beim Refactoring versehentlich beschädigt wurde. Denken Sie bei Änderungen an diese Dateien daran, dass Ihr Code innerhalb eines iframe auf der Domain von ChatGPT laufen wird – und nicht direkt auf localhost.
Fehler Nr. 4: Geheimnisse nicht in .env aufbewahren, sondern im Code oder in NEXT_PUBLIC_*‑Variablen.
Den OPENAI_API_KEY in const apiKey = 'sk-...' irgendwo in app/widget/page.tsx zu verstecken, ist eine der schlechtesten Ideen: Der Schlüssel landet im JS‑Bundle und bei jedem Nutzer. Fast genauso schlecht ist eine Variable NEXT_PUBLIC_OPENAI_API_KEY, denn das Präfix NEXT_PUBLIC_ garantiert die Sichtbarkeit im Browser. Legen Sie Geheimnisse stets in .env ohne dieses Präfix – und verwenden Sie sie nur auf der Serverseite (MCP‑Server, Backend‑Funktionen).
Fehler Nr. 5: Die Vorlage für „zu klug“ halten und sich nicht trauen, sie anzufassen.
Manche Entwickler behandeln das offizielle Starter‑Template wie etwas Sakrosanktes: „Lieber nicht anfassen, sonst zerstöre ich die Integration.“ Am Ende schreiben sie ihren gesamten Code „daneben“, verkomplizieren die Architektur und treten trotzdem auf dieselben Stolperfallen. In Wahrheit ist die Vorlage nur ein sauber zusammengestelltes Next.js‑Projekt mit ein paar Einstellungen für das Apps SDK. Das Verständnis, dass app/ UI und MCP enthält, und der Rest „nur“ Konfigs sind, befreit: Sie arbeiten mit dem Code wie mit einem gewohnten React/Next‑Projekt – und nicht mit einer magischen Box.
Fehler Nr. 6: Versuchen, alle Probleme „auf Widget‑Ebene“ zu lösen.
Man möchte manchmal alles im UI erledigen: Business‑Logik, Datenbankzugriff, externe API‑Requests. Im Kontext von ChatGPT‑Apps ist das besonders unglücklich: Das Widget lebt in einer sehr harten Sandbox, sieht Ihre Geheimnisse nicht und hängt stark von window.openai ab. Wenn etwas „Großes“ nötig ist – gehört es in die MCP‑Schicht und Backend‑Services, während das Widget eine schlanke Präsentationsschicht bleibt, die strukturierte Daten anzeigt und bei Bedarf Tools triggert.
GO TO FULL VERSION