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 -Sfü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
- CORS & Session: Wir erlauben Cross-Origin-Zugriffe, exposen
mcp-session-idund generieren eine UUID, falls ChatGPT noch keine Session-ID mitsendet. - JSON-RPC: Jede MCP-Nachricht kommt als JSON-RPC 2.0 Request. Wir parsen
method,params,idund antworten entsprechend. - Resource + Tool:
resources/readliefert das HTML-Template (als Text, eingebettet im JSON).tools/listbeschreibt unser Toolgreet– inklusive_meta.openai/outputTemplate, damit ChatGPT weiß, dass die UI dazugehört. - structuredContent: Beim
callTool-Resultat legen wir den Namen unterstructuredContent['name']ab. Genau diese Daten liest das Widget überwindow.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
- Developer Mode aktivieren: Settings → Apps & Connectors → Advanced settings
- Connector anlegen: Settings → Connectors → Create
- Trage deine ngrok-URL mit
/mcpein, z. B.https://purple-otter.ngrok.app/mcp - 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
- PHP kann MCP – Mit wenigen Zeilen JSON-RPC-Logik lässt sich ein kompatibler Endpoint bereitstellen.
- Resource & Tool klar trennen – Das HTML liegt als MCP-Resource vor, das Tool liefert Text +
structuredContent. window.openai.toolOutput– Widgets lesen Daten direkt aus diesem Objekt und reagieren aufopenai:set_globals.- Session-Stabilität –
mcp-session-idist wichtig, um Folge-Requests einer Session zuzuordnen. - 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.