PluginWorld
Mc

mcp-tictactoe

MCP

Enables ChatGPT and Claude users to play a turn-based game through an interactive board widget, with the server enforcing rules, validating moves, and tracking game state.

@cjayasur · MIT License · updated today

SECURITY

B

SCORE

60

STARS

▲ 0

PLUG IN

git clone https://github.com/cjayasur/mcp-tictactoe.git

See the README to configure this MCP server

README

Tic-tac-toe against the model

A complete app that runs inside ChatGPT (and Claude): an MCP server in Python plus a React widget that the chat app draws in a sandboxed iframe. You tap the board, the model answers with its move, and the board updates.

About 800 lines in three files:

File What it does
main.py The MCP server: game rules, four tools, and the one HTML file it hands out via resources/read.
web/src/bridge.js The only code that knows which chat app it's in: window.openai (ChatGPT) or JSON-RPC over postMessage (MCP Apps / Claude).
web/src/App.jsx The game screen in plain React: state, a 3×3 grid, and a loop that hands the turn to the model.

How it works

 ChatGPT app                                       main.py
 +-------------------------------+                 +---------------------------+
 | model ------------------------+-- tools/call -->| tictactoe_start / _move   |
 | iframe: React widget          |<-resources/read-| widget.html (Vite build)  |
 |   tap -- callTool ------------+-- tools/call -->| tictactoe_play / _state   |
 |   "your move" -- sendFollowUpMessage --> model  | GAMES{} = the real board  |
 +-------------------------------+                 +---------------------------+
  1. Tools with _meta. tictactoe_start carries openai/outputTemplate: ui://widget/tictactoe.html, so ChatGPT draws the widget. tictactoe_play and tictactoe_state are openai/visibility: private: only the widget can call them, so only a tap can place an X.
  2. The widget ships as one string. Vite + vite-plugin-singlefile builds React and the CSS into one ~160 KB HTML file, returned by resources/read as text/html+skybridge.
  3. One tool result, three readers. content is text for the model (ending in "Your move: call tictactoe_move…"), structuredContent is JSON for the widget, _meta is for the host.
  4. The loop. A tap calls tictactoe_play, then sendFollowUpMessage("I played X in cell 5 … your move") starts a model turn. The model calls tictactoe_move, and the widget polls tictactoe_state every 1.2 s until the O appears (servers can't push to widgets).
  5. The server owns the board. It checks turns, rejects taken cells and decides wins. The widget reloads the board from the server on every mount, because ChatGPT can re-create a widget and hand it a stale result.

TTT_HOST=chatgpt advertises the Apps SDK metadata; TTT_HOST=claude advertises MCP Apps metadata (_meta.ui.resourceUri, text/html;profile=mcp-app). Same image, same widget.

Run it

cp .env.example .env          # set PUBLIC_HOST
docker compose up -d --build  # ChatGPT server on 127.0.0.1:8094, Claude on :8093

ChatGPT and Claude need a public HTTPS URL: put nginx (see nginx/ttt.conf.example), a tunnel such as ngrok, or any reverse proxy in front.

Widget development without Docker:

cd web && npm install && npm run build   # writes web/dist/index.html
cp dist/index.html ../widget.html
cd .. && pip install -r requirements.txt && uvicorn main:app --port 8000

Add it to ChatGPT

  1. chatgpt.com → Settings → Apps & Connectors → Advanced → turn on Developer mode (paid plans). On a phone, first choose Request Desktop Website in Safari ("aA") or Chrome (⋯).
  2. Create: any name, URL https://<your-host>/ttt/mcp, No authentication, tick "I trust" → Create.
  3. In a chat: @Tic-tac-toe let's play, then tap a square.

ChatGPT caches the tool list and widget when the connector is created: after any change, delete the connector and add it again.

Try the live one: https://api.palomaressoftware.com/ttt/mcp (ChatGPT) or https://api.palomaressoftware.com/ttt-claude/mcp (Claude).

Known differences between hosts

  • ChatGPT plays hands-free: sendFollowUpMessage starts a model turn after each tap.
  • Claude (MCP Apps, tested on iOS): ui/message doesn't start a model turn, so you type "your move" after each tap. The widget asks you to after 6 s.
  • In voice mode the model may announce a move without calling the tool; the tool descriptions forbid it, and only the server's board counts.

Tracing (optional)

tracing.py prints one JSON line per request (method, tool, timing, trace id) and a game event per move. Set SPLUNK_HEC_URL and SPLUNK_HEC_TOKEN to also send them to Splunk. It logs ids and timings only.

License

MIT

SIMILAR PLUGINS