Skip to main content
Glama
dtour

excalidraw-mcp

by dtour
README.md
# excalidraw-mcp

A file-based [MCP server](https://modelcontextprotocol.io) that makes Excalidraw diagrams a first-class data type for LLMs. The server owns all spatial reasoning – coordinate math, element bindings, text measurement, overlap detection – so the LLM operates purely at the semantic level of nodes and edges.

Supports both `.excalidraw` (raw JSON) and `.excalidraw.md` (Obsidian plugin format with LZ-String compression).

**Best suited for:** flowcharts, state machines, ER diagrams, architecture diagrams, and other graph-shaped diagrams – rectangles, ellipses, and diamonds connected by arrows.

**Not suited for:** wireframes, freeform sketches, annotated screenshots, or spatial layouts where precise pixel positioning matters more than connectivity. These elements are still readable via `read_diagram` (they appear in the `other` bucket) but cannot be created through the semantic API.

## Install

```bash
bun add excalidraw-mcp
```

Or build from source:

```bash
git clone https://github.com/dtour/excalidraw-mcp.git
cd excalidraw-mcp
bun install && bun run build
```

For PNG rendering support (optional – SVG works out of the box):

```bash
bun add @resvg/resvg-js
```

## Configure

**Claude Code** (`~/.claude/settings.json`):

```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "bunx",
      "args": ["excalidraw-mcp"]
    }
  }
}
```

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "bunx",
      "args": ["excalidraw-mcp"]
    }
  }
}
```

Or point directly to a local build:

```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "node",
      "args": ["/path/to/excalidraw-mcp/dist/index.js"]
    }
  }
}
```

## Tools

### `read_diagram`

Parse any Excalidraw file and return a semantic **GraphSummary**.

```json
{ "path": "./diagram.excalidraw" }
```

Returns nodes, edges, groups, and an `other` bucket for non-graph elements (freehand, images, frames) – everything the LLM needs to reason about the diagram's structure:

```json
{
  "nodes": [
    { "id": "abc123", "type": "rectangle", "text": "API Gateway", "x": 100, "y": 50, "width": 180, "height": 60 }
  ],
  "edges": [
    { "id": "def456", "from": "abc123", "to": "ghi789", "label": "REST" }
  ],
  "groups": [],
  "other": [],
  "boundingBox": { "x": 50, "y": 20, "width": 600, "height": 400 }
}
```

---

### `create_diagram`

Create a new diagram from a declarative spec. The server handles coordinates, layout, bindings, and IDs. Output format is determined by file extension.

```json
{
  "path": "./flowchart.excalidraw",
  "spec": {
    "nodes": [
      { "type": "rectangle", "text": "Start" },
      { "type": "diamond", "text": "Decision?" },
      { "type": "rectangle", "text": "End" }
    ],
    "edges": [
      { "from": "Start", "to": "Decision?" },
      { "from": "Decision?", "to": "End", "label": "yes" }
    ],
    "layout": { "type": "vertical-flow", "spacing": 100 }
  }
}
```

Three layout algorithms are available:

| Layout | Description |
|---|---|
| `vertical-flow` | Nodes stacked top to bottom (default) |
| `horizontal-flow` | Nodes arranged left to right |
| `grid` | N-column grid (set `columns` to configure) |

A **low-level escape hatch** is available for pixel-precise control:

```json
{
  "path": "./precise.excalidraw",
  "spec": {
    "elements": [{ "type": "rectangle", "x": 100, "y": 100, "width": 200, "height": 80 }]
  }
}
```

---

### `modify_diagram`

Apply semantic operations to an existing diagram. All operations are **atomic** – if any fails, no changes are written.

```json
{
  "path": "./flowchart.excalidraw",
  "operations": [
    { "type": "change_text", "target": "Start", "text": "Begin" },
    {
      "type": "add_node",
      "spec": { "type": "rectangle", "text": "Retry" },
      "position": { "type": "relative", "anchor": "End", "direction": "below" }
    },
    { "type": "connect", "from": "End", "to": "Retry" },
    {
      "type": "restyle",
      "target": "Decision?",
      "style": { "backgroundColor": "#ffc9c9", "fillStyle": "solid" }
    }
  ]
}
```

Seven operations:

| Operation | Description |
|---|---|
| `change_text` | Update an element's text content |
| `add_node` | Create a new shape with absolute or relative positioning |
| `remove` | Delete an element and clean up all bindings |
| `connect` | Create an arrow between two elements |
| `disconnect` | Remove an arrow connection |
| `restyle` | Update visual properties (colors, stroke, fill, opacity) |
| `reposition` | Move an element to absolute coordinates or relative to another |

Target elements by **ID** or **text content**. If a text match is ambiguous (multiple elements share the same text), the server returns an error listing all candidates with their IDs so the LLM can disambiguate – it never silently picks the first match.

**Relative positioning** lets you place nodes relative to existing elements:

```json
{ "type": "relative", "anchor": "Error", "direction": "right", "gap": 120 }
```

Directions: `above`, `below`, `left`, `right`. Default gap is 80px.

---

### `render_diagram`

Render to SVG (built-in, no dependencies) or PNG (requires optional `@resvg/resvg-js`).

```json
{ "path": "./flowchart.excalidraw", "format": "svg", "scale": 1.5 }
```

Returns an inline image so the LLM can visually verify its work. The built-in SVG renderer is useful for checking topology (are the right things connected?) but does not match Excalidraw's full visual fidelity – no roughness textures or hachure fills.

## Style Reference

All style properties are optional and can be used with `create_diagram` node specs or `modify_diagram`'s `restyle` operation.

| Property | Values | Default |
|---|---|---|
| `strokeColor` | Any CSS color | `#1e1e1e` |
| `backgroundColor` | Any CSS color or `transparent` | `transparent` |
| `fillStyle` | `solid`, `hachure`, `cross-hatch`, `dots` | `hachure` |
| `strokeWidth` | Number (px) | `2` |
| `strokeStyle` | `solid`, `dashed`, `dotted` | `solid` |
| `roughness` | `0` precise, `1` normal, `2` rough | `1` |
| `opacity` | `0`–`100` | `100` |
| `fontSize` | Number (px) | `20` |
| `fontFamily` | `1` Excalifont, `2` Nunito, `3` Cascadia, `4` Liberation Sans, `5` CJK | `1` |

## Resource

### `excalidraw://schema`

Element type definitions, valid style properties, layout types, operation types, and example specs. MCP clients can load this on demand for validation and autocompletion.

## Architecture

```
src/
├── index.ts                # MCP server entry, tool registration
├── schemas/spec.ts         # Zod input validation
├── tools/
│   ├── read.ts             # read_diagram
│   ├── create.ts           # create_diagram
│   ├── modify.ts           # modify_diagram
│   └── render.ts           # render_diagram
├── core/
│   ├── bindings.ts         # Binding integrity engine
│   ├── elements.ts         # Element factories (rect, ellipse, diamond, arrow)
│   ├── graph.ts            # Raw elements → GraphSummary
│   ├── ids.ts              # ID, seed, and nonce generation
│   ├── layout.ts           # Auto-layout algorithms
│   ├── overlap.ts          # Text–line overlap detection and nudging
│   ├── parser.ts           # Format detection, .excalidraw / .excalidraw.md parsing
│   ├── resolve-target.ts   # Target resolution with disambiguation errors
│   ├── serializer.ts       # Serialization (JSON + LZ-String)
│   └── text.ts             # Text measurement (character-width averages)
└── types/index.ts          # TypeScript type definitions
```

### Design decisions

- **GraphSummary abstraction** – the LLM sees a semantic graph (nodes, edges, groups), not raw Excalidraw elements. Non-graph elements like freehand drawings and images land in the `other` bucket, preserving round-trip fidelity.
- **Binding integrity engine** – Excalidraw uses bidirectional references between arrows and shapes. The engine validates and repairs these after every mutation, preventing orphaned or dangling bindings.
- **Atomic writes** – files are written via temp file + `rename(2)`, so a crash mid-write never corrupts the original.
- **Text measurement without native deps** – pre-computed character-width tables for all five Excalidraw font families. Intentionally over-estimates by ~15% so containers never clip; Excalidraw recalculates precisely on load.

## Development

```bash
bun install          # Install dependencies
bun test             # Run all tests (unit + property-based + integration)
bun run typecheck    # Type-check without emitting
bun run dev          # Run server directly
bun run build        # Build to dist/
```

Tests use `bun:test` with [`fast-check`](https://github.com/dubzzz/fast-check) for property-based testing of the binding integrity engine and overlap resolution.

## Known Limitations

- **Text sizing is approximate** – character-width averages mean diagrams reflow slightly when opened in Excalidraw. Opening and saving in Excalidraw recalculates text metrics.
- **Straight-line arrow routing** – arrows use direct point-to-point paths. Excalidraw applies its own curve routing when you open the file.
- **No appState/files round-trip** – viewport position, theme, and embedded images are not preserved through edits.
- **No concurrency handling** – concurrent writes to the same file use last-writer-wins semantics.
- **SVG renderer is simplified** – useful for topology verification but does not reproduce Excalidraw's roughness textures or hachure fills.

## License

[MIT](LICENSE)