Programmatic canvas toolkit for creating, editing, and refining Excalidraw diagrams via MCP tools with real-time canvas sync. Use when an agent needs to (1) draw or lay out diagrams on a live canvas, (2) iteratively refine diagrams using describe_scene and get_canvas_screenshot to see its own work, (3) export/import .excalidraw files or PNG/SVG images, (4) save/restore canvas snapshots, (5) convert Mermaid to Excalidraw, or (6) perform element-level CRUD, alignment, distribution, grouping, duplication, and locking. Requires a running canvas server (EXPRESS_SERVER_URL, default http://localhost:3000).
Before doing anything, determine which mode is available. Run these checks in order:
mcp-cli tools | grep excalidraw
If you see tools like excalidraw/batch_create_elements → use MCP mode. Call MCP tools directly.
curl -s http://localhost:3000/health
If you get {"status":"ok"} → use REST API mode. Use HTTP endpoints (curl / fetch) from the cheatsheet.
If neither works, tell the user:
The Excalidraw canvas server is not running. To set up:
- Clone:
git clone https://github.com/yctimlin/mcp_excalidraw && cd mcp_excalidraw- Build:
npm ci && npm run build- Start canvas:
HOST=0.0.0.0 PORT=3000 npm run canvas- Open
http://localhost:3000in a browser- (Recommended) Install the MCP server for the best experience:
claude mcp add excalidraw -s user -e EXPRESS_SERVER_URL=http://localhost:3000 -- node /path/to/mcp_excalidraw/dist/index.js
| Operation | MCP Tool | REST API Equivalent |
|---|---|---|
| Create elements | batch_create_elements | POST /api/elements/batch with {"elements": [...]} |
| Get all elements | query_elements | GET /api/elements |
| Get one element | get_element | GET /api/elements/:id |
| Update element | update_element | PUT /api/elements/:id |
| Delete element | delete_element | DELETE /api/elements/:id |
| Clear canvas | clear_canvas | DELETE /api/elements/clear |
| Describe scene | describe_scene | GET /api/elements (parse manually) |
| Export scene | export_scene | GET /api/elements (save to file) |
| Import scene | import_scene | POST /api/elements/sync with {"elements": [...]} |
| Snapshot | snapshot_scene | POST /api/snapshots with {"name": "..."} |
| Restore snapshot | restore_snapshot | GET /api/snapshots/:name then POST /api/elements/sync |
| Screenshot | get_canvas_screenshot | Only via MCP (needs browser) |
| Design guide | read_diagram_guide | Not available — see cheatsheet for guidelines |
| Viewport | set_viewport | POST /api/viewport (needs browser) |
| Export image | export_to_image | POST /api/export/image (needs browser) |
| Export URL | export_to_excalidraw_url | Only via MCP |
"label": {"text": "My Label"} (not "text": "My Label"). MCP tools auto-convert, REST API does not."start": {"id": "svc-a"}, "end": {"id": "svc-b"} (not "startElementId"/"endElementId"). MCP tools accept startElementId and convert, REST API requires the start/end object format directly."1") or omit it entirely. Do NOT pass a number like 1.PUT /api/elements/:id, include the full label in the update body to preserve it. Omitting label from the update won't delete it, but re-sending ensures it renders correctly.POST /api/export/image returns {"data": "<base64>"}. Save to file and read it back for visual verification. Requires browser open.After EVERY iteration (each batch of elements added), you MUST run a quality check before proceeding. NEVER say "looks great" unless ALL checks pass.
width and/or height.max(160, labelTextLength * 9) pixels. For multi-word labels like "API Gateway (Kong)", count all characters.Before creating elements, plan your coordinate grid on paper first:
Do NOT place side panels (observability, external APIs) at the same x-range as the main diagram — they WILL overlap.
references/cheatsheet.md.read_diagram_guide first to load design best practices.clear_canvas to start fresh.batch_create_elements with shapes AND arrows in one call.id to shapes (e.g. "id": "auth-svc"). Set text field to label shapes.width: max(160, textLength * 9).startElementId / endElementId — arrows auto-route.set_viewport with scrollToContent: true to auto-fit the diagram.get_canvas_screenshot and critically evaluate. Fix issues before proceeding.references/cheatsheet.md for design guidelines.curl -X DELETE http://localhost:3000/api/elements/clear@file.json for large payloads):
curl -X POST http://localhost:3000/api/elements/batch \
-H "Content-Type: application/json" \
-d '{"elements": [
{"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 160, "height": 60, "label": {"text": "Service A"}},
{"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 160, "height": 60, "label": {"text": "Service B"}},
{"type": "arrow", "x": 0, "y": 0, "start": {"id": "svc-a"}, "end": {"id": "svc-b"}}
]}'
"label": {"text": "..."} for shape labels (not "text": "...")."start": {"id": "..."} / "end": {"id": "..."} — server auto-routes edges.width: max(160, labelTextLength * 9).Bind arrows to shapes for auto-routed edges. The format differs between MCP and REST API:
MCP Mode — use startElementId / endElementId:
{"elements": [
{"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "text": "Service A"},
{"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 120, "height": 60, "text": "Service B"},
{"type": "arrow", "x": 0, "y": 0, "startElementId": "svc-a", "endElementId": "svc-b", "text": "calls"}
]}
REST API Mode — use start: {id} / end: {id} and label: {text}:
{"elements": [
{"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "label": {"text": "Service A"}},
{"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 120, "height": 60, "label": {"text": "Service B"}},
{"type": "arrow", "x": 0, "y": 0, "start": {"id": "svc-a"}, "end": {"id": "svc-b"}, "label": {"text": "calls"}}
]}
Arrows without binding use manual x, y, points coordinates.
Straight arrows (2-point) cause crossing and overlap in complex diagrams. Use curved or elbowed arrows instead:
Option 1: Curved arrows — add intermediate waypoints + roundness:
{
"type": "arrow", "x": 100, "y": 100,
"points": [[0, 0], [50, -40], [200, 0]],
"roundness": {"type": 2},
"strokeColor": "#1971c2"
}
The waypoint [50, -40] pushes the arrow upward to arc over elements. roundness: {type: 2} makes it a smooth curve.
Option 2: Elbowed arrows — right-angle routing (L-shaped or Z-shaped):
{
"type": "arrow", "x": 100, "y": 100,
"points": [[0, 0], [0, -50], [200, -50], [200, 0]],
"elbowed": true,
"strokeColor": "#1971c2"
}
When to use which:
Rule of thumb: If an arrow would cross through an unrelated element, add a waypoint to route around it. Never accept crossing arrows — always fix them.
The feedback loop that makes this skill unique. Each iteration MUST include a quality check.
batch_create_elements, create_element).set_viewport with scrollToContent: true.get_canvas_screenshot — critically evaluate against the Quality Checklist.update_element, delete_element, resize, reposition).get_canvas_screenshot again — re-verify fix.POST /api/elements/batch.POST /api/viewport with {"scrollToContent": true}.POST /api/export/image → save PNG → critically evaluate against Quality Checklist.PUT /api/elements/:id or delete and recreate.Example flow (MCP):
batch_create_elements → get_canvas_screenshot → "text truncated on 2 shapes"
→ update_element (increase widths) → get_canvas_screenshot → "overlap between X and Y"
→ update_element (reposition) → get_canvas_screenshot → "all checks pass"
→ proceed to next iteration
describe_scene to understand current state.update_element to move/resize/recolor, delete_element to remove.get_canvas_screenshot to verify changes visually.get_element), element isn't locked (unlock_elements).export_scene with optional filePath.import_scene with mode: "replace" or "merge".export_to_image with format: "png" or "svg" (requires browser open).node scripts/export-elements.cjs --out diagram.elements.jsonnode scripts/import-elements.cjs --in diagram.elements.json --mode batch|syncsnapshot_scene with a name before risky changes.describe_scene / get_canvas_screenshot to evaluate.restore_snapshot to rollback if needed.duplicate_elements with elementIds and optional offsetX/offsetY (default 20,20).The points field accepts both formats:
[[0, 0], [100, 50]][{"x": 0, "y": 0}, {"x": 100, "y": 50}]Both are normalized to tuples automatically.
export_to_excalidraw_url — uploads encrypted scene, returns a shareable URL.set_viewport with scrollToContent: true — auto-fit all elements (zoom-to-fit).set_viewport with scrollToElementId: "my-element" — center view on a specific element.set_viewport with zoom: 1.5, offsetX: 100, offsetY: 200 — manual camera control.references/cheatsheet.md: Complete MCP tool list (26 tools) + REST API endpoints + payload shapes.