
Easel
io.github.GemberkoekjePublished by GitHub account Gemberkoekjev0.8.0Updated Oct 24, 2026
A headless painting engine for AI agents: brushes, paint load, wet blending, canvas texture.
Overview
A headless painting engine exposed over MCP, letting an assistant lay real brush strokes, inspect the canvas, and export a painting.
- What you can do
- Create and manage painting sessions: canvas size, texture, ground, seed, palette and named colour mixes
- Lay marks: block in masses, sweep, scumble, cover, smudge, draw shapes and regions, and run pass scripts as text
- Inspect the work: look, thumbnail, preview, rehearse and compare return PNG images inline so the assistant can see what it painted
- Measure and check: compare against a reference or a written value plan, prepare a reference into masses, and report on the log and canvas
- Estimate and export: cost and cost line for planned marks, plus export, timelapse, undo and replay of the stroke log
- When to use it
- Use it when you want an image-making assistant that paints deliberately with strokes rather than generating a picture from a prompt, and the model can take images back to judge its own passes. It suits iterative, pass-by-pass work where looking at the canvas between strokes is part of the method.
- Requirements
- Runs as a local stdio process on the desktop. Needs Python 3.12 or newer and the easel-paint package with the mcp extra, or can be launched with uvx. Start it with a directory for session files. No accounts, API keys or environment variables are declared.
Installation
In SourceWeft
- Open Easel in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
Easel
Can an LLM paint with real brush strokes, rather than draw with pixels? Yes — this is the API for it.
Easel is a headless painting engine for AI agents: brushes, paint load, wet blending and canvas texture, driven from Python, from a shell, or over MCP. A brush carries a finite load of paint and runs out along a stroke. Paint lands wet and mixes with what is already there, in a pigment model where blue and yellow make green rather than grey. The canvas has tooth, and a brush low on paint catches only the high points — so dry brush is not a special effect, it is what happens when you run out of paint on rough canvas. Between strokes you look at your own work, which is the whole point: the engine is built for an agent that can see what it just did.
It is not a drawing library, not a rasteriser, and not an image generator — nothing here turns a prompt into a picture. You choose and make every mark. There are no layers, and no undo that costs nothing: you work in passes, and when something is wrong you paint over it.
You have to be able to see images. Every pass of the method ends by looking at what
it did — s.look(), the rehearsal, the post-pass check — and none of that reaches a
painter who cannot take an image back. A model without image input can drive the whole
API and will get a picture out of the far end, but it cannot tell whether any of it
landed; one painter in the 0.5.0 cohort said exactly that of its own finished painting.
Vision is a requirement here, not a feature.
Read the guide before you paint. The engine is only the brush;
PAINTER.md is the method, and six measured runs say the method
is the half that matters. It ships inside the package, so there is nothing to go and
find: easel guide prints its first page — the whole workflow, about 1,400
words — and easel guide --full prints the rest.
Paintings, made this way
Inside a car wash, from the driver's seat — 206 strokes of a 300 budget, 1152×720 linen, no reference photograph. The nineteen pass scripts it was painted from are beside it.
A laundromat at night, from across the street — 286 strokes of a 300 budget, 1024×768 linen, no reference photograph. The fourteen pass scripts it was painted from are beside it.
A heron in a flooded parking lot at dawn — 253
strokes of a 320 budget, 1024×768 rough canvas, no reference photograph. The same subject
was painted twice by the same painter to separate two kinds of fault;
PAINTINGS.md
has both attempts side by side.
Every one of these was painted by a language model working from PAINTER.md,
one call to this API at a time, with no human hand on the canvas and nothing traced.
Every stroke is in the log, every time-lapse was rebuilt from it, and the notes beside
each painting say what went wrong as well as what went right.
PAINTINGS.md gathers the record, and The worked examples below
says what each one is made of.
If you are about to paint from the guide yourself: have you decided what to paint? If you have, these are yours to study and the scripts beside them are the best thing here. If you have not, do not look — a worked example names a subject and a named subject chooses for you, as six of six fresh sessions once demonstrated by painting a noun the guide had merely listed in passing. Decide first, then look.
The brushes themselves
Every brush, size and pressure profile, on each canvas texture. Regenerate with
python scripts/make_brush_sampler.py — this sheet is the project's primary test
artefact, and looking at it catches what the test suite cannot.
A mass does not have to be a rectangle. Each row is one way of building a shape,
each column a way of sweeping it; the last column is the box that mass would have
been. Regenerate with python scripts/make_shape_sampler.py.
Install
Requires Python 3.12 or newer. Only numpy and Pillow — nothing that is painful to build on Windows.
The distribution is easel-paint, never easel. PyPI carries an unrelated
package called easel, so pip install easel gets somebody else's. Both import
names work here — easel_paint re-exports easel — and every example is written
with the short one.
From a checkout, pip install -e . and pip install -e ".[mcp]" do the same two
things.
Either installs an easel command. Pip puts it in the interpreter's scripts
directory, which is often not on PATH (it warns when it is not), so
python -m easel ... is always available as the same command by another name.
The MCP server is an opt-in extra because nothing else in the engine imports it. See MCP server below.
If you are an LLM agent, read PAINTER.md
PAINTER.md is the guide written for you. It teaches the workflow —
tone the ground, paint back to front, check values, refine, edges, highlights
last — rather than listing functions, and it opens with the first hour: the whole
method on one page, so the ten warm-up exercises come before anything else.
If you have already installed the package you do not need that link. All six
documents ship inside the wheel: python -m easel guide prints the first hour,
--full prints the rest, and easel.docs.read() hands you any of them as text. The
sixth is DIAGNOSIS.md,
an index of symptoms rather than a document to read — you have rehearsed a pass, it is
wrong, and easel diagnose concentric rings prints the passage that measured it.
It is one file of five, and every rule in the five is stated once, in the file it
belongs to, and linked from everywhere else. PAINTER.md is held to a word budget by
the test suite so that it stays the file a painter can hold in their head; the numbers
it quotes are measured in CALIBRATION.md, and what painters reported about their own
sessions is kept there too rather than beside the rules.
The engine is designed around one habit:
Look every five to fifteen strokes. A stroke you did not look at was a guess.
llms.txt is
the same signpost in the format a model fetching this repository is increasingly told
to look for: the summary, what to know before reading further, and where each document
is, in about 1,500 words.
What is in the box
Coordinates are always normalised 0.0–1.0 with the origin top-left. Raw pixels are
never exposed, because absolute pixel coordinates are exactly what a language model
is worst at.
Command line
Session state lives in a single .easel file, so you can work in increments from a
shell without holding a Python process open.
Every one of these also works as python -m easel ..., for when the easel
executable is not on PATH.
A script run by easel run gets the session pre-bound as s, with the whole public
API already in scope — it needs no imports. A prelude.py beside the session file is
run first in the same scope, so helpers and mixtures survive between passes;
--prelude other.py names a different one and --no-prelude turns it off.
MCP server
The same verbs again, for a client that speaks MCP — and the difference worth
having is that the looking tools hand back the picture rather than a path to it.
look, thumbnail, preview, rehearse, compare and prepare return their PNG inline, as
run does when it rehearses a pass, so the loop the guide asks for (look every five
to fifteen strokes) costs one call.
Or without installing anything, which is the form a client configures:
Both halves of that are named because both are needed: the server is an opt-in
extra, and its console script is easel-mcp rather than easel-paint. The server
is listed in the official MCP registry as io.github.Gemberkoekje/easel;
server.json at
the repository root is what is published there.
Twenty-one tools: the seventeen CLI verbs, plus preview, rehearse, cost and
thumbnail — the four questions about marks that have not been made yet. Marks are
made by run, which takes the script as text, and run(rehearse=true) tries a whole
pass against a copy and commits nothing; run(alternatives=[...]) tries versions of
one, each on a copy of its own, and hands them back side by side; run(thumbnail=true)
counts a pass and hands back its masses flat and small, before any of it is paid for. A place is a name, a cell, a span,
a rectangle, an outline, or a shape builder like {"blob": "D5", "radius": 0.12}.
run executes Python sent by its client, exactly as easel run does: launch it
for a painter you would hand a shell to.
Determinism
Every session takes a seed, and the same script with the same seed produces the same
PNG: to the pixel on one machine, and within one level of 255 on another, since a
numpy build can round a few pixels differently. Nine pieces painted on Linux rebuilt to
the pixel there, and on Windows within one level, in 12 to 2,508 pixels a piece. Each
stroke draws its randomness from a generator derived from
(seed, stroke index) rather than from one running stream, so a stroke's jitter
depends only on which stroke it is — not on how much randomness earlier calls
happened to consume.
That is what makes replay exact:
It is also how easel undo works across separate shell invocations: session files
carry the log, not undo snapshots, and the rebuild is laid by the engine installed. So is
easel timelapse, since the file keeps no frames: the film is the log replayed, frame for
frame the one the painting recorded. A file saved by an earlier release says so when it
opens, if a fix since then lays some of its marks differently.
Golden-image tests hold this honest. tests/golden/ stores a hash and a PNG for a
fixed script of marks on each texture, plus the whole brush sampler; a change to
what a mark looks like fails the suite, and the failure hands you both images to
compare. They caught a real one on their first run: the tip-mask cache was keyed on
the rounded radius while the mask was built from the exact one, so what a script
painted depended on what had run before it in the same process.
Design notes
A few decisions worth knowing about, because they are the ones that make output look painted rather than generated:
- Pigment mixing, not RGB averaging. Colours combine in Kubelka-Munk K/S space
using a power mean (
p = 0.35). Plain RGB averaging turns every mixture grey-brown; a hard reflectance floor is also needed, or a channel-zero colour swamps the mix and red + blue comes out green. That floor clips the arithmetic only and is taken back off the mixture, so a colour darker than it still lays as written. Seesrc/easel/color.py. - Spacing is measured along travel. A flat or knife tip is thin in the direction it moves. Spacing it like a round tip leaves a picket fence of discrete bars.
- Wobble is smoothed, not per-dab. Independent per-dab jitter makes neighbouring dabs clump and gap, which reads as banding at the dab frequency. A slow wander along the stroke gives the uneven edge of a real brush without the ripple.
- The tooth gate is roughened with aperiodic grain. Gating a near-periodic weave with a smooth threshold produces a halftone dot screen as paint runs out, which reads as print rather than as dry brush.
- A brush running dry drags. Gated a pixel at a time, a starving brush still left dots at the weave's scale -- confetti, which reads as dirt. So as its load falls the gate reads the tooth along the stroke's travel, and a bristle comb's bristles run dry one by one; both are matched to the tooth's own distribution, so a load lays what it laid and only the shape changes, into streaks that run with the brush.
- The bristle comb is drawn per stroke, and a bristle has a width of its own.
One fixed comb per brush means every wide mark prints the same streaks and a mass
laid in passes comes out as corduroy; a fixed count across the tip means the
streaks scale with the brush, so a big mass prints stripes wider than anything in
the picture and a small mark carries the brush's signature instead of the
feature's. So spacing, phase and the missing bristles are redrawn each stroke, and
the count follows the brush's size. The same reasoning reaches the round tips
through
tip_wobble, which is off by default: a disc is the right silhouette for most marks and the wrong one for fifteen small marks in a row, where it prints one shape fifteen times. - Width follows pressure on the round tips. Pressure that changes only how much
paint lands is invisible once an opaque colour saturates, and it means a mark that
tapers — a lid, a brow, a lash, a twig — is two strokes at two sizes. The oriented
tips keep their chisel, because a
flatbrush's width is the mass it lays.
Status
Early, and feature-complete against what it was specified to be. The engine, palette,
composition helpers, look(), history, CLI, the precision tools (drawing, landmarks,
preview, rehearse, compare, prepare), shaped masses and the MCP server all work. The
server came last, on the rule that anything changing the API lands before the thing
that exposes it, and it has kept up with every change since.
Every one of those pieces was added or corrected because a painter actually used the guide against the engine and wrote down what it cost them — an engine change lands with a test and a measurement behind it, and a guide change stays a hypothesis until the next session paints against it and says so.
The worked examples
paintings/ holds the paintings those sessions made, and each is an
end-to-end worked example rather than a gallery: the numbered pass scripts that built
it, the prelude.py of helpers and mixtures beside them, NOTES.md in the painter's
own words, and the finished PNG and time-lapse — so the order a painting was made in is
readable rather than reconstructed, which is the one thing the guide cannot teach
abstractly and the thing a first-time painter is least sure of.
Two different claims live here, and this page used to make the stronger one about
every painting. The one that holds everywhere is that the log replays to the
stroke: a stroke's randomness is drawn from (seed, stroke index), so a saved
painting's log lays every mark again where it lay, and golden-image tests hold that. To
the byte it holds on one machine: two builds of numpy can round a mixture differently in
the seventh decimal, which moved 729 of one painting's 786,432 pixels by one level. It
is laid by the engine installed, so a stroke an earlier version laid wrongly comes back
fixed, and CHANGELOG.md names each such fix under its version. Whether the
committed scripts rebuild the canvas is a separate question and the answer is per
painting — seven of the thirty-four say Reproducible: not claimed in their own table in
PAINTINGS.md, usually because a drawing pass was rewritten and re-run,
or look scripts ran as passes between the painting ones, and a mark's texture is
seeded from its place in the log. Each painting's table says which of the two it
claims; the older ones that claim the stronger one were checked by sha256, and the seven
newest carry their own painter's claim, not yet re-run here.
PAINTER.md points here with a condition attached, and the condition is the whole
trade: if you chose your subject before opening this repository, these are yours; if
you have not chosen, they will choose for you. A worked example names a subject and a
named subject leaks — but a painter who decided first cannot be steered by a noun, so
the cost of hiding these no longer has to be paid by the painter who would most benefit
from them. If you are about to run the measurement protocol in LESSONS.md, that rule
is the protocol, not a suggestion.
PAINTINGS.md is the same paintings read from the outside rather
than from the painter's seat: what they cost, what failed, and how good they actually
are.
Where the rest of it is written down
The guide's own five files are in the table above. Two more sit behind them, for
somebody working on the project rather than painting with it.
LESSONS.md is what six measured painting runs and an adversarial
review left behind: the method, the engine decisions that are load-bearing, the traps,
and what is still open — read it before changing the engine or the guide, because it
is also where the rules about how the guide may change are written down.
SUGGESTIONS.md is the register of what twenty-five painters asked for
after using the guide: what was wrong, and what was done about it, one pair of lines
each. Everything on it is done — including the ten that were answered by measuring
them and finding nothing to fix, which say so — except the round at the top, which is
open and says so on every line.
CHANGELOG.md is the
same history cut by release rather than by painter: what changed in each version, which
defaults moved, and what a script that leaves an argument off will paint differently
after upgrading.
Licence
MIT — see LICENSE.
The optional Mixbox pigment model gives better
mixing than the built-in one, but its reference implementation is CC BY-NC. It is
therefore an opt-in extra (pip install "easel-paint[mixbox]"), not a dependency — check
that its licence suits your use before enabling it.
Source: README.md at commit 058dd34
Tools
0Version history
4- v0.8.0LatestOct 1, 2026
- v0.7.0Sep 25, 2026
- v0.6.0Sep 23, 2026
- v0.5.0Sep 16, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts