Build an Interactive ChatGPT App·续
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 />);
Note
This is a simplified version of the UI. For the complete implementation with player slots, better styling, and game state management, check out the [full example on GitHub ↗︎](https://github.com/cloudflare/agents/tree/main/openai-sdk/chess-app/src/app.tsx).
## 7. Build and deploy
1. Build your React UI:
```sh
npm run build
This compiles your React app into a single HTML file in the dist directory.
- Deploy to Cloudflare:
npx wrangler deploy
After deployment, you will see your app URL:
https://my-chess-app.YOUR_SUBDOMAIN.workers.dev
8. Connect to ChatGPT
Now connect your deployed app to ChatGPT:
- Open ChatGPT ↗︎.
- Go to Settings > Apps & Connectors > Create
- Give your app a name, and optionally a description and icon.
- Enter your MCP endpoint:
https://my-chess-app.YOUR_SUBDOMAIN.workers.dev/mcp. - Select "No authentication".
- Select "Create".
9. Play chess in ChatGPT
Try it out:
- In your ChatGPT conversation, type: "Let's play chess".
- ChatGPT will call the
playChesstool and render your interactive chess widget. - Select "Start a new game" to create a game.
- Share the game ID with a friend who can join via their own ChatGPT conversation.
- Make moves by dragging pieces on the board.
- Select "Ask for help" to get strategic advice from ChatGPT
Note
You might need to manually select the connector in the prompt box the first time you use it. Select "+" > "More" > [App name].
Key concepts
MCP Server
The Model Context Protocol (MCP) server defines tools and resources that ChatGPT can access. Note that we create a new server instance per request to prevent cross-client response leakage:
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;
}
Game Engine with Agents
The ChessGame class extends Agent to create a stateful game engine:
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);
}
Each game gets its own Agent instance, enabling:
- Isolated state per game
- Real-time synchronization across players
- Persistent storage that survives worker restarts
Callable methods
Use the @callable() decorator to expose methods that clients can invoke:
@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 };
}
React integration
The useAgent hook connects your React app to the Durable Object:
const { stub } = useAgent<ServerState>({
host,
name: gameId ?? "__lobby__",
agent: "chess",
onStateUpdate: (s) => {
gameRef.current.load(s.board);
setFen(s.board);
setServerState(s);
},
});
Call methods on the agent:
const res = await stub.join({ playerId, preferred: "any" });
await stub.move({ from: "e2", to: "e4" });
Bidirectional communication
Your app can send messages to 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.`,
});
};
This creates a new message in the ChatGPT conversation with context about the current game state.
Next steps
Now that you have a working ChatGPT App, you can:
- Add more tools: Expose additional capabilities and UIs through MCP tools and resources.
- Enhance the UI: Build more sophisticated interfaces with React.
Related resources
Agents API
Complete API reference for the Agents SDK.
Durable Objects
Learn about the underlying stateful infrastructure.
Model Context Protocol
MCP specification and documentation.
OpenAI Apps SDK
Official OpenAI Apps SDK reference.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/demos/chatgpt-app/#page","headline":"Build an Interactive ChatGPT App","description":"Build and deploy an interactive ChatGPT App on Cloudflare Workers with real-time multiplayer state using MCP.","url":"https://developers.cloudflare.com/workers/demos/chatgpt-app/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/demos/chatgpt-app/og.png?v=0c7d8183ab335947","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Develop and test your Workers locally. title: Local development image: https://developers.cloudflare.com/workers/local-development/og.png?v=f54ca19c6e1d7c99
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.
Docs