app-dev/obsidian-excalidraw/SKILL.md

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

  1. MCP server registered — the active harness must have an excalidraw MCP entry. Check with claude mcp list or codex mcp list. Registering the server does not start the canvas; that is the separate process below.
  2. 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 with ECONNREFUSED.

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:

  1. Clear the canvas if you're starting fresh — mcp__excalidraw__clear_canvas (or delete all elements).
  2. Buildmcp__excalidraw__batch_create_elements for bulk, or create_element one at a time. Rectangles, text, arrows, lines, ellipses, diamonds, freedraw.
  3. Tidyalign_elements, distribute_elements, group_elements.
  4. Verifymcp__excalidraw__get_canvas_screenshot returns a PNG of the live canvas. Read it back to see what you drew, then iterate.
  5. Checkpointsnapshot_scene before risky edits, restore_snapshot to 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.md first).

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.md and 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

SymptomCauseFix
mcp__excalidraw__* tools don't appearClaude Code not reloaded since MCP registrationReload Claude Code
Tool call errors ECONNREFUSED or fetch failedCanvas process not runningcanvas-ctl.sh start
Canvas up but tool says "server not ready"Canvas still bootingwait ~1 s and retry
Obsidian shows "Excalidraw ran into an unknown problem"Text Elements section corruptedregenerate file from canvas
Text labels overlap / wall of text in one boxSame as above (or semantic element IDs)regenerate
Screenshot shows empty canvasreset_scene was called / different sessionre-import or rebuild
Boxes / arrows render with no text labels in ObsidianMCP batch_create_elements stored labels as element.label = {text} (non-standard); raw export_scene doesn't convert themRe-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), boundElements bindings, appState fields. Useful when debugging malformed output from the MCP server or writing the wrapper.