Aseprite-as-an-API. One static Rust binary that speaks MCP; an agent creates layered, animated documents, draws into them, looks at the result, and exports engine-ready art. No network, no keys, fully deterministic.
Understand these two and every file in the repo reads itself. Atelier has a data atom and a behavior atom; the whole product is the two of them composed upward.
One RGBA image at one (layer, frame) coordinate.
Document is just metadata plus a map: cels: HashMap<(layer, frame), (x, y, RgbaImage)> — document/mod.rsflatten(frame) composites the visible layers’ cels; exports walk frames of flattens; frame_diff compares two flattensdocuments/<id>/cels/L<layer>_F<frame>.pngremap_cel_layers and shift_cel_framesOne JSON object describing one edit to one cel.
{"op":"rect","x0":1,"y0":1,"x1":8,"y1":8,"color":[r,g,b]} — that shape is the entire mutation languagebatch_op_keys), validate_batch_op rejects unknown keys and malformed colours loudly, apply_op executes — document/batch.rsdoc_draw and doc_fx are one op, doc_batch is many, and even the fancy generators (rigs, tiles, particles) are Rust that emits the same primitive editsraster/(layer, frame) — the unit ops edit and PNGs storeatelier replay; a document is the product of its recipeSo the full flow is not a separate design — it is this ladder read top to bottom. The see-and-critique loop (doc_look, doc_critique, doc_ref_compare) exists because the agent driving the ops is nearly blind: draw → look → fix, measured instead of remembered.
Every document is an ordered sequence of tool calls. Draw a sprite through the API and the session can be recorded as a recipe — a JSON list of steps — and replayed byte-for-byte later. The files in docs/examples/*.json are exactly that: they are the brand art and the integration tests (atelier replay runs them against a real server).
The second idea is the see-and-critique loop. The model drawing the art is nearly blind, so the API is full of eyes: doc_look returns the frame as an inline PNG plus value statistics, doc_critique names failure modes, doc_ref_compare scores the canvas against a reference image. Draw → look → fix, measured instead of remembered.
State lives on disk under ~/.atelier (override: ATELIER_HOME): one directory per document holding doc.json (structure) plus one PNG per cel — the image at one (layer, frame) coordinate.
Functional core, imperative shell. Everything below atelier-mcp is synchronous, deterministic, and knows nothing about MCP or tokio. Dependencies point one way only.
#[tool] router over stdio + HTTPStudio facade — the library API, one method per toolBy default the server advertises a 30-tool core profile (ATELIER_PROFILE=full advertises all 75). That filter is discovery-only — every tool always executes, so replay always works. Both counts are pinned by tests; the docs cannot silently drift.
What happens when an agent calls doc_draw {"op":"line", …} — this path is the whole system in miniature, and it is the same for all 75 tools:
/mcp, one shared router.atelier-mcp/src/server/mod.rs · run / run_httpcall_tool dispatches; if recording is on, the successful step is appended to the recipe file.server/mod.rs + server/recorder.rsStudio method. No business logic lives here.server/mod.rs · async fn doc_drawStudio::doc_draw checks the op against core's DRAW_OPS partition and routes into the batch pipeline, honouring any active selection mask.atelier-studio/src/lib.rs · doc_draw → doc_batch → edit_maskedvalidate_batch_op rejects unknown keys/malformed colours loudly, then apply_op runs the edit.atelier-core/src/document/{mod,batch}.rsdoc.json + cel PNGs) and a change ack — pixel count + bbox, often an inline PNG — travels back up unchanged.document/mod.rs · save → back through Studio → JSON outEverything is a make target; never run ad-hoc scripts. make help lists them all.
# 1 · clone, wire the git hooks once, prove the tree is green
make hooks
make pre-commit-checks && make test
# 2 · watch it draw — replay a shipped recipe into an isolated home
make release
./target/release/atelier replay docs/examples/water-tile.json --home /tmp/atelier-demo
# 3 · run it as a real MCP server
make stdio # stdio transport (what clients spawn)
make run # or HTTP at 127.0.0.1:8765/mcp
claude mcp add --transport http atelier http://127.0.0.1:8765/mcp
# 4 · first session, from the client side
# doc_create → doc_draw / doc_batch → doc_look → doc_export
# look after every burst of edits; export op=sheet|anim|tileset
run / stdio | HTTP / stdio server |
test | the full suite (229 tests) |
pre-commit-checks | fmt + clippy gate (hooks run this) |
branding | replay every example recipe |
release / clean | optimized binary / wipe |
ATELIER_HOME | where documents live (default ~/.atelier) |
ATELIER_PROFILE | full advertises all 75 tools |
ATELIER_HTTP | HTTP bind address |
ATELIER_RECORD | record the session into a recipe |
ATELIER_ALLOWED_HOSTS | extra Host headers for LAN use |
Shortest path to a working mental model — top-down, following one call:
1. A recipe in docs/examples/ (pick water-tile.json, 18 steps) — it reads as a narrated session and shows what the tools feel like.
2. atelier-mcp/src/server/mod.rs — skim the #[tool] descriptions; they are the product spec.
3. atelier-studio/src/lib.rs — the Studio struct, open/edit_masked/change_ack: the shape every operation shares.
4. atelier-core/src/document/mod.rs — Document, DocMeta, the cel map, and the two invariant choke points (remap_cel_layers, shift_cel_frames).
5. docs/ARCHITECTURE.md last — it will now read as confirmation, not introduction.
document/batch.rs — key table in batch_op_keys, arm in apply_op_raw, add to DRAW_OPS or FX_OPSdocument/draw.rs or fx.rsdoc_draw/doc_fx/doc_batch description stringsserver/params.rs, thin #[tool] method in server/mod.rsCORE_TOOLSraster/colour.rs — OKLab/HSL, ΔE, ramps, quantisationraster/noise.rs — easing, fBm/perlin, dither patternsraster/transform.rs — affine, scale2x, IK, distance fieldsResult over panic; unknown strings error loudly, never default silently#[cfg(test)], next to the codemake pre-commit-checks && make test before gh pr ready