A hackathon project where two LLMs Agents (Claude and Codex) play chess against each other through Metabase. Metabase isn't a passive viewer — it's the game platform: source of state (Postgres tables exposed as cards/models), abstraction (MCP tools the agents read), visualization (auto-refreshing dashboard for spectators #refresh=1), and meta-layer (file-based development for shipping the dashboard itself).
Hackathon page: https://www.metabase.com/blog/metabase-ai-hackathon
Two distinct agent roles. Player agents (Claude / Codex with the chess skill) play the game: WRITE moves through the bridge, READ board state through MCP — never the other way around. A dashboard-developer agent edits the BOARD card and dashboard YAML through Metabase's file-based development (serialization v2 import/export). Three completely separate channels:
┌────────────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ Claude — player │ │ Codex — player │ │ Claude — dashboard │
│ (chess skill) │ │ (chess skill) │ │ developer │
└─┬──────────────┬───┘ └─┬──────────────┬───┘ └─────────┬────────────┘
│ WRITE │ READ │ WRITE │ READ │ EXPORT / IMPORT
│ moves │ board │ moves │ board │ YAML tarball
│ via bridge │ via MCP │ via bridge │ via MCP │ (file-based dev)
▼ ▼ ▼ ▼ ▼
┌────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ chess-bridge :8091 │ │ Metabase EE :3030 │
│ FastAPI │ │ • MCP server ─ read-only (game / move / position)│
│ python-chess │ │ • REST API ─ cards · dashboards · revisions │
└─────────┬──────────┘ │ • serialization v2 — export / import YAML tarball │
│ INSERT └──────────────────────────┬──────────────────────────┘
▼ │ reads SQL from cards
┌────────────────────────────────────────────────────┐
│ Postgres :5437 ─ game · move · position · tableau │
└────────────────────────────────────────────────────┘
┌──────────────────┐ served as image URLs, rendered inline in the BOARD card
│ chess-svgs :8090 │ via Metabase conditional formatting on `?hl` URL suffixes
└──────────────────┘ (used for the last-move highlight overlay)
Invariants (enforced in CLAUDE.md and the chess skill):
- Player agents NEVER read board state from the bridge — turn checks and FEN go through MCP only.
- Player agents NEVER write directly to Postgres — moves go through the bridge only.
- The dashboard-developer agent NEVER edits cards via SQL or the in-app UI ad-hoc — only via file-based dev (
dashboards/scripts/{export,import}.sh→ serialization API). This keeps the dashboard reproducible and committable. - The bridge never decides moves — it only validates and persists. MCP is strictly read-only.
docker compose up -d # postgres, metabase, chess-bridge, chess-svgs
open http://localhost:3030/dashboard/2 # spectator dashboard
curl -s http://localhost:8091/games | jq . # bridge sees gamesThen ask Claude or Codex to play: invoke the chess skill (it auto-triggers on any chess-related request — see CLAUDE.md).
| Service | Port | Purpose |
|---|---|---|
metabase |
3030 | Metabase EE — dashboard, MCP server, serialization API |
postgres |
5437 | App DB (hackathon) — game, move, position, tableau |
chess-bridge |
8091 | FastAPI write-path — POST /games, POST /games/{id}/moves, validates via python-chess |
chess-svgs |
8090 | nginx serving piece SVGs referenced from BOARD card SQL |
README.md this file
CLAUDE.md / AGENTS.md hard rules for agents (symlinked — same file)
MCP.md MCP client config snippets
chess-hackathon-plan.md original plan (historical — see Status section)
chess/
SKILL.md canonical skill (symlinked from .claude / .codex)
README.md bridge + agent layer details
AGENTS.md multi-agent protocol
SCHEMA.md Postgres schema (current state + history)
bridge/main.py FastAPI bridge
agent.py ChessGame class — thin I/O wrapper around python-chess
move, state, turn-watcher CLI wrappers used by the skill
bot_runner.py, self_play.py automation helpers
schema_proposed.sql original additive migration (now applied)
dashboards/ committed Metabase YAMLs + file-based dev tooling
chess_hackathon.yaml collection metadata
chess_hackathon/*.yaml cards & dashboard
scripts/{backup,export,import,revert}.sh
.claude/skills/chess/SKILL.md → chess/SKILL.md (symlink)
.codex/skills/chess/SKILL.md → chess/SKILL.md (symlink)
- Any chess-related action goes through the
chessskill — never improvise REST calls. - Bridge is the only write path (create game, submit move). MCP is the only read path (whose turn, opponent's last move, board FEN). The
chess/turn-watcherscript is the single allowed direct-Postgres exception (background wake-up trigger only — decisions still come from MCP). - LLM is the brain. No minimax, no Stockfish, no piece-square tables, no engine search.
- Output narration in English regardless of conversation language. Every move ships with a one-sentence reason — both in the on-screen narration and in the bridge
-rthought field.
To change the BOARD card, MOVES card, dashboard, or anything else in the Chess Hackathon collection, use the scripts in dashboards/scripts/:
dashboards/scripts/backup.sh # snapshot every entity to dashboards/backups/
dashboards/scripts/export.sh # pull current Metabase state → dashboards/chess_hackathon/
# edit dashboards/chess_hackathon/board.yaml (or via Metabase UI then re-export)
dashboards/scripts/import.sh # push dashboards/chess_hackathon/ → Metabase
dashboards/scripts/revert.sh list card 45 # show revision history
dashboards/scripts/revert.sh revert card 45 85 # restore a previous revision if anything breaksThe scripts are thin wrappers around POST /api/ee/serialization/{export,import} and POST /api/revision/revert. See dashboards/README.md for details and the rationale (esp. why we only re-pack collections/).
