Skip to main content
Glama
README.md
# pixel-mcp

An MCP server that lets an LLM create and edit pixel art reliably. Implements
[SPEC.md](SPEC.md): indexed-color documents (8x8 to 256x256), layers, frames,
symmetry, grid-text editing, server-side PNG/GIF rendering, reference
quantization, and advisory quality linting.

Plain JavaScript with TypeScript-checked JSDoc types (`tsc --checkJs`, strict).
No native dependencies.

## Setup

```sh
pnpm install
pnpm check   # typecheck (tsc, strict, noEmit)
pnpm test    # node:test suite, includes an in-memory MCP end-to-end test
pnpm start   # run the server on stdio
```

Claude Desktop / Claude Code style config:

```json
{
  "mcpServers": {
    "pixel": {
      "command": "node",
      "args": ["c:/path/to/pixel-mcp/src/index.js"]
    }
  }
}
```

## Persistence

Documents survive restarts. Everything is written through to disk on every
successful edit, and loaded on boot:

```
<dataDir>/
  documents/<docId>/
    document.json    # size, palette, layers, frames, symmetry, tile mode, stamps, selections
    cels/<frame>__<layer>.bin   # raw palette-index buffers
    preview.png      # composited frame 0, human-viewable
  refs/<refId>.png   # loaded reference images
```

`dataDir` defaults to the platform data directory (`%LOCALAPPDATA%\pixel-mcp`
on Windows, `~/Library/Application Support/pixel-mcp` on macOS,
`$XDG_DATA_HOME/pixel-mcp` elsewhere). Override with `--data-dir <path>` or
`PIXEL_MCP_DATA_DIR`. Undo/redo stacks and checkpoints are in-memory only,
like a normal editor.

`PIXEL_MCP_EXPORT_SCALE` sets the default `export` scale factor for the project
(built-in 4). Set it to `1` for native-resolution delivery — game textures an
engine samples want no upscaling — so the model needn't pass `scale` on every
export. A per-call `scale` argument still overrides it.

## Tool surface (~60 tools)

Every tool that ingests a PNG accepts exactly one of `png_base64` or `path`.
Paths are read from the server filesystem; relative paths resolve against the
server process's working directory. Base64 remains useful for remote clients.

- **Lifecycle** — `create_document`, `open_document` (PNG import + quantize),
  `export` (png / gif / aseprite_json — writes a real file to a caller path or
  `<dataDir>/exports/` and returns the path; `return_base64` for the bytes;
  default scale from `PIXEL_MCP_EXPORT_SCALE`),
  `split_sprite_sheet` (regular PNG grid from base64 or a server-local path to
  separate documents),
  `pack_sprite_sheet` (document frames to a transparent PNG sheet with layout
  metadata),
  `resize_canvas`, `undo`, `redo`, `describe`, `list_documents`,
  `delete_document`, `set_symmetry`, `set_tile_mode`, `set_active_layer`,
  `set_active_frame`, `checkpoint`
- **Palette** — `set_palette_color`, `add_palette_color`,
  `remove_palette_color` (remaps pixels), `swap_palette_indices`,
  `generate_ramp` (cool shadows / warm highlights), `replace_color`,
  `mix_palette_colors` (blend two entries into a third), `adjust_palette_color`
  (HSL nudge in place or as a new variant), `extract_palette` (median-cut a PNG
  into hex colors + a swatch, no document — feeds `create_document`)
- **Drawing** — `set_pixels`, `line` (pixel-perfect `no_doubles`), `rect`,
  `ellipse`, `flood_fill`, `outline`, `paste_grid`
- **Shading & texture** — `shade` (ramp-step or nearest-snap darken/lighten),
  `dither_gradient` (ordered-dither blend between two indices), `scatter`
  (seeded grain/noise from an index set), `import_image` (drop an external PNG
  onto a layer or store it as a stamp)
- **Regions** — `copy_region` / `cut_region` (named stamps), `paste_stamp`
  (flip/rotate; ignores symmetry by default so placed content is never
  silently mirror-corrupted), `move_region`, `mirror_region`, `shift_layer`,
  `clear_region`, `save_selection`
- **Layers** — `add_layer`, `remove_layer`, `rename_layer`, `reorder_layer`,
  `set_layer_visibility`, `set_layer_lock`, `merge_down`
- **Viewing** — `view`, `view_text`, `view_diff` (vs. checkpoint),
  `view_window` (zoomed patch + context thumbnail), `view_tiled` (NxN repeat
  with seams marked — the perception tool for seamless textures)
- **References** — `load_reference`, `quantize_reference`, `view_reference`
- **Animation** — `add_frame`, `remove_frame`, `reorder_frame`,
  `set_frame_duration`, `copy_cel`, `view_frames` (onion skin; the model's
  perception tool for motion), `preview_gif` (human-facing output — clients
  show a GIF as a single still in model context)
- **Lint** — `lint` with all ten spec rules (orphans, doubles, banding,
  pillow_shading, unused_colors, near_duplicates, broken_outline,
  symmetry_drift, stray_alpha, tile_seams) plus persistent waivers via
  `lint_waive` / `lint_unwaive`: judged-and-accepted findings stay suppressed
  on every later pass. `doubles` flags rhythm breaks in diagonal staircases,
  not every 2-over-1 step, so ellipses don't light up; `symmetry_drift` reports
  one finding per contiguous asymmetric region; `tile_seams` only fires when a
  tile mode is set, so ordinary sprites stay quiet.

Every editing tool returns the changed bounding box plus a render of the
changed region (4px context) and a whole-canvas thumbnail; pass
`render: false` to skip. All edits are atomic — a failed call leaves the
document untouched — and undoable (depth 100).

## Grid text

One character per pixel, one line per row: `.` = transparent, `1`-`9` map to
palette indices 1-9, `a`-`z` to 10-35, `A`-`Z` to 36-61, `#` = 62, `@` = 63.
(`0` parses as transparent.) Digit characters equal their palette index,
matching the spec's example; the spec's prose lists one more character than
there are slots, which this resolves.

```
grid 8x8 palette=4
.111111.
11111111
11211211
11111111
12111121
11222211
11111111
.111111.
legend: 1=yellow #f8e030  2=black #202020
```

Workflow tip that earned its place in live testing: for a planned sequence of
edits, pass `render: false` on each call and batch-verify with `view` at
milestones. Use `view_text` for pixel-exact ground truth and `view` for
gestalt — the two channels together are the point.

## Spec deviations and gap-fills

- `set_symmetry`, `set_active_frame`, `save_selection`, `list_documents`,
  `delete_document` are added; the spec references the underlying state but
  defines no tool to manage it.
- `open_document` and `load_reference` accept PNG only; Aseprite binary import
  is not implemented (export to Aseprite JSON is).
- `quantize_reference` with `palette: 'auto'` appends the extracted colors to
  the document palette so the returned indices are immediately valid.
- Persistence (not in the spec) as described above.

## Development

Manual end-to-end smoke that writes renders to `scripts/smoke-out/` for
eyeballing: `node scripts/smoke.mjs`.

TDQS

B3.4/5.0

Scored across 70 tools

Disambiguation4/5

With 70 tools, some overlap is inevitable, but each tool has a detailed description that clarifies its distinct purpose. For example, the various view tools (view, view_text, view_window, etc.) serve specific viewing needs. A few tools like copy_region and cut_region could be confused, but overall disambiguation is strong.

Naming Consistency5/5

All tools follow a consistent snake_case naming pattern, typically verb_noun (e.g., set_tile_mode, add_layer, reorder_frame). Even single-word verbs like view and undo fit the pattern. No mixing of conventions like camelCase or inconsistent verb forms.

Tool Count2/5

70 tools is very high for an MCP server, far beyond the typical 3-15 range. While the domain of pixel art editing is complex, this count feels excessive and could overwhelm agents. Consolidating some tools (e.g., merging palette editing tools) would improve usability without losing functionality.

Completeness5/5

The tool surface covers the full workflow of pixel art creation: document management, layers, frames, palette editing, drawing primitives, region operations, stamps, selections, linting, and export. There are no obvious gaps for basic to intermediate pixel art tasks.

Maintenance

ActivitySlowing
ResponsivenessNo issues