INTEGRITY Dokumentace

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:

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:

1. Povolte ChatGPT Developer Mode

Chcete-li používat ChatGPT Apps (nazývané také konektory), musíte povolit režim pro vývojáře:

  1. Otevřete ChatGPT.
  2. Přejděte na Nastavení > Apps & Connectors > Pokročilá nastavení
  3. 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

  1. Vytvořte nový projekt pro svou aplikaci Chess App:
npm create cloudflare@latest -- my-chess-app
  1. Přejděte do svého projektu:
cd my-chess-app
  1. Nainstalujte požadované závislosti:
npm install agents @modelcontextprotocol/sdk chess.js react react-dom react-chessboard
  1. Nainstalujte vývojové závislosti:
npm install -D @cloudflare/vite-plugin @vitejs/plugin-react vite vite-plugin-singlefile @types/react @types/react-dom

3. Nakonfigurujte svůj projekt

  1. Aktualizujte wrangler.jsonc pro 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"
  1. Vytvořte vite.config.ts pro 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,
	},
});
  1. Aktualizujte package.json scripts:
{
	"scripts": {
		"dev": "vite",
		"build": "vite build",
		"deploy": "vite build && wrangler deploy"
	}
}

4. Vytvořte šachový herní engine

  1. 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

  1. 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

  1. 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>
  1. 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

  1. Vytvořte uživatelské rozhraní React:
npm run build

Tím se vaše aplikace React zkompiluje do jediného souboru HTML v dist adresář.

  1. Deploy to Cloudflare:
npx wrangler deploy

Po nasazení se zobrazí adresa URL vaší aplikace:

https://my-chess-app.YOUR_SUBDOMAIN.workers.dev

8. Připojte se k ChatGPT

Nyní propojte nasazenou aplikaci s ChatGPT:

  1. Otevřete ChatGPT.
  2. Přejděte na Nastavení > Apps & Connectors > Vytvořit
  3. Dejte své aplikaci název, a volitelně popis a ikona.
  4. Zadejte svůj koncový bod MCP: https://my-chess-app.YOUR_SUBDOMAIN.workers.dev/mcp.
  5. Vyberte "No authentication".
  6. Vyberte "Create".

9. Hrajte šachy v ChatGPT

Vyzkoušejte to:

  1. Do konverzace v ChatGPT napište: "Let's play chess".
  2. ChatGPT zavolá playChess nástroj a vykreslí váš interaktivní šachový widget.
  3. Vyberte "Start a new game" pro vytvoření hry.
  4. ID hry sdílejte s přítelem, který se může připojit prostřednictvím vlastní konverzace v ChatGPT.
  5. Tahy provádějte přetažením figurek na desce.
  6. 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:

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:

Agents API

Kompletní referenční dokumentace API pro Agents SDK.

Durable Objects

Přečtěte si o podkladové stavové infrastruktuře.

Model Context Protocol

Specifikace a dokumentace MCP.

OpenAI Apps SDK

Oficiální referenční dokumentace k OpenAI Apps SDK.