# Excalidraw MCP

> Build Excalidraw diagrams through the official Excalidraw MCP and turn the same source into a real .excalidraw file on disk or an Obsidian .excalidraw.md drawing. Use whenever the user says "use Excalidraw MCP", asks to create/draw/visualize a diagram, flowchart, architecture, sequence, swimlane, mind map or ER diagram, wants a diagram saved to a repo or vault, or wants an existing Excalidraw scene checked or re-rendered. Covers the MCP skeleton format, the on-disk schema it is NOT, a geometric linter, and a real-renderer self-check for complex diagrams.

- Skill: `anton-abyzov/excalidraw-mcp` (Agent Skill, multi-file: 14 files)
- Install (CLI): `npx skillmds@latest add anton-abyzov/excalidraw-mcp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/anton-abyzov/excalidraw-mcp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: anton-abyzov (https://skillmd.com/u/anton-abyzov)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/anton-abyzov/excalidraw-mcp

---


# 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:

```bash
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.

```powershell
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

0. **Preflight** (new machine only): if the `mcp__excalidraw__*` tools are missing,
   `python3 scripts/ensure_mcp.py --install` (`py -3` on Windows).
1. **Author the skeleton.** One JSON array, the same one `create_view` takes. Positions
   come from a layout formula (see `references/layout-recipes.md`), never from eyeballing.
2. **Lint it** — offline, no dependencies, catches overflow/collision/geometry:
   ```bash
   python3 scripts/excalidraw_lint.py scene.json --camera
   ```
   `--camera` prints a correctly framed 4:3 `cameraUpdate` to paste at the top.
3. **Render it live** with `mcp__excalidraw__create_view`, elements streamed in
   z-order with camera moves (see `references/mcp-workflow.md`). Keep the returned
   `checkpointId`.
4. **Save the file** when the user wants one on disk:
   ```bash
   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
   ```
5. **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:
   ```bash
   python3 scripts/excalidraw_render.py out.excalidraw -o /tmp/preview.png
   ```
   Then Read the PNG. Fix what you see; re-lint; re-render.

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

```jsonc
{"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 `.excalidraw` file.
- `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.md` wrapper, 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.py` needs `playwright` plus 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: documented `py -3`, `claude.cmd` resolution, explicit UTF-8 and `\n`
  newlines 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/`, matching
  `remotion-best-practices` and the other in-repo skills. The standalone
  `anton-abyzov/excalidraw-mcp-skill` repo is deprecated.
- **1.0.1** — document MCP setup: installing the skill does not install the server;
  added the `claude mcp add` one-liner and what still works without it.
- **1.0.0** — first release. Replaces the file-only `excalidraw-diagram-generator`
  skill, whose templates emitted inline `text` on shapes and therefore opened as empty
  boxes.

