NodeJS: Entwicklung von In-Chat Apps für ChatGPT mit MCP und der OpenAI App SDK

In diesem Tutorial lernst du, wie du eine eigene App für ChatGPT baust. Wir erstellen eine einfache „Hello“-App, die einen Namen von ChatGPT empfängt und diesen in der App anzeigt.

Was wir bauen: Eine App, die auf den Prompt @Hello Begrüsse David in der App reagiert und „Hallo David!“ anzeigt.


Voraussetzungen

  • Node.js (Version 18 oder höher)
  • ngrok (um deinen lokalen Server für ChatGPT erreichbar zu machen)
  • Ein ChatGPT Plus/Pro Account mit aktiviertem Developer Mode

Projektstruktur

Unsere App besteht aus nur 3 Dateien:

hello-app/
├── server.js           # Der MCP-Server
├── public/
│   └── hello.html      # Das UI-Widget
└── package.json        # Abhängigkeiten

Schritt 1: Projekt initialisieren

Erstelle einen neuen Ordner und initialisiere das Projekt:

mkdir hello-app
cd hello-app
npm init -y

Installiere die benötigten Pakete:

npm install @modelcontextprotocol/sdk zod

Füge in der package.json den Typ module hinzu:

{
  "name": "hello-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "zod": "^3.25.23"
  }
}

Schritt 2: Das HTML-Widget erstellen

Erstelle den Ordner public und darin die Datei hello.html:

mkdir public

public/hello.html:

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <title>Hello App</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100px;
      margin: 0;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }
    .greeting {
      font-size: 2rem;
      color: white;
      text-align: center;
      padding: 20px;
    }
  </style>
</head>
<body>
  <div class="greeting" id="output">Warte auf Daten...</div>

  <script>
    const outputEl = document.getElementById("output");

    function render() {
      // Daten von ChatGPT über window.openai.toolOutput abrufen
      const data = window.openai?.toolOutput;
      if (data?.name) {
        outputEl.textContent = `Hallo ${data.name}!`;
      }
    }

    // Initiales Rendern
    render();

    // Neu rendern, wenn ChatGPT neue Daten setzt
    window.addEventListener("openai:set_globals", render, { passive: true });
  </script>
</body>
</html>

Wie funktioniert das?

Der entscheidende Teil ist das Auslesen der Daten:

const data = window.openai?.toolOutput;

ChatGPT stellt im iframe das globale Objekt window.openai bereit. Dein structuredContent vom Server landet in window.openai.toolOutput.

Das Event openai:set_globals wird ausgelöst, wenn ChatGPT neue Daten an das Widget übergibt – so kann dein UI dynamisch reagieren.


Schritt 3: Den MCP-Server erstellen

server.js:

import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

// HTML-Widget laden
const helloHtml = readFileSync("public/hello.html", "utf8");

function createHelloServer() {
  const server = new McpServer({ name: "hello-app", version: "1.0.0" });

  // 1. Widget als Resource registrieren
  server.registerResource(
    "hello-widget",
    "ui://widget/hello.html",
    {},
    async () => ({
      contents: [{
        uri: "ui://widget/hello.html",
        mimeType: "text/html+skybridge",
        text: helloHtml,
      }],
    })
  );

  // 2. Tool registrieren, das ChatGPT aufrufen kann
  server.registerTool(
    "greet",
    {
      title: "Begrüssung",
      description: "Begrüsst eine Person mit Namen",
      inputSchema: {
        name: z.string().describe("Der Name der Person"),
      },
      _meta: {
        "openai/outputTemplate": "ui://widget/hello.html",
      },
    },
    async (args) => {
      return {
        content: [{ type: "text", text: `Begrüsse ${args.name}` }],
        structuredContent: {
          name: args.name,
        },
      };
    }
  );

  return server;
}

// HTTP-Server starten
const port = 8787;

const httpServer = createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  // CORS für Preflight-Requests
  if (req.method === "OPTIONS") {
    res.writeHead(204, {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "POST, GET, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "content-type, mcp-session-id",
      "Access-Control-Expose-Headers": "mcp-session-id",
    });
    res.end();
    return;
  }

  // MCP-Endpunkt
  if (url.pathname === "/mcp") {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");

    const server = createHelloServer();
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: randomUUID,
      enableJsonResponse: true,
    });

    res.on("close", () => {
      transport.close();
      server.close();
    });

    await server.connect(transport);
    await transport.handleRequest(req, res);
    return;
  }

  res.writeHead(404).end("Not Found");
});

httpServer.listen(port, () => {
  console.log(`🚀 Hello App läuft auf http://localhost:${port}/mcp`);
});

Der Datenfluss erklärt

┌─────────────────────────────────────────────────────────────────┐
│  ChatGPT: "@Hello Begrüsse David in der App"                    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  ChatGPT erkennt das Tool "greet" und ruft es auf mit:          │
│  { name: "David" }                                              │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  Server empfängt den Aufruf und gibt zurück:                    │
│  {                                                              │
│    content: [{ type: "text", text: "Begrüsse David" }],         │
│    structuredContent: { name: "David" }                         │
│  }                                                              │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  ChatGPT lädt das Widget (hello.html) und stellt                │
│  structuredContent in window.openai.toolOutput bereit           │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  Widget liest window.openai.toolOutput und zeigt:               │
│  "Hallo David!"                                                 │
└─────────────────────────────────────────────────────────────────┘

Schritt 4: Server starten und mit ChatGPT verbinden

Server starten:

npm start

Mit ngrok öffentlich machen:

ngrok http 8787

Du erhältst eine URL wie https://abc123.ngrok.app.

In ChatGPT verbinden:

  1. Gehe zu Settings → Apps & Connectors → Advanced settings
  2. Aktiviere den Developer Mode
  3. Gehe zu Settings → Connectors → Create
  4. Füge deine ngrok-URL mit /mcp ein: https://abc123.ngrok.app/mcp
  5. Gib der App einen Namen (z.B. „Hello“)

Testen:

Öffne einen neuen Chat, wähle deine App aus und schreibe:

@Hello Begrüsse David in der App

Die App zeigt nun „Hallo David!“ an!


Zusammenfassung

Die wichtigsten Konzepte:

  1. Resource registrieren – Das HTML-Widget, das in ChatGPT angezeigt wird
  2. Tool registrieren – Die Funktion, die ChatGPT aufrufen kann
  3. structuredContent – Die Daten, die an das Widget gesendet werden
  4. window.openai.toolOutput – So liest das Widget die Daten im Browser
  5. openai:set_globals – Event, um auf neue Daten zu reagieren

Mit diesen Grundlagen kannst du beliebige interaktive Apps für ChatGPT bauen!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert