Skip to main content
Glama
Aseprite MCP Server

Aseprite MCP Server

README.md
<div align="center">

# Aseprite MCP Server

An MCP server for drawing pixel art in Aseprite through pixel-level operations, canvas inspection, and iterative correction.

[![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
[![FastMCP](https://img.shields.io/badge/FastMCP-4.x-FF6B35?style=flat-square)](https://github.com/jlowin/fastmcp)
[![Aseprite](https://img.shields.io/badge/Aseprite-v1.3%2B-7D9F37?style=flat-square)](https://aseprite.org/)

<p align="center">
  <a href="README_CN.md">简体中文</a>
</p>

</div>

## Quick start

Prerequisites: Aseprite v1.3+ installed (note the full path to its executable) and
uv (Windows `winget install astral-sh.uv`, macOS `brew install uv`, or
`curl -LsSf https://astral.sh/uv/install.sh | sh`). `uv run` creates an isolated
environment from `pyproject.toml` and installs `fastmcp`, `pillow`, and `websockets`;
no separate pip step or virtualenv is needed.

```bash
git clone https://github.com/ZhangDongyang800/Aseprite_MCP.git
cd Aseprite_MCP
uv run -q python -c "import json, os; print(json.dumps({'mcpServers': {'aseprite': {'command': 'uv', 'args': ['run', '--directory', os.getcwd(), 'server.py'], 'env': {'ASEPRITE_PATH': 'C:/Program Files/Aseprite/aseprite.exe'}}}}, indent=2))"
```

The last command prints a client configuration with your clone path filled in and
Windows backslashes escaped for JSON. Set `ASEPRITE_PATH` to your Aseprite
executable; if auto-detection finds it, the `env` block can be omitted. TRAE,
Claude Desktop, Cursor, and Qoder use the same configuration.

To verify the setup, ask the client: *"create a new 8x8 canvas, flood it red, then
inspect it"*. A red square and `coverage 1.0` indicate a working server.

Aseprite must be installed locally. The server supports two execution modes:

- **CLI mode** (default): each tool call starts a headless Aseprite process
  (`aseprite -b`). No UI; state is passed through `.ase` files.
- **Live mode** (WebSocket): operations run against a running Aseprite instance,
  with visible UI and persistent state. See [Live mode](#live-mode-optional-websocket).

## Demo

The sprites and tileset below were generated through this MCP server. Pixels are
written by `apply_operations` (`paint_grid`), and frame structure is created through
ops. Each demo includes a re-runnable generator under `demo/<name>/generator/`.

| Character | ↓ Down | ↑ Up | ← Left | → Right | Sprite Sheet |
|:--|:--:|:--:|:--:|:--:|:--:|
| **Chibi Knight**<br>4 dirs × 6 frames | ![](demo/Knightling/knight_walk_down.gif) | ![](demo/Knightling/knight_walk_up.gif) | ![](demo/Knightling/knight_walk_left.gif) | ![](demo/Knightling/knight_walk_right.gif) | ![](demo/Knightling/chibi_knight_spritesheet.png) |
| **Dark Reaper**<br>4 dirs × 6 frames | ![](demo/dark_reaper/dark_reaper_walk_down.gif) | ![](demo/dark_reaper/dark_reaper_walk_up.gif) | ![](demo/dark_reaper/dark_reaper_walk_left.gif) | ![](demo/dark_reaper/dark_reaper_walk_right.gif) | ![](demo/dark_reaper/dark_reaper_spritesheet.png) |
| **Slime Devourer**<br>4 dirs × 5 frames | ![](demo/slime_devourer/slime_devourer_devour_down.gif) | ![](demo/slime_devourer/slime_devourer_devour_up.gif) | ![](demo/slime_devourer/slime_devourer_devour_left.gif) | ![](demo/slime_devourer/slime_devourer_devour_right.gif) | ![](demo/slime_devourer/slime_devourer_spritesheet.png) |

The first two are walk cycles. The third is an action animation with per-phase frame
durations: idle → crouch → lunge with open maw → chomp → swallow.

### Terrain tileset

| Tileset sheet | Composed scene | Water loop |
|:--:|:--:|:--:|
| **Wilderness 47-blob**<br>228 slots · 32px · 12x19 | ![](demo/wilderness_47/wilderness_scene.png) | ![](demo/wilderness_47/wilderness_water.gif) |

The wilderness tileset covers four overlay terrains (dirt road, sand, water, and
stone), each with a complete 47-tile blob transition matrix, plus interior variants
and 24 object tiles in a 384x608 sheet. Water is a four-frame loop. The scene is
composed by looking up tiles from a terrain map rather than by hand placement, so a
missing tile is reported immediately.

## Table of contents

- [Quick start](#quick-start)
- [Demo](#demo)
- [Tools](#tools)
- [How to use](#how-to-use)
- [Live mode (optional, WebSocket)](#live-mode-optional-websocket)
- [Example prompts](#example-prompts)
- [Contributing](#contributing)
- [License](#license)

## Tools

The server exposes three MCP tools.

| Tool | Description |
|------|-------------|
| `apply_operations` | Execute a batch of ops inside one transaction — the only mutation entry point. Pass `session_id` + `ops[]`; `dry_run=true` validates without side effects; `confirmed=true` is required when the batch contains a destructive op (`clear_canvas`, `delete_layer`, `close_session`). If `session_id` is omitted and the first op is `create_sprite` / `open_sprite`, a session is created automatically. |
| `inspect` | Read-only perception: returns a canvas preview image plus quantitative metrics (palette, color count, bounding box, coverage, semi-transparent and isolated pixels). On animated documents, `frame=N` steps through frames one at a time. Never modifies the document. |
| `run_lua` | Escape hatch: run arbitrary Lua. Requires `unsafe=true` and `confirmed=true`. |

Ops are named operations registered in `src/v2/ops/` (Pydantic parameter models) with
their Lua implementations in `scripts/ops_*.lua`. Built-in ops: `create_sprite`,
`open_sprite`, `save_sprite`, `close_session`, `add_layer`, `set_layer`,
`delete_layer`, `set_palette`, `draw_pixel`, `draw_rect`, `draw_line`, `draw_ellipse`,
`fill_region`, `clear_canvas`, `undo`, `redo`, plus the structural ops `add_frames`,
`set_durations`, `add_tag`, `paint_grid`, and `export` (`png`/`gif`/`sheet`/`frames`;
it writes artifact files but never changes the document).

`paint_grid` takes a path to a Lua file returning
`{palette = {b = "#F0A65A"}, rows = {"..bb..", ...}}`, one character per pixel, with
`.` for transparent. A 32×32 four-frame sheet costs a few hundred tokens through
`paint_grid` instead of tens of thousands as `draw_pixel` ops, and still runs
validated and inside the same rollback as everything else.

`export` writes the deliverable without touching the document:
`{"op": "export", "format": "gif", "output": "walk.gif"}`; `format="png"` needs a
`frame` on multi-frame sprites, while `format="frames"` writes `name1.png…nameN.png`
and `format="sheet"` takes `layout` / `columns` / `padding` / `scale`. Output paths
are resolved to absolute; put export last (or in its own batch) so a later failing
op cannot leave an uncommitted artifact.

```python
apply_operations(ops=[
    {"op": "create_sprite", "width": 32, "height": 32},
    {"op": "draw_rect", "x": 4, "y": 4, "width": 24, "height": 24, "color": "#E74C3C", "filled": True},
    {"op": "draw_pixel", "x": 16, "y": 6, "color": "#FFFFFF"},
])
```

All drawing ops accept `layer` / `frame` (1-based, default 1/1). A batch runs inside
one `app.transaction`, so `atomic=true` (the default) rolls the whole batch back if
any op fails. With `atomic=false` the ops that worked are saved and listed one by
one, the failed one keeps its reason, and the envelope returns `ok=true` with a
`partial success: N of M ops ...` warning.

After drawing, call `inspect` to review the canvas and decide whether to correct it.
This forms a draw → inspect → correct loop.

## How to use

### 1. Prerequisites

| Dependency | Version | Notes |
|------------|---------|-------|
| [uv](https://docs.astral.sh/uv/) | any recent | manages Python and the dependencies for you |
| Aseprite | v1.3+ | note the full path to the **executable**, not the folder holding it |

Install uv with `winget install astral-sh.uv` (Windows), `brew install uv` (macOS),
or `curl -LsSf https://astral.sh/uv/install.sh | sh`.

### 2. Client configuration

The quick-start command prints a ready-to-paste configuration with your own paths
filled in and escaped. By hand it looks like:

```json
{
  "mcpServers": {
    "aseprite": {
      "command": "uv",
      "args": ["run", "--directory", "C:\\path\\to\\Aseprite_MCP", "server.py"],
      "env": {
        "ASEPRITE_PATH": "C:\\Program Files\\Aseprite\\aseprite.exe",
        "ASEPRITE_WORK_DIR": "C:\\ase_work"
      }
    }
  }
}
```

`ASEPRITE_WORK_DIR` is optional (it defaults to the repo's own `work/`). An
ASCII-only path is recommended but not required: current Aseprite builds (1.3.17
verified) load scripts from non-ASCII paths, but older builds and some console code
pages fail there with a "can't load … (lua)" error that resembles a build without
the Lua engine. The server prints a note at startup when the path is non-ASCII.

If the client cannot find `uv`, give `command` the absolute path to `uv`. Avoid a
bare `python` (the Windows Store alias intercepts it) and `pip install --user` (some
hosts strip `APPDATA`, so Python cannot find the packages). To skip uv entirely,
install into a virtualenv and point `command` at that interpreter:

```bash
python -m venv .venv                                  # Windows, if `python` is missing: py -3 -m venv .venv
.venv/Scripts/python.exe -m pip install -e .          # Windows
.venv/bin/python -m pip install -e .                  # macOS / Linux
```

## Live mode (optional, WebSocket)

Live mode operates on the Aseprite instance you have open. Drawing is visible as it
happens, and sprite state persists across tool calls, avoiding repeated file
open/save overhead.

### How it works

```
┌─────────┐    MCP (stdio)    ┌──────────────┐   WebSocket    ┌──────────────────┐
│  AI/TRAE │ ───────────────► │ Python MCP   │ ─────────────► │ Aseprite Extension│
│          │ ◄─────────────── │ Server       │ ◄──────────── │ (WebSocket client) │
└─────────┘                   └──────────────┘                └──────┬───────────┘
                                                                     │ Lua app.* API
                                                                     ▼
                                                              ┌──────────────┐
                                                              │ Visible Aseprite │
                                                              │ Sprite + UI      │
                                                              └──────────────┘
```

The Python MCP server starts a WebSocket server on `127.0.0.1:9001`. The Aseprite
extension connects to it as a client. Each MCP tool call is forwarded to Aseprite
over WebSocket, executed through the existing Lua scripts, and the result is sent
back.

### Setup

**1. Install the Aseprite extension**

The extension is in the `extension/` folder of this repo. Install it via:

- Open Aseprite → `File > Scripts > Open Scripts Folder`
- Copy the entire `extension/` folder contents into the scripts folder (or use
  `Edit > Preferences > Extensions > Add Extension` and select the `extension/` folder)

**2. Enable WebSocket mode in the MCP config**

Add `ASEPRITE_MCP_MODE=ws` to the `env` section of your MCP server config:

```json
{
  "mcpServers": {
    "aseprite": {
      "command": "uv",
      "args": ["run", "--directory", "C:\\path\\to\\Aseprite_MCP", "server.py"],
      "env": {
        "ASEPRITE_PATH": "C:\\Program Files\\Aseprite\\aseprite.exe",
        "ASEPRITE_WORK_DIR": "C:\\ase_work",
        "ASEPRITE_MCP_MODE": "ws",
        "ASEPRITE_WS_HOST": "127.0.0.1",
        "ASEPRITE_WS_PORT": "9001"
      }
    }
  }
}
```

**3. Connect Aseprite**

With the MCP server running, open Aseprite and click:

`File > Scripts > MCP Bridge: Toggle Connection`

Aseprite reports "MCP Bridge: Connected to ws://127.0.0.1:9001" when the connection
succeeds; drawing calls then run against the open document.

### Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `ASEPRITE_PATH` | auto-detected | Path to the Aseprite executable (overrides auto-detection via `PATH` and common install locations) |
| `ASEPRITE_WORK_DIR` | `<repo>/work` | The server's state directory: sessions live in `<work>/sessions/<uuid>/` and expire after `ASEPRITE_SESSION_TIMEOUT`. Absolute by default; non-ASCII paths work on current Aseprite builds, but older ones reject them and report a Lua load error |
| `ASEPRITE_SESSION_TIMEOUT` | `3600` | Seconds an idle session survives before the cleanup thread removes it |
| `ASEPRITE_TIMEOUT` | `30` | Seconds one Aseprite call may take. Raise it for big canvases (`fill_region` on 4096², blur); a timeout rolls the batch back and reports the failure |
| `ASEPRITE_MCP_MODE` | `cli` | Execution mode: `cli` or `ws` |
| `ASEPRITE_WS_HOST` | `127.0.0.1` | WebSocket server bind address |
| `ASEPRITE_WS_PORT` | `9001` | WebSocket server port |

## Example prompts

Example prompts for the demos above.

**Chibi Knight · 4 directions × 6 frames · 32x32**

> Use Aseprite MCP to generate a pixel art sprite sheet of a brave knight in silver armor holding a long sword, red plume and red cape. Four-direction walk cycle (down, up, left, right), 6 frames per direction, 32x32, flat colors, transparent background, 1px dark outline, light from the top-left.

**Dark Reaper · 4 directions × 6 frames · 32x32**

> Use Aseprite MCP to generate a pixel art sprite sheet of a dark reaper in a tattered black robe wielding a giant scythe, glowing red eyes under the hood. Four-direction walk cycle (down, up, left, right), 6 frames per direction, 32x32, flat colors, transparent background, 1px dark outline.

**Slime Devourer · 4 directions × 5 frames · 32x32**

> Use Aseprite MCP to generate a pixel art sprite sheet of a slime monster that devours its prey — green blob body, huge jaws, fangs. Four-direction devour animation (down, up, left, right), 5 frames per direction: idle → crouch → lunge with open maw → chomp → swallow. 32x32, flat colors, transparent background, sprite sheet layout.

## Contributing

Issues and pull requests are welcome. Please include reproduction steps and your
Aseprite version.

## License

This project is open-sourced under the [MIT License](LICENSE).

Copyright © 2026 [ZhangDongyang800](https://github.com/ZhangDongyang800)

TDQS

A4.3/5.0

Scored across 3 tools

Disambiguation5/5

The three tools have clearly distinct purposes: apply_operations is the sole programmatic mutation entry point, run_lua is an escape hatch for arbitrary scripts, and inspect is strictly read-only. No two tools overlap in functionality, making selection unambiguous for an agent.

Naming Consistency5/5

All names follow a consistent lowercase snake_case pattern starting with a verb: apply_operations, run_lua, inspect. The convention is uniform and predictable, with no mixed styles or ambiguous verbs.

Tool Count5/5

With only 3 tools, the surface is minimal but perfectly aligned with the server's purpose. The heavy lifting is encapsulated inside operation types for apply_operations, so the small count is intentional and well-scoped rather than sparse.

Completeness5/5

The tool set covers the full lifecycle: mutate (apply_operations), arbitary scripting (run_lua), and read/analyze state (inspect). Supporting operations like create_sprite and open_sprite are exposed as ops within apply_operations, so no critical gap exists for typical Aseprite workflows.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues