Skip to main content
Glama
README.md
# pico-8-mcp

An [MCP](https://modelcontextprotocol.io) server that lets an AI assistant **build, test and balance PICO-8 games** —
not just count tokens. Edit a `.p8` cart, validate it, **simulate a whole 25-minute game headless in about a minute**,
author sprites / sound effects / music from readable notation, and launch the real PICO-8 to take screenshots.

Works with Claude Code, Claude Desktop, Codex, Cursor, or any other MCP client.

<p align="center">
  <img src="docs/starvivor-sector3.png" width="480" alt="STARVIVOR, a 25-minute R-Type x Vampire Survivors shooter built and balanced entirely through this server">
  <br><em>STARVIVOR — a 25-minute shooter written, crash-tested and balanced with these tools (the boss fights were tuned from <code>simulate_cart</code> telemetry, not by playing it 30 times).</em>
</p>

> **Credits.** This is a fork of [EBonura/pico8-mcp-server](https://github.com/EBonura/pico8-mcp-server), which
> provided the original server and the cart analysis tools. Token counting, linting, minification and cart
> parsing come from [shrinko8](https://github.com/thisismypassport/shrinko8) by thisismypassport. PICO-8 is made
> by [Lexaloffle](https://www.lexaloffle.com/pico-8.php). This fork adds the headless simulator, window control,
> data-section authoring and the workflow skill.

## Why

Writing PICO-8 games with an LLM hits the same walls every time: you can't see the game, you can't play it for
20 minutes to check the balance, lint drowns you in false positives about globals, and the sprite / sfx / music
sections are raw hex nobody should type by hand. The tools here remove each of those:

| Problem | Tool |
|---|---|
| "Does it crash 12 minutes in?" / "Is sector 4 too hard?" | `simulate_cart` runs the game loop headless with telemetry |
| "Does this helper function work?" | `run_headless` runs any Lua against the cart and returns `printh` output |
| "What does it look like?" | `run_cart` + `capture_game` return real screenshots; `send_keys` plays it |
| "Draw me a sprite / write a jingle" | `set_sprite`, `set_sfx` (`"c4:2 e4:2 g4:4"`), `set_music`, `render_gfx` |
| 300 lint warnings about globals | `validate_cart` hides PICO-8-style global noise by default |

## Install

Requirements: [PICO-8](https://www.lexaloffle.com/pico-8.php) (any recent 0.2.x), Python 3.11+, [uv](https://github.com/astral-sh/uv).

```bash
git clone --recurse-submodules https://github.com/nutshot2000/pico-8-mcp.git
cd pico-8-mcp
uv sync
```

PICO-8 is auto-detected in the usual install locations on Windows, macOS and Linux, or set `PICO8_EXE` to the
executable path. The window tools (`run_cart`, `send_keys`, `capture_game`) are Windows-only for now; everything
else, including headless simulation, is cross-platform.

### Claude Code

```bash
claude mcp add pico8 --scope user -- uv --directory /path/to/pico-8-mcp run server.py
```

### Claude Desktop / other clients

```json
{
  "mcpServers": {
    "pico8": {
      "command": "uv",
      "args": ["--directory", "/path/to/pico-8-mcp", "run", "server.py"]
    }
  }
}
```

### Optional: the workflow skill

`skills/pico8-dev/SKILL.md` teaches Claude Code the edit → validate → simulate → look loop and the PICO-8
gotchas that bite LLM-written carts (fixed-point overflow, token budget, uninitialised globals). Copy it to
`~/.claude/skills/pico8-dev/SKILL.md` and it loads automatically whenever PICO-8 comes up.

## The workflow

1. Edit the `__lua__` section of the `.p8` as plain text.
2. `validate_cart` — tokens (8192 max), compressed size, syntax, meaningful lint.
3. `simulate_cart` — run it. Catch runtime errors (reported with cart line numbers) and read your own telemetry:
   levels per minute, kills, enemies on screen, boss HP, hits taken. Use `patches` to inject a god mode or an
   autopilot so the game plays itself; `stop_when` to end on win/death.
4. `set_sprite` / `set_sfx` / `set_music`, then `render_gfx` to check the art.
5. `run_cart` + `capture_game` when you actually need to see it.

Example `simulate_cart` call:

```json
{
  "cart_path": "game.p8",
  "seconds": 1500,
  "setup_lua": "newgame() st=\"play\"",
  "log_every": 60,
  "log_lua": "\"lvl=\"..p.lvl..\" hits=\"..hits..\" enemies=\"..#e..\" boss=\"..(boss and boss.hp or 0)",
  "stop_when": "st==\"win\" or st==\"over\"",
  "patches": [
    {"old": "function hurt_p()\n if p.inv>0 then return end\n",
     "new": "function hurt_p()\n if p.inv>0 then return end\n hits+=1 p.inv=60 do return end\n"}
  ]
}
```

returns

```
[1:00] lvl=3 hits=2 enemies=4 boss=0
[2:00] lvl=5 hits=4 enemies=3 boss=0
...
[24:39] STOP lvl=31 hits=33 enemies=0 boss=0
```

## Tools

### Analysis (from shrinko8)
- **validate_cart** `(cart_path, lint="default"|"all"|"none")`
- **count_tokens**, **analyze_cart**, **search_code**, **compare_carts**, **list_carts**, **minify_cart**
- **read_cart** `(cart_path, section)` — code with PICO-8 glyphs intact, plus a summary of the sprites / sfx / music defined

### Headless execution (`pico8 -x`)
- **simulate_cart** `(cart_path, seconds, setup_lua, log_every, log_lua, stop_when, patches, call_draw, timeout)`
- **run_headless** `(cart_path, driver_lua, timeout)`

### Window control (Windows)
- **run_cart** `(cart_path, width=1024, height=1024, restart=true)` — `restart=true` closes any PICO-8 already
  running; pass `false` when a human may be playing in their own window
- **send_keys** `(keys)` — `x z c v up down left right enter esc p space r f6`, `wait:MS`, `hold:KEY:MS`
- **capture_game** `(keys?, delay_ms, count, interval_ms, max_size)` — PNG screenshots of the game area
- **stop_cart**

### Data authoring
- **set_sprite** `(cart_path, index, rows, overwrite=false)` — rows of hex digits; 8×8 or larger blocks.
  The sheet is a 16-wide grid of 8×8 cells, so a 16×16 sprite at `index` also occupies `index+1`, `index+16`
  and `index+17` and is drawn with `spr(index, x, y, 2, 2)`. Place 16×16 sprites at 0, 2, 4 … and 32, 34 …
  — never at consecutive indices. The tool refuses to write a multi-cell block over cells that already contain
  pixels (the error explains the stride); `overwrite=true` forces it, e.g. when redrawing an existing sprite.
  The result includes the covered `cells` and the matching `spr()` call.
- **render_gfx** `(cart_path, sprites="0-15", scale, size=8)` — each index is one 8×8 cell by default, so a
  16×16 sprite appears as four labelled quarters; pass `size=16` and list the top-left indices to see it whole
- **set_sfx** `(cart_path, index, notes, speed, wave, volume, effect, loop_start, loop_end)` —
  `note:len:wave:vol:fx` tokens, `r` = rest, a4 = 440 Hz, range c2..d#7
- **set_music** `(cart_path, pattern, channels[4], loop_start, loop_end, stop)`

## PICO-8 facts the tools rely on

- Numbers are 16.16 fixed point: **anything above 32767 wraps negative**. A frame counter overflows at 18:12
  and `for i=1,36000` runs zero times. Track time in seconds; keep scores small.
- Code budget: 8192 tokens, 65535 chars, 15616 compressed bytes.
- `__gfx__` rows are 128 hex chars, `__sfx__` rows 168 chars (`00 speed loop_start loop_end` + 32 × `pitch wave vol fx`),
  `__music__` rows `flags ch0ch1ch2ch3` with `41..44` meaning a muted channel.
- `pico8 -x cart.p8` runs a cart headless; `printh` goes to stdout. On a runtime error it prints the message and
  hangs, and if `_update`/`_draw` exist it starts the game loop — the server handles both.
- The screen is always 128×128; `-width`/`-height` only scale the window.

## Development

```bash
uv run python test_p8tools.py            # runs against examples/demo.p8
uv run python test_p8tools.py my.p8      # or your own cart
uv run python test_tools.py              # the analysis-tool smoke tests take the same optional cart argument
```

`server.py` registers the MCP tools; `p8tools.py` holds the simulator, window control and data authoring;
`shrinko8/` is the analysis submodule. Contributions welcome — a macOS/Linux capture backend and a `set_map`
tool are the obvious next additions.

## License

MIT — see [LICENSE](LICENSE). shrinko8 is MIT licensed by its author; the original server is © EBonura.