name: obsidian-excalidraw
description: Create, edit, or debug Excalidraw diagrams inside an Obsidian vault. Drive the mcp-excalidraw-server MCP tools to draw on a live canvas, then wrap the exported scene into the .excalidraw.md format Obsidian's plugin expects. Handles canvas-process lifecycle, the Obsidian wrapper (frontmatter + # Drawing), and the Text-Elements-corruption gotcha. Trigger when the user asks to create, edit, fix, or embed an Excalidraw diagram; when reporting rendering issues with .excalidraw.md files; or when planning visual diagrams in an Obsidian knowledge base.
requires_mcp:
- excalidraw
Obsidian Excalidraw
Build diagrams interactively via the mcp-excalidraw-server MCP tools, export the scene, then wrap it into the .excalidraw.md format Obsidian's plugin reads.
For drawing craft itself — layout anti-patterns, arrow routing, MCP-vs-REST format quirks, quality checklist, snapshots, mermaid conversion — load the companion
excalidraw-skill. This skill focuses only on the Obsidian wrapper and canvas-process plumbing.
Prerequisites
- MCP server registered — the active harness must have an
excalidrawMCP entry. Check withclaude mcp listorcodex mcp list. Registering the server does not start the canvas; that is the separate process below. - Canvas process running — the MCP server is stdio-launched by Claude Code but proxies to a separate canvas process on
http://127.0.0.1:3333/. Without it, every MCP call fails withECONNREFUSED.
Start the canvas before drawing
~/.claude/skills/obsidian-excalidraw/canvas-ctl.sh status # is it up?
~/.claude/skills/obsidian-excalidraw/canvas-ctl.sh start # if not
~/.claude/skills/obsidian-excalidraw/canvas-ctl.sh logs # tail log
~/.claude/skills/obsidian-excalidraw/canvas-ctl.sh stop # kill it
Live view (handy for a human to watch): open http://127.0.0.1:3333/ in a browser.
The canvas is stateful, in-memory. Everything you draw persists until the process dies. Export early and often; treat the canvas as a scratchpad, the .excalidraw.md file as the artifact of record.
The drawing loop
Use the MCP tools to build the scene iteratively, screenshotting to verify:
- Clear the canvas if you're starting fresh —
mcp__excalidraw__clear_canvas(or delete all elements). - Build —
mcp__excalidraw__batch_create_elementsfor bulk, orcreate_elementone at a time. Rectangles, text, arrows, lines, ellipses, diamonds, freedraw. - Tidy —
align_elements,distribute_elements,group_elements. - Verify —
mcp__excalidraw__get_canvas_screenshotreturns a PNG of the live canvas. Read it back to see what you drew, then iterate. - Checkpoint —
snapshot_scenebefore risky edits,restore_snapshotto undo.
Specialty tools worth knowing:
mcp__excalidraw__create_from_mermaid— seed the scene from a mermaid sequence / flow diagram, then refine interactively.mcp__excalidraw__describe_scene— text summary of the current canvas (useful when an LLM needs to reason about what's there without a screenshot).mcp__excalidraw__read_diagram_guide— built-in layout best-practices.mcp__excalidraw__import_scene— load existing Excalidraw JSON into the canvas (e.g., to edit a previously-saved file — export the raw JSON from the.excalidraw.mdfirst).
Save to Obsidian
export_scene emits raw Excalidraw JSON — not Obsidian-compatible. Wrap it:
# 1. Export from the canvas to a temp file
mcp__excalidraw__export_scene filePath=/tmp/scene.json
# 2. Wrap into Obsidian's .excalidraw.md format
node ~/.claude/skills/obsidian-excalidraw/wrap-for-obsidian.mjs \
/tmp/scene.json \
"/path/to/Vault/Diagrams/My Diagram.excalidraw.md"
The wrapper accepts (a) a raw scene JSON file path, (b) - for stdin, or (c) the MCP tool-response envelope ({content:[{text:"<scene json>"}]}). It prepends the frontmatter + warning line, wraps the JSON in a # Drawing code block, and appends the closing %%.
The wrapper also normalizes MCP's non-standard label: {text} shorthand (see the format gotcha below) into proper container-bound text elements — without this, box/arrow labels render blank in Obsidian.
Do not hand-author this wrapper format — use the script.
The #1 format gotcha
Never hand-author a # Text Elements section.
After first load, the Obsidian plugin auto-generates a ## Text Elements section listing every text-type element with ^id anchors matching each text element's id. If you hand-write that section with mismatched anchors, the parser concatenates adjacent entries and stuffs the result into whichever element it can match — your "Studio DynamoDB" title becomes "Studio DynamoDB • accounts • ..." and an "Excalidraw ran into an unknown problem!" dialog appears.
The wrapper script does not emit a Text Elements section. Keep it that way. If you edit a .excalidraw.md file by hand and see a Text Elements section, leave it alone — the plugin owns it.
Related: element IDs must look random (aB3xK9mZ-style, 8-char base62), not semantic (studio, node-1). Short predictable IDs collide with the plugin's ^anchor parser. The MCP canvas already generates random IDs, so this only matters if you hand-edit.
Verification recipe
After the wrapper writes the file:
# 1. Open in Obsidian
obsidian open path="Diagrams/My Diagram.excalidraw.md"
# 2. Confirm the plugin recognized it
obsidian eval code='(() => { const p = app.plugins.getPlugin("obsidian-excalidraw-plugin"); const f = app.vault.getAbstractFileByPath("Diagrams/My Diagram.excalidraw.md"); return JSON.stringify({isExcalidraw: p.isExcalidrawFile(f)}); })()'
# 3. Visual check
obsidian dev:screenshot path=/tmp/check.png
Read the PNG back to confirm layout is what you expected. If the MCP canvas screenshot and the Obsidian render disagree, the wrapper is at fault — file a bug, don't hand-patch.
Compressed vs parsed mode
On first save after a user opens the file in Obsidian, the plugin rewrites your plain-JSON # Drawing block into # Excalidraw Data with a ## Drawing block containing compressed-json (LZ-String). That's expected. Do not re-author over compressed files by hand. To regenerate, either:
- Delete the
.excalidraw.mdand re-export from the MCP canvas, or - Edit visually in Obsidian and let the plugin save.
If you need to programmatically edit a compressed file: mcp__excalidraw__import_scene can't read the compressed wrapper directly — you'd need to decompress via the Obsidian plugin's API (obsidian eval code='…ea.getSceneFromFile(f)…') or re-open in parsed mode first.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
mcp__excalidraw__* tools don't appear | Claude Code not reloaded since MCP registration | Reload Claude Code |
Tool call errors ECONNREFUSED or fetch failed | Canvas process not running | canvas-ctl.sh start |
| Canvas up but tool says "server not ready" | Canvas still booting | wait ~1 s and retry |
| Obsidian shows "Excalidraw ran into an unknown problem" | Text Elements section corrupted | regenerate file from canvas |
| Text labels overlap / wall of text in one box | Same as above (or semantic element IDs) | regenerate |
| Screenshot shows empty canvas | reset_scene was called / different session | re-import or rebuild |
| Boxes / arrows render with no text labels in Obsidian | MCP batch_create_elements stored labels as element.label = {text} (non-standard); raw export_scene doesn't convert them | Re-run wrap-for-obsidian.mjs — it normalizes label into proper container-bound text elements |
References
- references/format-gotchas.md — deeper format reference: wrapper structure, element shape (rectangle, text, arrow),
boundElementsbindings,appStatefields. Useful when debugging malformed output from the MCP server or writing the wrapper.
