Excalidraw via MCP
Saying "use Excalidraw MCP" should be enough to get a complicated diagram, drawn live and saved where it belongs. This skill is what makes that true.
The one thing that breaks everything
create_view's element format is a skeleton, not the on-disk schema. A shape's
label is skeleton-only sugar. Excalidraw's canvas renderer has no text branch for
rectangle / ellipse / diamond — container text is drawn only via
boundElements → containerId. So pasting create_view JSON into a .excalidraw file
produces empty boxes, silently. That single fact is why scripts/excalidraw_build.py
exists: author once in the skeleton, get both the live render and a correct file.
Second fact: hand-authored diagrams do not degrade gradually with size — they fail on irregularity. Measured on a real generated scene, elements produced by a repeating layout formula had a 0.00 defect rate; bespoke one-off elements had 0.71. So compute positions with a formula, and let the linter check the result.
Preflight on a new machine
Installing this skill does not install the MCP server — mcp-deps is a declaration
that vskill check verifies, not an installer. If the mcp__excalidraw__* tools are
not available, run this first — it detects and registers the server, on any OS:
python3 scripts/ensure_mcp.py --install
It reports where the server was found, or runs
claude mcp add --transport http --scope user excalidraw https://mcp.excalidraw.com
and re-verifies. Exit 0 configured, 1 missing, 2 could not register (no Claude CLI on
PATH — it prints the command to run by hand). Claude Code needs a restart afterwards to
pick up a newly added server.
Scopes: --scope user (default) covers every project on the machine via ~/.claude.json;
--scope project writes ./.mcp.json so teammates get it on clone; --scope local is
this project only. A server registered under a different project counts as missing —
Claude Code will not load it here, and the script says so.
Without the MCP everything except the live inline render still works.
excalidraw_build.py and excalidraw_lint.py are stdlib-only Python 3 — no packages, no
network. Build files, lint them, ship them. excalidraw_render.py additionally needs
pip install playwright && playwright install chromium.
Windows
Use py -3 and backslashes; python3 on Windows is a Microsoft Store stub that opens
the Store instead of running anything.
py -3 scripts\ensure_mcp.py --install
py -3 scripts\excalidraw_build.py scene.json -o out.excalidraw
Everything else is portable: paths go through pathlib, output is written with explicit
UTF-8 and \n newlines so a Windows run does not bake CRLF into the scene JSON, and the
preflight resolves claude.cmd / claude.exe as well as claude. If vskill i warns
that symlinks are unavailable, it falls back to copying — enable Developer Mode to get
symlinks back.
Workflow
- Preflight (new machine only): if the
mcp__excalidraw__*tools are missing,python3 scripts/ensure_mcp.py --install(py -3on Windows). - Author the skeleton. One JSON array, the same one
create_viewtakes. Positions come from a layout formula (seereferences/layout-recipes.md), never from eyeballing. - Lint it — offline, no dependencies, catches overflow/collision/geometry:
python3 scripts/excalidraw_lint.py scene.json --camera--cameraprints a correctly framed 4:3cameraUpdateto paste at the top. - Render it live with
mcp__excalidraw__create_view, elements streamed in z-order with camera moves (seereferences/mcp-workflow.md). Keep the returnedcheckpointId. - Save the file when the user wants one on disk:
python3 scripts/excalidraw_build.py scene.json -o out.excalidraw python3 scripts/excalidraw_build.py scene.json -o out --obsidian # → out.excalidraw.md python3 scripts/excalidraw_build.py scene.json -o out.excalidraw --dark - Look at it before declaring success on anything non-trivial. This renders through
Excalidraw's own
exportToSvg, so text is measured with real font metrics:
Then Read the PNG. Fix what you see; re-lint; re-render.python3 scripts/excalidraw_render.py out.excalidraw -o /tmp/preview.png
Steps 2 and 5 are the quality gate. Skipping them is how labels end up outside their boxes and arrows end up pointing at nothing.
Skeleton cheat sheet
{"type":"rectangle","id":"api","x":100,"y":80,"width":180,"height":80,
"roundness":{"type":3},"backgroundColor":"#a5d8ff","fillStyle":"solid",
"strokeColor":"#4a9eed","label":{"text":"API Gateway","fontSize":18}}
{"type":"arrow","id":"e1","from":"api","to":"db","label":{"text":"SQL"}} // auto-bound
{"type":"arrow","id":"e2","from":"api","to":"db","route":"ortho"} // L-shaped
{"type":"arrow","id":"e3","x":300,"y":150,"points":[[0,0],[120,0]]} // manual
{"type":"text","id":"ttl","center":400,"y":20,"text":"Title","fontSize":24} // auto-centred
{"type":"rectangle","id":"zone","x":40,"y":40,"width":700,"height":420,
"opacity":30,"zoneLabel":"Data layer"} // caption, not bound label
Builder-only extensions: from/to (perimeter anchors + two-way binding),
route:"ortho", zoneLabel, center on text, fixedWidth on a shape (wrap instead
of grow). Everything else is passed through unchanged, so the same array still works
verbatim with create_view.
Never put a bound label on a background zone rectangle — it centres in the middle
of the zone and cannot be grabbed. Use zoneLabel.
Choosing the output
| Destination | Command | Notes |
|---|---|---|
| Show the user now | create_view |
animated, camera-guided; returns checkpointId |
| File in a repo | excalidraw_build.py -o x.excalidraw |
opens at excalidraw.com or in the VS Code extension |
| Obsidian vault | ... -o x --obsidian |
.excalidraw.md, plugin-parsed, git-diffable |
| Shareable link | mcp__excalidraw__export_to_excalidraw |
uploads to excalidraw.com — ask first |
The MCP has no local-file tool. export_to_excalidraw is a public upload, so treat it
as publishing: confirm with the user before calling it.
Iterating
create_view returns a checkpointId. To continue from it — including user edits made
in fullscreen — start the next array with
{"type":"restoreCheckpoint","id":"<checkpointId>"} and append only what is new. Use
{"type":"delete","ids":"a,b"} to remove elements; never reuse a deleted id. Keep the
skeleton file on disk in sync, since that file is what builds and lints.
References
references/file-format.md— the on-disk schema, verified against upstream: font codes, bound text, arrow geometry, bindings, what Excalidraw does and does not recompute on open. Read before hand-editing any.excalidrawfile.references/mcp-workflow.md— camera choreography, streaming order, checkpoints, dark mode, and the MCP's own limits.references/layout-recipes.md— formulas for flow, layered, swimlane, sequence, grid, radial and matrix layouts, plus how to keep a 60-element diagram legible.references/obsidian.md— the.excalidraw.mdwrapper, block-ref rules, and the compressed-scene gotcha.scripts/ensure_mcp.py— detect and register the Excalidraw MCP server on any OS.scripts/split_excalidraw_library.py— split an.excalidrawlib(AWS/GCP/K8s icon packs from libraries.excalidraw.com) into per-icon JSON plus a lookup table, so icon data never enters context.
Limits worth stating out loud
- Width estimates are calibrated per-character, not measured from the font binary; the linter warns inside 10% of overflow. When it warns on something important, render it.
excalidraw_render.pyneedsplaywrightplus network access to esm.sh. Without them, lint is still fully offline.- The linter cannot judge whether a diagram is good, only whether it is correct. Composition is still your job.
Changelog
- 1.2.0 —
scripts/ensure_mcp.py: cross-platform preflight that detects the MCP server (including~/.claude.json's per-project map) and registers it when missing. Windows support: documentedpy -3,claude.cmdresolution, explicit UTF-8 and\nnewlines on every write. - 1.1.1 — linter no longer applies the box-fit rules (R1/R2) to arrow containers; arrow labels are laid along the path and are covered by R10.
- 1.1.0 — moved into the vskill monorepo at
skills/excalidraw-mcp/, matchingremotion-best-practicesand the other in-repo skills. The standaloneanton-abyzov/excalidraw-mcp-skillrepo is deprecated. - 1.0.1 — document MCP setup: installing the skill does not install the server;
added the
claude mcp addone-liner and what still works without it. - 1.0.0 — first release. Replaces the file-only
excalidraw-diagram-generatorskill, whose templates emitted inlinetexton shapes and therefore opened as empty boxes.