paint-mcp
Supports SVG as an output and input format: shapes can be drawn using SVG path data commands (M, L, H, V, C, S, Q, T, A, Z), and the recorded vector operations of the canvas can be exported to SVG.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@paint-mcpdraw a 640x400 sunset over hills and export it as a PNG"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
paint-mcp — an MCP server that lets an AI agent actually draw
Give any MCP client a real paint canvas: layers, anti-aliased shapes, brushes, bucket fill, filters, undo — and every drawing tool sends the picture back, so the model can look at what it made, judge it, and fix it. No native dependencies, no image libraries, no Python.
agent: paint_ops → [ picture comes back in the tool result ] → agent looks, adjusts, redraws
![]()
Three pictures above were produced by node scripts/demo.mjs, i.e. by the exact same operation
registry an agent calls over MCP. Re-generate them any time with npm run demo.
Install
One command (any agent / any machine, Node 20+):
git clone https://github.com/Mishaadevv/paint-mcp.git && cd paint-mcp && npm install && npm run buildThen register it with your MCP client (Qoder CLI, Claude Desktop, Cursor, …):
{
"mcpServers": {
"paint": {
"command": "node",
"args": ["/absolute/path/to/paint-mcp/dist/index.js"],
"env": { "PAINT_MCP_ROOT": "/absolute/path/where/art/should/land" }
}
}
}Qoder CLI users can do it in one line:
qodercli mcp add paint node /absolute/path/to/paint-mcp/dist/index.js --scope userOnce the package is on npm the same config collapses to npx -y paint-mcp-server (see Development for how a release is cut).
Reload MCP servers in a running session with /mcp reload, then say:
“draw a 640×400 sunset over hills and export it”.
Related MCP server: NeuroPaint-X
What you get
Canvas | multi-layer RGBA documents, |
Geometry | rect (rounded), ellipse/arc/pie, line (dashed, arrows), polygon, SVG path data ( |
Painting | freehand brush with pressure, hardness, spacing, jitter, flatten+angle nibs; bucket fill with tolerance; exact pixel writes; pixel-art sprites from text rows |
Colour | 140+ named colours, hex/rgb/hsl, gradients (linear/radial, multi-stop), 12 blend modes, per-op opacity |
Editing | 18 image filters (blur, sharpen, edge detect, hue, posterize, dither, vignette…), scale/rotate/flip/crop/resize/translate/trim, layer add/remove/reorder/merge |
History | undo/redo per operation, batch-aware, with labels |
Feedback | every reply carries a PNG thumbnail ( |
Live view |
|
Tools
paint_canvas (create/list/info/open/save/close/files/history) · paint_ops (batch, one call = one scene) ·
paint_preview · paint_inspect · paint_undo · paint_export · paint_import · paint_help · paint_serve
…plus one tool per operation, generated straight from the op registry so the docs and the behaviour
can never drift: paint_rect, paint_ellipse, paint_line, paint_polygon, paint_path,
paint_brush, paint_text, paint_pixel_grid, paint_pixels, paint_fill, paint_gradient,
paint_stamp, paint_clear, paint_filter, paint_transform, paint_layer.
Token-tight client? Start with --toolset compact to expose only the batch tool and utilities.
A session that actually works
// 1. give yourself a surface
paint_canvas { "action": "create", "canvas": "art", "width": 640, "height": 400, "background": null }
// 2. block out the whole idea in one cheap call, and look at the picture that comes back
paint_ops { "canvas": "art", "ops": [
{ "op": "gradient", "type": "linear", "from": [0, 0], "to": [0, 400], "colors": ["#0a1330", "#2b4d86", "#e7a45d"] },
{ "op": "ellipse", "cx": 430, "cy": 236, "radius": 26, "fill": "#fff3c4" },
{ "op": "path", "d": "M0 268 Q90 232 180 262 T360 258 T640 264 L640 400 L0 400 Z", "fill": "#16233d" },
{ "op": "text", "text": "DUSK", "x": 28, "y": 30, "scale": 5, "color": "#f4f7ff", "outline": 1 }
]}
// 3. not happy with the ridge? roll it back and redo just that piece
paint_undo { "canvas": "art", "steps": 1 }
paint_path { "canvas": "art", "d": "M0 300 Q160 240 320 300 T640 290 L640 400 L0 400 Z", "fill": "#0e1728" }
// 4. ship it
paint_export { "canvas": "art", "format": "png", "path": "out/dusk.png" }paint_help { "topic": "workflow" } and paint_help { "topic": "examples" } hold the same advice
in-server, so an agent never has to leave the protocol to learn the tool.
Configuration
Flag | Env | Default | Meaning |
|
|
| where canvases and exports live; paths are confined to it |
|
|
|
|
|
|
| longest edge of the returned thumbnail |
|
|
| undo steps per canvas |
|
| off | localhost live preview page |
|
| off | write the |
|
|
| expose only batch + utility tools |
|
| off | let paths leave the paint root |
— |
| 40 000 000 | canvas size guard |
node dist/index.js --help prints the same list.
Design notes
Zero native dependencies. PNG encode/decode (adaptive filtering, palette, grey, 1–16 bit, tRNS), BMP and the compositing engine are implemented in this repo on top of
node:zlib; only JPEG leans on the pure-JSjpeg-js. That is what makes "clone → install → build" work on any OS with no toolchain.One registry, many faces. Every operation is declared once (
src/core/ops.ts) with a Zod schema, an apply function and metadata. Individual tools, thepaint_opsbatch runner,paint_help, the SVG exporter and the test suite all read from that single source.Anti-aliasing without a rasteriser library. Fills use sub-row scanline coverage; strokes measure per-pixel distance to each segment capsule, so joins and round caps are smooth for free. Semi-transparent geometry renders into a scratch tile first, so overlapping spans never double-darken.
Undo that scales. A step snapshots only the layers it touches, not the whole document.
Errors teach. Bad input returns the failing field, the valid alternatives and the name of the tool that fixes it —
paint_helpis always one call away.
Limits worth knowing
Text uses a built-in 5×7 pixel font (ASCII; other scripts are transliterated). It suits pixel art and labels; for lettering, draw
paint_pathoutlines instead. Adding a TTF rasteriser is the obvious next step.Interlaced PNGs and RLE-compressed BMPs are rejected with a clear message.
Everything is in-memory per process: canvases survive
save/open, not a server restart.
Development
npm install
npm run build # tsc → dist/
npm test # 20 tests: engine + real stdio MCP handshake, drawing, undo, export, error paths
npm run demo # renders canvases/demo/*.png through the op registryLayout: src/core (engine: colour, raster, paths, filters, codecs, document, ops), src/util
(config, preview, live server, tool registration), src/index.ts (MCP entry), test, scripts.
Releasing to npm: store an Automation token once with gh secret set NPM_TOKEN, then
git tag v1.0.0 && git push --tags — the release workflow builds, runs the tests and publishes
paint-mcp-server.
License
MIT — see LICENSE.
Available Tools
25 toolspaint_brushPaint: Freehand brush strokeA
Mouse-drag feel: soft stamps along a polyline. Each point may carry a third value 0..1 as pressure. flatten+angle make a calligraphy nib, jitter a hand wobble, low hardness a spray.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: color (required), canvas (required), points (required), seed, +11 more.
paint_canvasPaint: canvas lifecycleAIdempotent
Create, list, inspect, open, save or close canvases.
Actions: create {name, width, height, background} new canvas; background is a colour or null for transparent. Layers start as ["background"? , "layer N"]. info {name} size, layers, active index, undo depth, pixel statistics list {} every open canvas open {path} load a .paint / png / jpg / bmp file from the paint root save {name, path?} write the layered .paint document close {name} drop it from memory (files stay on disk) files {} what is on disk in the paint root history {name} undo/redo stack labels
A canvas must be created (or opened) before any drawing tool can be used.
Inputs: action (required), path, width, canvas, +3 more.
paint_clearPaint: Clear or erase an areaADestructive
Wipes a rectangle (whole layer when area omitted) to color; color:null erases to transparency. shape:'ellipse' and radius round the mask.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: canvas (required), area, color, layer, +5 more.
paint_ellipsePaint: Ellipse, circle or arcA
Circle (radius) or ellipse (rx,ry). start_angle/end_angle in degrees cut an arc; pie:true closes it to the centre; rotation tilts the shape.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: cx (required), cy (required), canvas (required), rx, +14 more.
paint_exportPaint: export a fileAIdempotent
Write the canvas out: png, jpg, bmp, svg (rebuilt from recorded vector operations) or .paint (layered, re-openable). Returns the path under the paint root plus a preview of exactly what was written.
svg keeps rect/ellipse/line/polygon/path/text/gradient operations; raster-only steps (brush, fill, pixels, filters, stamps) cannot be vectorised and are reported. Set embed_raster_fallback to also place the current PNG behind them.
Inputs: canvas (required), path, format, flatten, +4 more.
paint_fillPaint: Bucket fill / recolourA
Paint bucket. tolerance is summed channel distance (0 = exact, 60 ≈ shades of the same hue). contiguous:false hits every matching pixel of the layer; mode:'replace' swaps the colour instead of painting over it.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: x (required), y (required), color (required), canvas (required), +8 more.
paint_filterPaint: Image filterA
Filters a layer: blur, sharpen, edge_detect, brightness, contrast, gamma, saturation, hue_rotate, grayscale, sepia, invert, threshold, posterize, pixelate, noise, vignette, chroma_shift, dither. amount is per filter (blur px, degrees, +/- levels, multiplier); area limits it; level = posterize/dither levels.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: name (required), canvas (required), area, seed, +5 more.
paint_gradientPaint: Gradient washA
Linear or radial gradient over the canvas or an area — skies, glows, metal, shadows.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: canvas (required), colors (required), to, area, +10 more.
paint_helpPaint: reference cardARead-onlyIdempotent
In-server documentation, no round trip to GitHub. Topics: ops (every operation and its fields), colors (formats and the named palette), blend, filters, svg, limits, workflow (how to plan a drawing), examples (copy-paste batches).
Inputs: topic.
paint_importPaint: import an imageA
Bring a picture in: path for a file under the paint root (png/jpg/bmp, real format is sniffed from the bytes), or data with base64 when the agent already holds the bytes. Creates a canvas, or adds a layer when the target canvas exists.
Inputs: data, path, canvas, preview, +1 more.
paint_inspectPaint: read pixelsARead-onlyIdempotent
Ask exact questions about the picture: colour at points, statistics and bounding box of painted pixels, or a colour histogram of a region. Cheaper than a thumbnail when you only need numbers.
Inputs: canvas (required), top, layer, points, +1 more.
paint_layerPaint: Manage layersA
list | add | remove | select | move | merge | set (rename, opacity, blend, visible). Layers stack bottom→top; index 0 is the bottom one.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: action (required), canvas (required), to, name, +7 more.
paint_linePaint: Straight lineB
One segment, with optional dashes ([8,6]) and arrow head. hardness 0 gives an airbrush edge. For rules, grids and outlines.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: x1 (required), x2 (required), y1 (required), y2 (required), +14 more.
paint_opsPaint: batch of operationsA
Run many drawing operations in ONE call and get ONE picture back — the cheapest way to build a whole scene.
ops is a list of {op: "", ...that op's parameters}. Available ops: rect, ellipse, line, polygon, path, brush, text, pixel_grid, pixels, fill, gradient, stamp, clear, filter, transform, layer. Every op also exists as its own tool (paint_rect, paint_brush, …) with the same fields. Optional per-item "layer" picks the layer; add layers first with paint_layer or inline via {op:"layer", action:"add"}.
Example: ops: [ {op:"gradient", type:"linear", colors:["#0b1d3a","#3b6fb5"]}, {op:"ellipse", cx:600, cy:120, radius:46, fill:"#ffe27a"}, {op:"path", d:"M0 300 L120 220 L260 300 Z", fill:"#233"}, {op:"text", text:"NIGHT RUN", x:320, y:40, scale:6, color:"#fff"} ]
Returns a summary line per operation plus one preview of the result. Undo rolls back the whole batch step by step.
Inputs: ops (required), canvas (required), preview, preview_size, +1 more.
paint_pathPaint: SVG path dataB
Draw SVG path d data (M L H V C S Q T A Z, lowercase = relative) — the compact way for an agent to place curved or complex art. Supports fill, stroke, dashes, even-odd.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: d (required), canvas (required), cap, fill, +11 more.
paint_pixel_gridPaint: Pixel-art sprite from text rowsA
Draws a sprite where each character of rows looks up a colour in palette (space and . are skipped) and fills a cellxcell block, starting at origin. Perfect for sprites, tiles, icons and logos.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: rows (required), canvas (required), palette (required), cell, +6 more.
paint_pixelsPaint: Write single pixelsA
Exact pixel writes (no blending): pixels=[{x,y,color}, {x,y,width,height,color}, or [x,y,0xRRGGBB]]. Use for pixel art and small fixes.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: canvas (required), pixels (required), layer, preview, +1 more.
paint_polygonPaint: Polygon or polylineB
Any many-sided shape from [x,y] points — stars, arrows, speech bubbles. closed:false strokes the outline only.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: canvas (required), points (required), fill, blend, +8 more.
paint_previewPaint: look at the canvasARead-onlyIdempotent
Return the current picture without changing anything — use it to check your work, compare against a reference, or re-read a canvas after a long session. Set layer:true to see only the active layer.
Inputs: canvas (required), layer, region, preview, +1 more.
paint_rectPaint: RectangleB
Filled and/or outlined rectangle. radius rounds the corners (number, or [top-left, top-right, bottom-right, bottom-left]). fill/stroke take a colour or a gradient object.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: x (required), y (required), width (required), canvas (required), +10 more.
paint_servePaint: live preview pageAIdempotent
Start/stop a localhost page that shows every open canvas and repaints once a second, so a human can watch the drawing happen. Only 127.0.0.1 is bound. action:'status' reports the URL.
Inputs: port, action.
paint_stampPaint: Stamp an imageA
Places another canvas or an image file onto this layer. source is a canvas name or a path under the paint root (png/jpg/bmp). Supports scale, rotate, opacity, blend, region crop and tile.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: canvas (required), source (required), x, y, +9 more.
paint_textPaint: Text labelA
Writes text with the built-in 5x7 pixel font (ASCII). scale multiplies the pixel size, align/valign anchor around (x,y), max_width wraps. Non-ASCII is transliterated (or errors) — no font files needed.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: x (required), y (required), text (required), canvas (required), +15 more.
paint_transformPaint: Transform a layer or the canvasADestructive
scale | rotate | flip | translate on one layer; crop | resize | trim on the whole canvas. Bilinear resampling; trim shrinks to painted content.
Target the canvas by name (created with paint_canvas) and optionally a layer index. Layers, filters and undo work the same for every drawing tool.
Inputs: mode (required), canvas (required), x, y, +10 more.
paint_undoPaint: undo / redoADestructive
Roll a canvas back or forward. Each drawing operation is one step (a paint_ops batch rolls back one step per op). Use action:'steps' to see the stack without changing anything.
Inputs: canvas (required), steps, action, preview.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
TDQS
Scored across 25 tools
Each tool targets a clearly distinct drawing operation or canvas/layer management function. paint_ops batches existing ops but is explicitly differentiated as a batch tool. Minor overlaps like paint_pixels vs paint_pixel_grid are resolved by descriptions.
All tools use the paint_ prefix with consistent snake_case verb/noun naming. No deviations or mixed conventions.
25 tools is heavy for a drawing server, exceeding the typical 3-15 range. While each drawing primitive is distinct, paint_ops already batches all operations, making many individual tools potentially redundant.
Covers canvas lifecycle, layers, drawing primitives, filters, transforms, undo, import/export, preview, and help. No obvious gaps for a 2D drawing domain.
Maintenance
Related MCP Connectors
214 tools for AI agents over one MCP endpoint: image, video, audio, 3D, data, storage, mail, GPUs.
Live, editable canvas for AI agents — author graphs via MCP, then watch and reroute runs live.
Generate images from Claude, ChatGPT, Cursor and Codex with 12 top AI image models and 10 tools: up to 4 images per call, SVG vectorization, editing and more. Remote MCP with OAuth, no API keys. Pay per image, no subscription.
OCR, transcription, file extraction, and image generation for AI agents via MCP.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceExposes a Pygame-based drawing canvas as an MCP server, allowing LLMs to create digital art using standard shapes and freehand paths. It features a specialized oil paint mode that simulates realistic color mixing, paint depletion, and textured brush strokes.-
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to drive a layered raster image editor with painting, shapes, text, filters, and internet-sourced images and fonts via MCP tool calls, with token-efficient rendering and export.MIT
- AlicenseNot gradedqualityBmaintenanceEnables LLM-driven painting agents to control Krita through MCP tools, iteratively reconstructing a target image by observing canvas snapshots, diff heatmaps, and session state. It exposes observation, drawing, brush, color, layer, selection, vector, filter, document I/O, and session tools for closed-loop artwork generation.MIT
- AlicenseAqualityBmaintenanceLocal vector and raster editing engine for agents: layered documents, saved sessions and exports, as a Rust library, JSON CLI and MCP stdio server.221MIT