Excalidraw MCP Diagram Agent
by dev48v
README.md
# Excalidraw MCP Diagram Agent
An MCP server that turns a sentence about a system into a real, editable Excalidraw file.
Not a picture of a diagram — a `.excalidraw` document you can open at [excalidraw.com](https://excalidraw.com), drag a box in, and watch the arrows follow. Arrows are genuinely bound to the shapes they connect, labels are genuinely bound to their containers, and shapes never overlap.

That picture was generated from this, with no model involved:
```
A React storefront calls the API gateway. The API gateway authenticates with Auth0
and forwards requests to the orders service. The orders service queries the Postgres
database, caches in Redis and publishes to Kafka. A billing worker consumes from
Kafka, calls Stripe and writes to the Postgres database.
```
The scene it produced is in [`docs/example-checkout.excalidraw`](docs/example-checkout.excalidraw) — open it and move something.
## The thing that is actually hard
Emitting JSON that Excalidraw accepts is easy. Excalidraw's `restore()` runs on every import and quietly patches whatever you left out, so a half-correct file still opens and looks fine. You find out it was wrong later, when someone drags a box and every arrow stays behind.
Three failures hide behind a valid-looking file:
1. **One-sided bindings.** An arrow names its shapes in `startBinding`/`endBinding`, but the shapes must name the arrow back in `boundElements`. Miss the back-reference and the file opens perfectly and stops behaving like a diagram the moment you move anything.
2. **Floating arrows.** Excalidraw decides an arrow *is* bound from the binding records, but it *draws* the shaft from the element's own `points`. Both have to agree, and the points have to land on the real outline of the shape — which is a different equation for a rectangle, an ellipse and a diamond.
3. **Boxes on top of boxes.** Schema-valid JSON that renders as an unreadable pile is still a failed diagram.
So the validator here checks three separate things, and only the third catches the failure that matters most: bindings that are perfect on paper attached to arrows that visibly float.
## How it works
```
description ──► graph (nodes + edges) ──► layout ──► scene ──► validate
▲ ▲
rule parser language model
```
Everything upstream of the graph is interchangeable; everything downstream is deterministic Python. The model — when there is one — never sees Excalidraw's schema and never produces a coordinate. It emits a tiny JSON graph and nothing else. That split is the whole design: the part that must be exactly right is kept out of the hands of the part that is only usually right, and a model failure degrades to the rule parser instead of to a broken file.
The layout is a small Sugiyama-style layered pass: break cycles, assign layers by longest path, insert dummy nodes so a long edge reserves a corridor instead of ploughing through the boxes between its ends, order within layers by barycentre sweeps to cut crossings, then assign coordinates. Non-overlap is structural rather than checked afterwards — layers occupy disjoint bands and siblings are stacked with a fixed gap — and the validator asserts it anyway.
## Install
Python 3.10+.
```bash
git clone https://github.com/dev48v/excalidraw-mcp-agent
cd excalidraw-mcp-agent
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
```
## Quickstart
The CLI runs the same pipeline the MCP server does:
```bash
excalidraw-diagram --offline -t "Checkout" -o checkout.excalidraw \
"A React storefront calls the API gateway, which queries Postgres and publishes to Kafka."
```
```
[rules] 4 nodes, 3 edges, 15 elements, 6 bindings -> checkout.excalidraw
```
Open `checkout.excalidraw` at excalidraw.com. Useful flags:
| Flag | Effect |
| --- | --- |
| `--offline` | never call a model; rule parser only, fully deterministic |
| `--require-model` | fail rather than silently downgrading to the rule parser |
| `-d down` | flow top-to-bottom instead of left-to-right |
| `--stdin` | read the description from a pipe |
Arrow syntax works too, and mixes freely with prose:
```bash
excalidraw-diagram --offline "Browser -> CDN -> Load Balancer -> App Server: HTTPS"
```
### Optional: the model backend
The rule parser is a first-class path, not a fallback — the renderer cannot tell which front end produced the graph it is drawing. But a model handles phrasing the verb table does not.
```bash
cp .env.example .env
# NVIDIA_API_KEY=nvapi-... free tier at https://build.nvidia.com
```
Any OpenAI-compatible endpoint works — Ollama, vLLM, OpenRouter — via `EXCALIDRAW_MCP_BASE_URL` and `EXCALIDRAW_MCP_MODEL`. With no key set, everything still runs; it just uses the rules.
## Wire it into an MCP client
The server speaks stdio.
**Claude Code**
```bash
claude mcp add excalidraw -- /abs/path/to/.venv/bin/excalidraw-mcp-agent
```
**Claude Desktop** — add to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`):
```json
{
"mcpServers": {
"excalidraw": {
"command": "/abs/path/to/.venv/bin/excalidraw-mcp-agent",
"env": { "NVIDIA_API_KEY": "" }
}
}
}
```
On Windows the command is `C:\\path\\to\\.venv\\Scripts\\excalidraw-mcp-agent.exe`. Use an absolute path: MCP clients do not inherit your shell's `PATH`.
Then ask for a diagram in plain English:
> Draw me the checkout flow: a React storefront calls the API gateway, which authenticates with Auth0 and forwards to the orders service; the orders service queries Postgres, caches in Redis and publishes to Kafka. Save it to ~/checkout.excalidraw
### Tools
| Tool | Does |
| --- | --- |
| `create_diagram` | description → complete scene. Optional `title`, `direction`, `save_path`, and `use_model` (`auto` / `never` / `always`). |
| `validate_diagram` | checks a scene you already have — JSON string or a path — for schema errors, one-sided bindings, floating arrows and overlaps. |
| `diagram_backend_status` | says whether a model is configured or the run will be offline, so a downgrade is distinguishable from a deliberate offline run. |
`create_diagram` returns the scene *and* a summary, so the client can see what it got:
```json
{
"title": "Order Checkout Flow",
"backend": "rules",
"nodes": 9, "edges": 9,
"elements": 37, "shapes": 9, "arrows": 9, "labels": 19,
"bindings": 18,
"valid": true, "errors": [], "warnings": [], "notes": []
}
```
Nine arrows, eighteen bindings — every arrow attached at both ends.
## Node kinds
The kind decides the shape and the colour, and is inferred from the label when no model supplies one.
| Kind | Shape | Inferred from |
| --- | --- | --- |
| `client` | violet rectangle | browser, frontend, mobile, SPA, dashboard |
| `service` | blue rectangle | api, server, worker, lambda, gateway *(the default)* |
| `database` | green ellipse | postgres, mysql, mongo, dynamo, rds |
| `store` | green rectangle | redis, cache, s3, bucket, elasticsearch |
| `queue` | dashed orange rectangle | kafka, sqs, topic, stream, pubsub |
| `external` | dashed grey rectangle | stripe, twilio, third-party, webhook |
| `decision` | orange diamond | router, switch, branch, condition |
## Verifying it for yourself
`pytest` covers the graph, the parser, the layout, the geometry, the scene builder and the validator:
```bash
pytest
# 114 passed
```
Those are Python assertions about Python objects, which is exactly the trap this project is about — so there is a second harness that renders the output with Excalidraw's own engine. It needs Node and Chrome and a 14 MB bundle, so it is not part of the package or the test suite:
```bash
cd tools/render
npm install
npx esbuild entry.mjs --bundle --outfile=bundle.js --format=iife \
--define:process.env.NODE_ENV=\"production\" --loader:.woff2=dataurl
cd ../..
python tools/render/verify.py docs/example-checkout.excalidraw
```
```
[PASS] example-checkout.excalidraw
svg : 2260 x 953.7184460878227
drawn nodes: 129 (20 text)
restore() : 0 element(s) changed
screenshot : tools/render/out/example-checkout.png
```
It does two things a schema check cannot. It draws the scene with the real `exportToSvg`, so a malformed element shows up as a broken or empty picture rather than a passing assertion. And it runs the file through Excalidraw's own `restore()` and diffs the result against what we generated — `restore()` being the function that silently repairs imported files. **Zero elements changed** means the file needed no repair at all.
It also writes a PNG, cropped to the drawing, which is the only way to catch the failure no assertion describes: a diagram that is correct and unreadable.
## Honest limits
- **The rule parser is a verb table, not language understanding.** It knows about fifty verb phrases and the `A -> B` form. Sentences it cannot parse are dropped rather than guessed at, so odd phrasing costs you an edge, never a wrong diagram — but it does cost you the edge.
- **The model summarises.** Asked to draw a worker that "fetches the file from S3 and writes a thumbnail back to S3", an 8B model returned one edge, not two. `--require-model` gets you the model's reading of your description, not a transcription of it.
- **Crossings are reduced, not eliminated.** Barycentre ordering is a heuristic. On the example above, `billing worker → Kafka` still crosses `orders service → Postgres`. Non-overlap of *boxes* is guaranteed; non-crossing of *arrows* is not, and cannot be in general.
- **Text is measured by estimate.** Excalidraw measures with real font metrics in a browser; this has neither the fonts nor a canvas, so widths are a deliberate slight over-estimate. Boxes are a few pixels wider than they need to be, which beats a box that clips its own label.
- **Arrow labels may overhang their arrows.** Given the choice between a label wider than its edge and hard-splitting `authenticates` into `authenticate` + `s with`, this picks the overhang.
- **Self-loops are dropped.** An edge from a node to itself needs routing this layout does not do, so it is skipped rather than drawn as junk.
- **Layered layout suits pipelines and architectures.** A dense mesh with no clear direction will lay out legally and read poorly.
- **Two directions, seven kinds, one style.** No groups, frames, images, freedraw or nested containers. Node labels are capped at five words.
- **The MCP server is built against `mcp` 2.x**, where the ergonomic class is `mcp.server.mcpserver.MCPServer` and `FastMCP` is gone. Most tutorials you will find describe the 1.x API.
## Licence
MIT © Devanshu Biswas. See [LICENSE](LICENSE).
Project #1 of the Weekend Builds series.
TDQS
A4.1/5.0
Scored across 3 tools
Disambiguation5/5
Each tool serves a distinct purpose: creating diagrams, validating diagrams, and checking backend status. There is no overlap or ambiguity between them.
Naming Consistency4/5
Two tools follow the verb_noun pattern (create_diagram, validate_diagram), while diagram_backend_status is more of a noun phrase. All use snake_case consistently, and the names are clear and descriptive.
Tool Count5/5
With 3 tools, the server is well-scoped for the domain of diagram creation and validation. Each tool is essential and there is no bloat.
Completeness4/5
The core workflow of creating and validating diagrams is covered. Minor gaps exist such as no explicit update/delete tool for existing diagrams, but the 'editable scene' output and file writing mitigate this.
Maintenance
ActivityStale
ResponsivenessNo issues