Skip to main content
Glama
Kobe-Vandenberghe

Game of Life MCP

Game of Life · MCP

A shared Conway's Game of Life world that an AI agent controls through MCP tools while you watch it evolve live in your browser. One Python process serves everything:

Surface

URL

Browser viewer

http://localhost:8000

Live state WebSocket

ws://localhost:8000/ws

MCP endpoint (Streamable HTTP)

http://localhost:8000/mcp

The server owns the authoritative board (NumPy). Every MCP tool call mutates it and immediately broadcasts the new state to all connected browser tabs.

Codex / Claude Code ──MCP──▶ ┌───────────────────┐
                             │  Python server    │──▶ shared GoL world
Browser UI ◀──WebSocket────  │ (FastAPI+FastMCP) │
                             └───────────────────┘

Quickstart

.\run.ps1

(First run creates the venv and installs dependencies. If PowerShell blocks the script, run it as powershell -ExecutionPolicy Bypass -File run.ps1.)

…or manually:

py -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe -m uvicorn gol_server:app --host 127.0.0.1 --port 8000

Open http://localhost:8000. The server binds to localhost only and has no auth — it's a local toy, don't expose it.

Connect an agent

Claude Code (run in any terminal; the server must be running when the agent starts):

claude mcp add --transport http gol http://localhost:8000/mcp

Codex CLI — add to ~/.codex/config.toml (exact key names vary by Codex version; check its MCP docs):

[mcp_servers.gol]
url = "http://localhost:8000/mcp"

MCP tools

Tool

What it does

get_world_status()

Dimensions, generation, population, live-cell bounding box, dynamics, autorun state

observe_world(x, y, width, height)

ASCII view of a region (# alive, . dead) with coordinate rulers; defaults to the full board, capped at 20 000 cells

create_world(width, height, edge, random_fill)

New board, 8–1024 per side; edge="wrap" (toroidal, default) or "dead"; optional random soup

clear_world()

Kill everything, reset generation to 0

set_cells(alive, dead)

Set individual cells from lists of [x, y] pairs

place_pattern(rle, x, y, clear_rect)

Stamp a standard RLE pattern (e.g. glider bob$2bo$3o!) at a position

advance(generations)

Step the simulation, 1–100 per call, animated in the browser

advance_generations(count, sample_every)

Efficiently commit up to thousands of generations in one call, returning compact per-sample stats (not the full board) plus a run summary

preview_generations(count, sample_every)

Same as advance_generations but doesn't touch the world — test what would happen before committing

set_autorun(enabled, fps)

Continuous simulation on the server, 1–1000 gen/s

Coordinates everywhere: (x, y), 0-indexed, (0,0) top-left, x → right, y ↓ down.

advance_generations / preview_generations

Both return {"samples": [...], "summary": {...}} as a JSON string — never the full board, so responses stay small even over thousands of generations. sample_every controls how often a sample is recorded (always including the final generation); each sample has generation, population, births and deaths (since the previous sample), bbox, a state_hash (matches only a bit-for-bit identical board) and a shape_hash (translation-independent — stays the same while a pattern like a glider moves). The summary reports start/end generation, min/max population, and whether the run went extinct, settled into a static state, or fell into a short repeating cycle.

Two caps apply, both surfaced as a {"error": ...} object in the response (not a thrown error) when exceeded:

  • At most 500 samples per call. Raise sample_every if you hit this — the error tells you the minimum value that fits.

  • count is capped by board size, not a flat number. The real cost of a run is width × height × count, so a 1024×1024 board allows far fewer generations per call than the default 160×100 board — the error reports the exact ceiling for the current board. Call the tool repeatedly for more.

advance_generations commits each sample to the live world as it goes (so the browser animates progress at the sample_every cadence); preview_generations never touches the world or the browser. Both walk the identical simulation code, so a preview and a subsequent advance with the same arguments always agree exactly.

Browser viewer

  • Play / Pause / Step / speed — drives the same server-side simulation the agent uses.

  • Draw / Erase / Pan — left-drag paints (right-drag always erases), wheel zooms, middle-drag pans, Fit re-centers.

  • Activity panel — live feed of every MCP tool call, so you can watch the agent think.

Challenge ideas

  • "Place a Gosper glider gun and report the population after 200 generations."

  • "I've drawn a mess in the middle — stabilize the board into only still lifes."

  • "Make two gliders collide head-on and tell me what survives."

  • "create_world(300, 200, random_fill=0.2), run it until it settles, then find and report the coordinates of every oscillator."

  • "Write my initials using still lifes."

Files

  • gol_server.py — FastAPI app: WebSocket hub, MCP tools, autorun loop

  • gol_world.py — pure simulation: NumPy stepping, RLE parser, ASCII renderer

  • static/ — the browser viewer (vanilla JS + canvas)