PHP: Deine erste In-Chat App für ChatGPT, in PHP programmiert

Dieses Tutorial baut auf derselben Idee wie das JavaScript-Pendant auf: Wir entwickeln eine minimalistische „Hello“-App, die einen Namen von ChatGPT erhält und ihn im Widget anzeigt. Diesmal bestehen Server und Tool-Logik komplett aus PHP.

Was wir bauen: Ein Tool greet, das den Prompt @HelloPHP Begrüsse David in der App entgegennimmt und im Widget „Hallo David!“ rendert.


Voraussetzungen

  • PHP 8.2+ (inkl. php -S für den Development-Server)
  • Composer für das Dependency-Management
  • ngrok oder ein anderes HTTPS-Tunnel-Tool
  • Ein ChatGPT Plus/Pro Account mit aktiviertem Developer Mode

Projektstruktur

hello-app-php/
├── composer.json
├── public/
│   └── hello.html
└── server.php

Alles, was unser Server benötigt (HTML-Template + eine einzige PHP-Datei), passt in diesen kleinen Baum.


Schritt 1: Composer-Projekt initialisieren

mkdir hello-app-php
cd hello-app-php
composer init --no-interaction
composer require ramsey/uuid

ramsey/uuid nutzen wir, um robuste Session-IDs für das MCP-Transportprotokoll zu erzeugen.

Aktualisiere anschließend composer.json (wichtige Ausschnitte):

{
  "name": "example/hello-app-php",
  "description": "Mini MCP Server in PHP",
  "type": "project",
  "require": {
    "php": "^8.2",
    "ramsey/uuid": "^4.7"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

Eine dedizierte src/-Struktur brauchst du für dieses Beispiel nicht zwingend, hilft aber, falls du den Server später modularisieren möchtest.


Schritt 2: Widget erstellen (public/hello.html)

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

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

    function render() {
      const data = window.openai?.toolOutput; // structuredContent vom Server
      if (data?.name) {
        outputEl.textContent = `Hallo ${data.name}!`;
      }
    }

    render();
    window.addEventListener("openai:set_globals", render, { passive: true });
  </script>
</body>
</html>

Wichtig: ChatGPT stellt window.openai.toolOutput bereit. Darüber erhältst du den structuredContent aus deinem Tool. Mit dem Event openai:set_globals renderst du neu, sobald ChatGPT aktualisierte Daten liefert.


Schritt 3: PHP-Server mit MCP-Logik (server.php)

Wir implementieren einen kleinen HTTP-Server, der die MCP-Methoden behandelt (initialize, resources/*, tools/*, callTool). Die JSON-RPC-Kommunikation ist bewusst simpel gehalten, deckt aber den kompletten Happy Path für unsere Hello-App.

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Ramsey\Uuid\Uuid;

const MCP_PATH = '/mcp';

$helloHtml = file_get_contents(__DIR__ . '/public/hello.html');

function sendJson(array $payload, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}

function respondError($id, string $message, int $code = -32603): void
{
    sendJson([
        'jsonrpc' => '2.0',
        'id' => $id,
        'error' => [
            'code' => $code,
            'message' => $message,
        ],
    ]);
}

function respondResult($id, array $result): void
{
    sendJson([
        'jsonrpc' => '2.0',
        'id' => $id,
        'result' => $result,
    ]);
}

function handleMcp(array $body, string $helloHtml): void
{
    $id = $body['id'] ?? null;
    $method = $body['method'] ?? null;
    $params = $body['params'] ?? [];

    switch ($method) {
        case 'initialize':
            respondResult($id, [
                'serverInfo' => [
                    'name' => 'hello-app-php',
                    'version' => '1.0.0',
                ],
                'capabilities' => [
                    'resources' => ['listChanged' => false],
                    'tools' => ['callTools' => true],
                ],
            ]);
            return;

        case 'resources/list':
            respondResult($id, [
                'resources' => [[
                    'uri' => 'ui://widget/hello.html',
                    'name' => 'hello-widget',
                    'description' => 'Hello UI für die PHP-App',
                ]],
            ]);
            return;

        case 'resources/read':
            respondResult($id, [
                'contents' => [[
                    'uri' => 'ui://widget/hello.html',
                    'mimeType' => 'text/html+skybridge',
                    'text' => $helloHtml,
                    '_meta' => [
                        'openai/widgetPrefersBorder' => true,
                    ],
                ]],
            ]);
            return;

        case 'tools/list':
            respondResult($id, [
                'tools' => [[
                    'name' => 'greet',
                    'title' => 'Begrüssung',
                    'description' => 'Begrüsst eine Person mit Namen',
                    'inputSchema' => [
                        'type' => 'object',
                        'properties' => [
                            'name' => [
                                'type' => 'string',
                                'description' => 'Der Name der Person',
                            ],
                        ],
                        'required' => ['name'],
                    ],
                    '_meta' => [
                        'openai/outputTemplate' => 'ui://widget/hello.html',
                    ],
                ]],
            ]);
            return;

        case 'callTool':
            $arguments = $params['arguments'] ?? [];
            $name = trim($arguments['name'] ?? 'Freund');

            respondResult($id, [
                'content' => [[
                    'type' => 'text',
                    'text' => "Begrüsse {$name}",
                ]],
                'structuredContent' => [
                    'name' => $name,
                ],
            ]);
            return;

        default:
            respondError($id, "Nicht unterstützte Methode: {$method}", -32601);
            return;
    }
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: content-type, mcp-session-id');
header('Access-Control-Expose-Headers: mcp-session-id');

if ($method === 'OPTIONS') {
    http_response_code(204);
    exit;
}

if ($path === '/') {
    echo '🚀 Hello App PHP MCP Server';
    return;
}

if ($path === MCP_PATH && $method === 'POST') {
    $sessionId = $_SERVER['HTTP_MCP_SESSION_ID'] ?? Uuid::uuid4()->toString();
    header('mcp-session-id: ' . $sessionId);

    $raw = file_get_contents('php://input');
    $body = json_decode($raw ?? '', true);

    if (!is_array($body)) {
        respondError(null, 'Ungültige JSON-RPC-Nachricht', -32700);
        return;
    }

    handleMcp($body, $helloHtml);
    return;
}

http_response_code(404);
echo 'Not Found';

Was hier passiert

  1. CORS & Session: Wir erlauben Cross-Origin-Zugriffe, exposen mcp-session-id und generieren eine UUID, falls ChatGPT noch keine Session-ID mitsendet.
  2. JSON-RPC: Jede MCP-Nachricht kommt als JSON-RPC 2.0 Request. Wir parsen method, params, id und antworten entsprechend.
  3. Resource + Tool: resources/read liefert das HTML-Template (als Text, eingebettet im JSON). tools/list beschreibt unser Tool greet – inklusive _meta.openai/outputTemplate, damit ChatGPT weiß, dass die UI dazugehört.
  4. structuredContent: Beim callTool-Resultat legen wir den Namen unter structuredContent['name'] ab. Genau diese Daten liest das Widget über window.openai.toolOutput.

Datenfluss

┌─────────────────────────────────────────────────────────────┐
│ ChatGPT-User: "@HelloPHP Begrüsse David in der App"         │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ ChatGPT ruft Tool "greet" mit { name: "David" } auf       │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ PHP-Server antwortet mit                                   │
│ { content: [...], structuredContent: { name: "David" } }  │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ ChatGPT rendert hello.html                                 │
│ und setzt window.openai.toolOutput = { name: "David" }     │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ Widget liest toolOutput, zeigt "Hallo David!"              │
└─────────────────────────────────────────────────────────────┘

Schritt 4: Server starten & verbinden

Server lokal starten

php -S 0.0.0.0:8787 server.php

Über ngrok öffentlich machen

ngrok http 8787

Merke dir die HTTPS-URL, z. B. https://purple-otter.ngrok.app.

In ChatGPT einbinden

  1. Developer Mode aktivieren: Settings → Apps & Connectors → Advanced settings
  2. Connector anlegen: Settings → Connectors → Create
  3. Trage deine ngrok-URL mit /mcp ein, z. B. https://purple-otter.ngrok.app/mcp
  4. Benenne die App (z. B. „HelloPHP“) und speichere

Testen

In einem neuen Chat:

@HelloPHP Begrüsse David in der App

Dein Tool wird aufgerufen, ChatGPT lädt das Widget – und du siehst „Hallo David!“.


Zusammenfassung

  1. PHP kann MCP – Mit wenigen Zeilen JSON-RPC-Logik lässt sich ein kompatibler Endpoint bereitstellen.
  2. Resource & Tool klar trennen – Das HTML liegt als MCP-Resource vor, das Tool liefert Text + structuredContent.
  3. window.openai.toolOutput – Widgets lesen Daten direkt aus diesem Objekt und reagieren auf openai:set_globals.
  4. Session-Stabilitätmcp-session-id ist wichtig, um Folge-Requests einer Session zuzuordnen.
  5. ngrok + /mcp – So stellst du deinen lokalen Server ChatGPT zur Verfügung.

Mit diesem Fundament kannst du die PHP-Version jederzeit erweitern: zusätzliche Tools, dynamische Datenbanken oder eigene Styling-Systeme – alles möglich, solange du die MCP-Kontrakte (Resources, Tools, structuredContent) einhältst.

Schreibe einen Kommentar

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