Skip to main content
Glama

pixel-mcp

An MCP server that lets an LLM create and edit pixel art reliably. Implements 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

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:

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

Related MCP server: kayamcp

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.

Available Tools

70 tools
add_frameA

Append a frame (optionally duplicating an existing one). The new frame becomes active.

Inputs: doc_id (required), duration_ms, duplicate_from.

add_layerB

Add a new transparent layer (becomes the active layer).

Inputs: name (required), doc_id (required), index.

add_palette_colorB

Append a color to the palette. Fails when the palette is full (64 entries).

Inputs: hex (required), doc_id (required), name.

adjust_palette_colorA

Nudge a palette color in HSL space: darken/lighten via lightness, shift saturation or hue. Saves the model from hand-computing hex. Edits in place (all pixels using it update), or appends a fresh variant with as_new.

Inputs: index (required), doc_id (required), hue, name, +4 more.

checkpointB

Label the current state for later comparison with view_diff.

Inputs: label (required), doc_id (required).

clear_regionB

Set every pixel in a region to transparent.

Inputs: doc_id (required), region (required), clip, frame, +3 more.

copy_celC

Copy one layer's pixels from one frame to another.

Inputs: doc_id (required), to_frame (required), from_frame (required), layer.

copy_from_documentA

Copy a region of pixels from another document into this one, lossless in palette-index space — the assembly tool for multi-agent fan-out: subagents draw parts in their own documents, then a parent composes them. Source colors map to identical destination palette entries where they exist; the rest are appended ('extend', default) or snapped to the perceptually nearest existing entry ('nearest'). Placed content: ignores symmetry by default, like paste_stamp.

Inputs: doc_id (required), from_doc_id (required), x, y, +10 more.

copy_regionB

Copy a region into a named stamp for later paste_stamp.

Inputs: name (required), doc_id (required), region (required), frame, +1 more.

create_documentA

Create a new pixel art document. Palette is a preset name (gameboy, grayscale4, pico8, sweetie16, nes) or a list of hex colors; index 0 is always transparent.

Inputs: width (required), height (required), palette.

cut_regionB

Copy a region into a named stamp and clear it from the layer.

Inputs: name (required), doc_id (required), region (required), clip, +4 more.

delete_documentA

Permanently delete a document from memory and disk. Cannot be undone.

Inputs: doc_id (required).

describeB

Full document state: size, palette with usage counts, layers, frames, symmetry, stamps, selections.

Inputs: doc_id (required).

dither_gradientA

Fill a region with an ordered-dither gradient between two palette colors. The indexed-color way to blend a smooth-looking transition: from_index at one end, to_index at the other. axis horizontal/vertical/radial; pattern bayer (4x4, smoother) or checker (2x2, coarser). Either index may be 0 to fade to/from transparent.

Inputs: doc_id (required), region (required), to_index (required), from_index (required), +7 more.

ellipseC

Draw an ellipse centered at (cx,cy) with radii rx,ry (outline or filled).

Inputs: cx (required), cy (required), rx (required), ry (required), +8 more.

exportA

Export the document to a real file and return its absolute path. png: one frame with true transparency. gif: all frames animated. aseprite_json: JSON with embedded spritesheet. This is delivery for humans — the response contains no rendered image; use view/view_frames for perception.

Inputs: doc_id (required), format (required), loop, path, +3 more.

extract_paletteA

Extract a color palette from a PNG supplied as exactly one of png_base64 or path, with median-cut quantization, WITHOUT creating a document. Returns a list of hex colors ready to pass to create_document(palette=[...]) (or add_palette_color), plus a swatch render so the colors are visible. Use this to pull a palette from a reference image; use open_document when you also want its pixels.

Inputs: path, sort, max_colors, png_base64.

flood_fillA

Flood fill from a seed pixel. contiguous=false recolors every pixel that matches the seed color.

Inputs: x (required), y (required), index (required), doc_id (required), +6 more.

generate_rampB

Append a shading ramp built around a base color (dark to light). Shadows hue-shift toward cool, highlights toward warm.

Inputs: steps (required), doc_id (required), base_hex (required), name, +1 more.

import_imageA

Import an external PNG supplied as exactly one of png_base64 or path onto the canvas. Resamples to width x height (default native size), maps to the document palette ('document') or extends the palette with median-cut colors ('extend'), then writes it to a layer at (x,y). With as_stamp it is stored as a named stamp instead (place later with paste_stamp). Unlike open_document (new doc) this drops pixels into the current canvas.

Inputs: doc_id (required), x, y, clip, +12 more.

lineB

Draw a Bresenham line. no_doubles removes L-shaped double pixels for pixel-perfect diagonals.

Inputs: x1 (required), x2 (required), y1 (required), y2 (required), +8 more.

lintA

Run pixel-art quality checks and return findings with a render circling them. Rules: orphans, doubles, banding, pillow_shading, unused_colors, near_duplicates, broken_outline, symmetry_drift, stray_alpha, tile_seams. Findings are advisory; nothing is auto-fixed.

Inputs: doc_id (required), frame, rules, region.

lint_unwaiveA

Remove a lint waiver by id, or all waivers for the document when no id is given.

Inputs: doc_id (required), waiver_id.

lint_waiveA

Persistently waive lint findings so they stay waived on every future pass (reported only as a count). Scope to a region to waive only there; omit the region to waive the whole rule.

Inputs: rule (required), doc_id (required), note, region.

list_documentsA

List all documents, including ones persisted by other sessions sharing the data dir.

Inputs: no named parameters.

load_referenceA

Store a reference PNG supplied as exactly one of png_base64 or path under an id for quantizing or side-by-side viewing.

Inputs: ref_id (required), path, png_base64.

merge_downA

Merge a layer into the one below it. Non-transparent top pixels replace bottom pixels; partial layer opacity is discarded.

Inputs: doc_id (required), layer.

mirror_regionB

Mirror a region in place across its own bounding-box axis.

Inputs: axis (required), doc_id (required), region (required), clip, +4 more.

mix_palette_colorsA

Blend two palette colors and store the result. The indexed-color way to make an in-between shade: t interpolates from color a (0) to color b (1). Appends a new entry by default, or overwrites into_index.

Inputs: a (required), b (required), doc_id (required), t, +2 more.

move_regionB

Move a region by (dx,dy), clearing its old location.

Inputs: dx (required), dy (required), doc_id (required), region (required), +5 more.

open_documentA

Import a PNG image as a new document from exactly one of png_base64 or path, quantizing to at most max_colors colors. Max 256x256.

Inputs: path, max_colors, png_base64.

outlineB

Draw a 1px border around all non-transparent pixels of the layer (or region). 'outside' draws into transparent pixels; 'inside' onto the shape's edge.

Inputs: mode (required), index (required), doc_id (required), clip, +5 more.

pack_sprite_sheetA

Pack document frames into a transparent PNG sprite sheet in row-major order and write it to disk. Sprites may have different sizes; cell dimensions default to the largest sprite. This is delivery output and returns no preview image.

Inputs: sprites (required), path, align, margin, +5 more.

paste_gridA

Bulk-write a region from grid text (one char per pixel, '.'=transparent, then 1-9, a-z, A-Z, '#', '@' for palette indices 1-63; rows top to bottom). The workhorse for blocking in shapes.

Inputs: x (required), y (required), doc_id (required), grid_text (required), +6 more.

paste_stampA

Paste a named stamp with optional flips and rotation. Transparent stamp pixels are skipped by default. Unlike other tools, paste_stamp ignores document symmetry by default: stamps are placed content, and mirroring would silently flip deliberate asymmetry.

Inputs: x (required), y (required), name (required), doc_id (required), +9 more.

preview_gifA

Render the animation as a GIF (on checkerboard background). Human-facing output only: most clients show a GIF as a single still in model context, so it verifies nothing — use view_frames to check motion.

Inputs: doc_id (required), loop, scale.

quantize_referenceA

Downsample a reference to the given size and map it to the document palette ('document') or to a fresh median-cut palette appended to the document ('auto'). Optionally write the result into a layer.

Inputs: width (required), doc_id (required), height (required), ref_id (required), +4 more.

rectC

Draw a rectangle (outline or filled).

Inputs: h (required), w (required), x (required), y (required), +8 more.

redoB

Redo previously undone edit(s).

Inputs: doc_id (required), steps, render.

remove_frameC

Delete a frame.

Inputs: frame (required), doc_id (required).

remove_layerB

Delete a layer and its pixels on every frame.

Inputs: layer (required), doc_id (required).

remove_palette_colorA

Delete a palette entry, remapping its pixels to remap_to. Higher indices shift down by one.

Inputs: index (required), doc_id (required), remap_to (required), render.

rename_layerC

Rename a layer.

Inputs: name (required), layer (required), doc_id (required).

reorder_frameC

Move a frame to a new position on the timeline.

Inputs: to (required), from (required), doc_id (required).

reorder_layerB

Move a layer to a new position in the stack (0 = bottom).

Inputs: to (required), layer (required), doc_id (required).

replace_colorA

Replace every pixel of one palette index with another on a layer (palette untouched). Optionally restricted to a region.

Inputs: doc_id (required), to_index (required), from_index (required), frame, +3 more.

resize_canvasA

Resize the canvas. Existing content is placed according to the anchor; new area is transparent. Clears saved selections.

Inputs: width (required), doc_id (required), height (required), anchor, +1 more.

save_selectionC

Store a named selection that region parameters can reference later.

Inputs: name (required), doc_id (required), region (required).

scatterA

Randomly scatter pixels from a set of palette indices across a region, at the given coverage density (0-1). For grain and noise: stone, dirt, wood texture. Deterministic for a given seed, so the same call reproduces the same grain. weights biases which indices are picked.

Inputs: doc_id (required), region (required), indices (required), clip, +7 more.

set_active_frameC

Set the default target frame for editing tools.

Inputs: frame (required), doc_id (required).

set_active_layerB

Set the default target layer for editing tools.

Inputs: layer (required), doc_id (required).

set_frame_durationC

Set how long a frame displays (ms).

Inputs: frame (required), doc_id (required), duration_ms (required).

set_layer_lockC

Lock or unlock a layer against edits.

Inputs: layer (required), doc_id (required), locked (required).

set_layer_visibilityC

Show or hide a layer.

Inputs: layer (required), doc_id (required), visible (required).

set_palette_colorB

Change the color (and optionally name) of a palette entry. All pixels using the index update visually.

Inputs: hex (required), index (required), doc_id (required), name, +1 more.

set_pixelsC

Set individual pixels to palette indices.

Inputs: doc_id (required), pixels (required), clip, frame, +3 more.

set_symmetryA

Set the document symmetry mode. While set, drawing and region tools mirror writes across the canvas center automatically.

Inputs: mode (required), doc_id (required).

set_tile_modeA

Set seamless-tiling mode. While set, drawing tools wrap across the tiling edges (a stroke off one side continues on the opposite side) and the tile_seams lint rule activates. 'horizontal' tiles left-right, 'vertical' top-bottom, 'both' for a full repeat. Use view_tiled to check seams.

Inputs: mode (required), doc_id (required).

shadeA

Darken or lighten existing pixels. A pixel whose color is part of a ramp steps along that ramp; otherwise it snaps to the nearest palette color in that direction. mode 'ramp' only moves ramp colors, 'nearest' ignores ramps, 'auto' (default) does ramp-then-nearest. Operates on the whole layer unless a region is given.

Inputs: doc_id (required), direction (required), mode, frame, +4 more.

shift_layerB

Translate the whole layer by (dx,dy), optionally wrapping around the edges.

Inputs: dx (required), dy (required), doc_id (required), wrap, +3 more.

split_sprite_sheetA

Split a regular PNG sprite sheet into separate pixel-mcp documents in row-major order. Provide exactly one of png_base64 or path. Supports sheet margins and spacing between cells; transparent cells are skipped by default.

Inputs: sprite_width (required), sprite_height (required), path, rows, +6 more.

swap_palette_indicesA

Swap two palette entries and all pixel references to them. The image looks unchanged.

Inputs: a (required), b (required), doc_id (required).

undoB

Undo the last edit(s).

Inputs: doc_id (required), steps, render.

viewA

Render the canvas (or a region) as a PNG. The primary perception tool: call it after every couple of edits.

Inputs: doc_id (required), frame, scale, coords, +3 more.

view_diffB

Compare the current state against a checkpoint: before, after, and changed-pixels panels.

Inputs: since (required), doc_id (required).

view_framesA

Render frames as a horizontal strip. With onion, each frame shows the previous frame at 40% opacity beneath it. This (plus per-frame view) is the perception tool for animation — use it to verify motion, not preview_gif.

Inputs: doc_id (required), onion, frames.

view_referenceB

Render a reference image, optionally beside the current canvas at matched display size.

Inputs: ref_id (required), frame, doc_id, beside_canvas.

view_textA

Read pixels as grid text (max 64x64; use view_window or a region for bigger canvases).

Inputs: doc_id (required), frame, layer, coords, +1 more.

view_tiledA

Render the canvas repeated reps x reps so tiling seams are visible, with the tile boundaries marked in faint red. The perception tool for seamless textures — a tile only reads as seamless when you see it tiled. Pairs with set_tile_mode and the tile_seams lint rule.

Inputs: doc_id (required), reps, frame, scale.

view_windowA

Zoomed view of a 16 or 32 px window plus a whole-canvas thumbnail with the window marked. The intended workflow for 64px+ canvases.

Inputs: cx (required), cy (required), size (required), doc_id (required), +1 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers