← Cloudflare Workers / workers / demos
Vytvořte interaktivní aplikaci ChatGPT
Nasaďte svou první ChatGPT App
Tento průvodce vám ukáže, jak vytvořit a nasadit interaktivní aplikaci ChatGPT App na Cloudflare Workers, která dokáže:
- Vykreslujte bohaté interaktivní widgety uživatelského rozhraní přímo v konverzacích ChatGPT
- Udržujte stav v reálném čase pro více uživatelů pomocí Durable Objects
- Povolte obousměrnou komunikaci mezi vaší aplikací a ChatGPT
- Vytvářejte zážitky pro více hráčů, které běží zcela uvnitř ChatGPT
Vytvoříte multiplayerovou šachovou hru v reálném čase, která tyto možnosti demonstruje. Hráči mohou zakládat hry nebo se k nim připojovat, tahat na interaktivní šachovnici a dokonce se ptát ChatGPT na strategické rady, aniž by museli konverzaci opustit.
Vaše aplikace ChatGPT App bude používat Model Context Protocol (MCP) abyste vystavili nástroje a UI prostředky, které může ChatGPT vyvolat vaším jménem.
Úplný kód tohoto příkladu najdete zde ↗.
Předpoklady
Než začnete, budete potřebovat:
- A účet Cloudflare ↗
- Node.js ↗ nainstalovaný (v18 nebo novější)
- A Účet ChatGPT Plus nebo Team ↗ se zapnutým vývojářským režimem
- Základní znalost React a TypeScript
1. Povolte ChatGPT Developer Mode
Chcete-li používat ChatGPT Apps (nazývané také konektory), musíte povolit režim pro vývojáře:
- Otevřete ChatGPT ↗.
- Přejděte na Nastavení > Apps & Connectors > Pokročilá nastavení
- Přepnout Režim pro vývojáře: ZAPNUTO
Jakmile to povolíte, budete moci během vývoje a testování instalovat vlastní aplikace.
2. Vytvořte projekt své aplikace ChatGPT
- Vytvořte nový projekt pro svou aplikaci Chess App:
npm create cloudflare@latest -- my-chess-app- Přejděte do svého projektu:
cd my-chess-app- Nainstalujte požadované závislosti:
npm install agents @modelcontextprotocol/sdk chess.js react react-dom react-chessboard- Nainstalujte vývojové závislosti:
npm install -D @cloudflare/vite-plugin @vitejs/plugin-react vite vite-plugin-singlefile @types/react @types/react-dom3. Nakonfigurujte svůj projekt
- Aktualizujte
wrangler.jsoncpro konfiguraci Durable Objects a assetů:
{
"name": "my-chess-app",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "CHESS",
"class_name": "ChessGame",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["ChessGame"],
},
],
"assets": {
"directory": "dist",
"binding": "ASSETS",
},
}name = "my-chess-app"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]
name = "CHESS"
class_name = "ChessGame"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "ChessGame" ]
[assets]
directory = "dist"
binding = "ASSETS"- Vytvořte
vite.config.tspro sestavení vašeho React UI:
import { cloudflare } from "@cloudflare/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [react(), cloudflare(), viteSingleFile()],
build: {
minify: false,
},
});- Aktualizujte
package.jsonscripts:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"deploy": "vite build && wrangler deploy"
}
}4. Vytvořte šachový herní engine
- Vytvořte herní logiku pomocí Durable Objects na
src/chess.tsx:
import { Agent, callable, getCurrentAgent } from "agents";
import { Chess } from "chess.js";
type Color = "w" | "b";
type ConnectionState = {
playerId: string;
};
export type State = {
board: string;
players: { w?: string; b?: string };
status: "waiting" | "active" | "mate" | "draw" | "resigned";
winner?: Color;
lastSan?: string;
};
export class ChessGame extends Agent<Env, State> {
initialState: State = {
board: new Chess().fen(),
players: {},
status: "waiting",
};
game = new Chess();
constructor(
ctx: DurableObjectState,
public env: Env,
) {
super(ctx, env);
this.game.load(this.state.board);
}
private colorOf(playerId: string): Color | undefined {
const { players } = this.state;
if (players.w === playerId) return "w";
if (players.b === playerId) return "b";
return undefined;
}
@callable()
join(params: { playerId: string; preferred?: Color | "any" }) {
const { playerId, preferred = "any" } = params;
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Not connected");
connection.setState({ playerId });
const s = this.state;
// Already seated? Return seat
const already = this.colorOf(playerId);
if (already) {
return { ok: true, role: already as Color, state: s };
}
// Choose a seat
const free: Color[] = (["w", "b"] as const).filter((c) => !s.players[c]);
if (free.length === 0) {
return { ok: true, role: "spectator" as const, state: s };
}
let seat: Color = free[0];
if (preferred === "w" && free.includes("w")) seat = "w";
if (preferred === "b" && free.includes("b")) seat = "b";
s.players[seat] = playerId;
s.status = s.players.w && s.players.b ? "active" : "waiting";
this.setState(s);
return { ok: true, role: seat, state: s };
}
@callable()
move(
move: { from: string; to: string; promotion?: string },
expectedFen?: string,
) {
if (this.state.status === "waiting") {
return {
ok: false,
reason: "not-in-game",
fen: this.game.fen(),
status: this.state.status,
};
}
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Not connected");
const { playerId } = connection.state as ConnectionState;
const seat = this.colorOf(playerId);
if (!seat) {
return {
ok: false,
reason: "not-in-game",
fen: this.game.fen(),
status: this.state.status,
};
}
if (seat !== this.game.turn()) {
return {
ok: false,
reason: "not-your-turn",
fen: this.game.fen(),
status: this.state.status,
};
}
// Optimistic sync guard
if (expectedFen && expectedFen !== this.game.fen()) {
return {
ok: false,
reason: "stale",
fen: this.game.fen(),
status: this.state.status,
};
}
const res = this.game.move(move);
if (!res) {
return {
ok: false,
reason: "illegal",
fen: this.game.fen(),
status: this.state.status,
};
}
const fen = this.game.fen();
let status: State["status"] = "active";
if (this.game.isCheckmate()) status = "mate";
else if (this.game.isDraw()) status = "draw";
this.setState({
...this.state,
board: fen,
lastSan: res.san,
status,
winner:
status === "mate" ? (this.game.turn() === "w" ? "b" : "w") : undefined,
});
return { ok: true, fen, san: res.san, status };
}
@callable()
resign() {
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Not connected");
const { playerId } = connection.state as ConnectionState;
const seat = this.colorOf(playerId);
if (!seat) return { ok: false, reason: "not-in-game", state: this.state };
const winner = seat === "w" ? "b" : "w";
this.setState({ ...this.state, status: "resigned", winner });
return { ok: true, state: this.state };
}
}5. Vytvořte server MCP a prostředek UI
- Vytvořte svůj hlavní worker na
src/index.ts:
import { createMcpHandler } from "agents/mcp";
import { routeAgentRequest } from "agents";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { env } from "cloudflare:workers";
const getWidgetHtml = async (host: string) => {
let html = await (await env.ASSETS.fetch("http://localhost/")).text();
html = html.replace(
"<!--RUNTIME_CONFIG-->",
`<script>window.HOST = \`${host}\`;</script>`,
);
return html;
};
function createServer() {
const server = new McpServer({ name: "Chess", version: "v1.0.0" });
// Register a UI resource that ChatGPT can render
server.registerResource(
"chess",
"ui://widget/index.html",
{},
async (_uri, extra) => {
return {
contents: [
{
uri: "ui://widget/index.html",
mimeType: "text/html+skybridge",
text: await getWidgetHtml(
extra.requestInfo?.headers.host as string,
),
},
],
};
},
);
// Register a tool that ChatGPT can call to render the UI
server.registerTool(
"playChess",
{
title: "Renders a chess game menu, ready to start or join a game.",
annotations: { readOnlyHint: true },
_meta: {
"openai/outputTemplate": "ui://widget/index.html",
"openai/toolInvocation/invoking": "Opening chess widget",
"openai/toolInvocation/invoked": "Chess widget opened",
},
},
async (_, _extra) => {
return {
content: [
{ type: "text", text: "Successfully rendered chess game menu" },
],
};
},
);
return server;
}
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(req.url);
if (url.pathname.startsWith("/mcp")) {
// Create a new server instance per request
const server = createServer();
return createMcpHandler(server)(req, env, ctx);
}
return (
(await routeAgentRequest(req, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;
export { ChessGame } from "./chess";6. Sestavte uživatelské rozhraní v Reactu
- Vytvořte vstupní bod HTML na
index.html:
<!doctype html>
<html>
<head>
<!--RUNTIME_CONFIG-->
</head>
<body>
<div id="root" style="font-family: verdana"></div>
<script type="module" src="/src/app.tsx"></script>
</body>
</html>- Vytvořte aplikaci React na
src/app.tsx:
import { useEffect, useRef, useState } from "react";
import { useAgent } from "agents/react";
import { createRoot } from "react-dom/client";
import { Chess, type Square } from "chess.js";
import { Chessboard, type PieceDropHandlerArgs } from "react-chessboard";
import type { State as ServerState } from "./chess";
function usePlayerId() {
const [pid] = useState(() => {
const existing = localStorage.getItem("playerId");
if (existing) return existing;
const id = crypto.randomUUID();
localStorage.setItem("playerId", id);
return id;
});
return pid;
}
function App() {
const playerId = usePlayerId();
const [gameId, setGameId] = useState<string | null>(null);
const [gameIdInput, setGameIdInput] = useState("");
const [menuError, setMenuError] = useState<string | null>(null);
const gameRef = useRef(new Chess());
const [fen, setFen] = useState(gameRef.current.fen());
const [myColor, setMyColor] = useState<"w" | "b" | "spectator">("spectator");
const [pending, setPending] = useState(false);
const [serverState, setServerState] = useState<ServerState | null>(null);
const [joined, setJoined] = useState(false);
const host = window.HOST ?? "http://localhost:5173/";
const { stub } = useAgent<ServerState>({
host,
name: gameId ?? "__lobby__",
agent: "chess",
onStateUpdate: (s) => {
if (!gameId) return;
gameRef.current.load(s.board);
setFen(s.board);
setServerState(s);
},
});
useEffect(() => {
if (!gameId || joined) return;
(async () => {
try {
const res = await stub.join({ playerId, preferred: "any" });
if (!res?.ok) return;
setMyColor(res.role);
gameRef.current.load(res.state.board);
setFen(res.state.board);
setServerState(res.state);
setJoined(true);
} catch (error) {
console.error("Failed to join game", error);
}
})();
}, [playerId, gameId, stub, joined]);
async function handleStartNewGame() {
const newId = crypto.randomUUID();
setGameId(newId);
setGameIdInput(newId);
setMenuError(null);
setJoined(false);
}
async function handleJoinGame() {
const trimmed = gameIdInput.trim();
if (!trimmed) {
setMenuError("Enter a game ID to join.");
return;
}
setGameId(trimmed);
setMenuError(null);
setJoined(false);
}
const handleHelpClick = () => {
window.openai?.sendFollowUpMessage?.({
prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice.`,
});
};
function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) {
if (!gameId || !sourceSquare || !targetSquare || pending) return false;
const game = gameRef.current;
if (myColor === "spectator" || game.turn() !== myColor) return false;
const piece = game.get(sourceSquare as Square);
if (!piece || piece.color !== myColor) return false;
const prevFen = game.fen();
try {
const local = game.move({
from: sourceSquare,
to: targetSquare,
promotion: "q",
});
if (!local) return false;
} catch {
return false;
}
const nextFen = game.fen();
setFen(nextFen);
setPending(true);
stub
.move({ from: sourceSquare, to: targetSquare, promotion: "q" }, prevFen)
.then((r) => {
if (!r.ok) {
game.load(r.fen);
setFen(r.fen);
}
})
.finally(() => setPending(false));
return true;
}
return (
<div style={{ padding: "20px", background: "#f8fafc", minHeight: "100vh" }}>
{!gameId ? (
<div
style={{
maxWidth: "420px",
margin: "0 auto",
background: "#fff",
borderRadius: "16px",
padding: "24px",
}}
>
<h1>Ready to play?</h1>
<p>Start a new match or join an existing game.</p>
<button
onClick={handleStartNewGame}
style={{
padding: "12px",
background: "#2563eb",
color: "#fff",
border: "none",
borderRadius: "8px",
cursor: "pointer",
width: "100%",
}}
>
Start a new game
</button>
<div style={{ marginTop: "16px" }}>
<input
placeholder="Paste a game ID"
value={gameIdInput}
onChange={(e) => setGameIdInput(e.target.value)}
style={{
width: "100%",
padding: "10px",
borderRadius: "8px",
border: "1px solid #ccc",
}}
/>
<button
onClick={handleJoinGame}
style={{
marginTop: "8px",
padding: "10px",
background: "#0f172a",
color: "#fff",
border: "none",
borderRadius: "8px",
cursor: "pointer",
width: "100%",
}}
>
Join
</button>
{menuError && (
<p style={{ color: "red", fontSize: "0.85rem" }}>{menuError}</p>
)}
</div>
</div>
) : (
<div style={{ maxWidth: "600px", margin: "0 auto" }}>
<div
style={{
background: "#fff",
padding: "16px",
borderRadius: "16px",
marginBottom: "16px",
}}
>
<h2>Game {gameId}</h2>
<p>Status: {serverState?.status}</p>
<button
onClick={handleHelpClick}
style={{
padding: "10px",
background: "#2563eb",
color: "#fff",
border: "none",
borderRadius: "8px",
cursor: "pointer",
}}
>
Ask for help
</button>
</div>
<div
style={{
background: "#fff",
padding: "16px",
borderRadius: "16px",
}}
>
<Chessboard
position={fen}
onPieceDrop={onPieceDrop}
boardOrientation={myColor === "b" ? "black" : "white"}
/>
</div>
</div>
)}
</div>
);
}
const root = createRoot(document.getElementById("root")!);
root.render(<App />);7. Sestavte a nasaďte
- Vytvořte uživatelské rozhraní React:
npm run buildTím se vaše aplikace React zkompiluje do jediného souboru HTML v dist adresář.
- Deploy to Cloudflare:
npx wrangler deployPo nasazení se zobrazí adresa URL vaší aplikace:
https://my-chess-app.YOUR_SUBDOMAIN.workers.dev8. Připojte se k ChatGPT
Nyní propojte nasazenou aplikaci s ChatGPT:
- Otevřete ChatGPT ↗.
- Přejděte na Nastavení > Apps & Connectors > Vytvořit
- Dejte své aplikaci název, a volitelně popis a ikona.
- Zadejte svůj koncový bod MCP:
https://my-chess-app.YOUR_SUBDOMAIN.workers.dev/mcp. - Vyberte "No authentication".
- Vyberte "Create".
9. Hrajte šachy v ChatGPT
Vyzkoušejte to:
- Do konverzace v ChatGPT napište: "Let's play chess".
- ChatGPT zavolá
playChessnástroj a vykreslí váš interaktivní šachový widget. - Vyberte "Start a new game" pro vytvoření hry.
- ID hry sdílejte s přítelem, který se může připojit prostřednictvím vlastní konverzace v ChatGPT.
- Tahy provádějte přetažením figurek na desce.
- Vyberte "Ask for help" abyste získali strategické rady od ChatGPT
Klíčové koncepty
MCP Server
Server Model Context Protocol (MCP) definuje nástroje a zdroje, ke kterým má ChatGPT přístup. Upozorňujeme, že pro každý požadavek vytváříme novou instanci serveru, abychom zabránili úniku odpovědí mezi klienty:
function createServer() {
const server = new McpServer({ name: "Chess", version: "v1.0.0" });
// Register a UI resource that ChatGPT can render
server.registerResource(
"chess",
"ui://widget/index.html",
{},
async (_uri, extra) => {
return {
contents: [
{
uri: "ui://widget/index.html",
mimeType: "text/html+skybridge",
text: await getWidgetHtml(
extra.requestInfo?.headers.host as string,
),
},
],
};
},
);
// Register a tool that ChatGPT can call to render the UI
server.registerTool(
"playChess",
{
title: "Renders a chess game menu, ready to start or join a game.",
annotations: { readOnlyHint: true },
_meta: {
"openai/outputTemplate": "ui://widget/index.html",
"openai/toolInvocation/invoking": "Opening chess widget",
"openai/toolInvocation/invoked": "Chess widget opened",
},
},
async (_, _extra) => {
return {
content: [
{ type: "text", text: "Successfully rendered chess game menu" },
],
};
},
);
return server;
}Herní engine s Agents
ChessGame třída rozšiřuje Agent pro vytvoření stavového herního enginu:
export class ChessGame extends Agent<Env, State> {
initialState: State = {
board: new Chess().fen(),
players: {},
status: "waiting"
};
game = new Chess();
constructor(
ctx: DurableObjectState,
public env: Env
) {
super(ctx, env);
this.game.load(this.state.board);
}Každá hra získá vlastní instanci Agent, což umožňuje:
- Izolovaný stav na hru
- Synchronizace v reálném čase napříč hráči
- Perzistentní úložiště která přežije restarty Workeru
Volatelné metody
Použijte @callable() dekorátor k odhalení metod, které mohou klienti volat:
@callable()
join(params: { playerId: string; preferred?: Color | "any" }) {
const { playerId, preferred = "any" } = params;
const { connection } = getCurrentAgent();
if (!connection) throw new Error("Not connected");
connection.setState({ playerId });
const s = this.state;
// Already seated? Return seat
const already = this.colorOf(playerId);
if (already) {
return { ok: true, role: already as Color, state: s };
}
// Choose a seat
const free: Color[] = (["w", "b"] as const).filter((c) => !s.players[c]);
if (free.length === 0) {
return { ok: true, role: "spectator" as const, state: s };
}
let seat: Color = free[0];
if (preferred === "w" && free.includes("w")) seat = "w";
if (preferred === "b" && free.includes("b")) seat = "b";
s.players[seat] = playerId;
s.status = s.players.w && s.players.b ? "active" : "waiting";
this.setState(s);
return { ok: true, role: seat, state: s };
}Integrace React
useAgent hook propojí vaši React aplikaci s Durable Object:
const { stub } = useAgent<ServerState>({
host,
name: gameId ?? "__lobby__",
agent: "chess",
onStateUpdate: (s) => {
gameRef.current.load(s.board);
setFen(s.board);
setServerState(s);
},
});Volání metod agenta:
const res = await stub.join({ playerId, preferred: "any" });
await stub.move({ from: "e2", to: "e4" });Obousměrná komunikace
Vaše aplikace může odesílat zprávy do ChatGPT:
const handleHelpClick = () => {
window.openai?.sendFollowUpMessage?.({
prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice as there are no tools for you to use.`,
});
};Tím se vytvoří nová zpráva v konverzaci ChatGPT s kontextem o aktuálním stavu hry.
Další kroky
Nyní, když máte funkční aplikaci ChatGPT App, můžete:
- Přidejte další nástroje: zpřístupněte další funkce a uživatelská rozhraní prostřednictvím nástrojů a zdrojů MCP.
- Vylepšete uživatelské rozhraní: vytvářejte propracovanější rozhraní pomocí React.