Skip to main content
Glama
README.md
# Excalidraw MCP Server & Agent Skill

[![CI](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml/badge.svg)](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml)
[![Docker Build & Push](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml/badge.svg)](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml)
[![NPM Version](https://img.shields.io/npm/v/mcp-excalidraw-server)](https://www.npmjs.com/package/mcp-excalidraw-server)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Run a live Excalidraw canvas and control it from AI agents. This repo provides:

- **MCP Server**: Connect via Model Context Protocol (Claude Desktop, Cursor, Codex CLI, etc.)
- **Agent Skill**: Portable skill for Claude Code, Codex CLI, and other skill-enabled agents

Keywords: Excalidraw agent skill, Excalidraw MCP server, AI diagramming, Claude Code skill, Codex CLI skill, Claude Desktop MCP, Cursor MCP, Mermaid to Excalidraw.

## Demo

![MCP Excalidraw Demo](demo.gif)

*AI agent creates a complete architecture diagram from a single prompt (4x speed). [Watch full video on YouTube](https://youtu.be/ufW78Amq5qA)*

## Table of Contents

- [Demo](#demo)
- [What It Is](#what-it-is)
- [How We Differ from the Official Excalidraw MCP](#how-we-differ-from-the-official-excalidraw-mcp)
- [What's New](#whats-new)
- [Quick Start (Local)](#quick-start-local)
- [Quick Start (Docker)](#quick-start-docker)
- [Configure MCP Clients](#configure-mcp-clients)
  - [Claude Desktop](#claude-desktop)
  - [Claude Code](#claude-code)
  - [Cursor](#cursor)
  - [Codex CLI](#codex-cli)
  - [OpenCode](#opencode)
  - [Antigravity (Google)](#antigravity-google)
- [Agent Skill (Optional)](#agent-skill-optional)
- [MCP Tools (6 Total)](#mcp-tools-6-total)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
- [Known Issues / TODO](#known-issues--todo)
- [Development](#development)

## What It Is

This repo contains two separate processes:

- Canvas server: web UI + REST API + WebSocket updates (default `http://127.0.0.1:3000`)
- MCP server: exposes MCP tools over stdio; syncs to the canvas via `EXPRESS_SERVER_URL`

## How We Differ from the Official Excalidraw MCP

Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mcp) — it's great for quick, prompt-to-diagram generation rendered inline in chat. We solve a different problem.

| | Official Excalidraw MCP | This Project |
|---|---|---|
| **Approach** | Prompt in, diagram out (one-shot) | Programmatic element-level control (6 focused tools) |
| **State** | Stateless — each call is independent | Persistent live canvas with real-time sync |
| **Element CRUD** | No | `create` / `read` / `update` / `delete`, batch-first, with `id`-based upsert |
| **AI sees the canvas** | No | `read` mode `describe` (structured text) + `screenshot` (headless image) |
| **Iterative refinement** | No — regenerate the whole diagram | Draw → look → adjust → look again, element by element |
| **Layout tools** | No | `update` `arrange`: align / distribute / group / ungroup / lock, plus deterministic auto-layout on `create` |
| **Live-bound arrows** | No | `create` `from`/`to` writes native Excalidraw bindings — arrows re-route when nodes move |
| **File I/O** | No | `read` mode `export` (.excalidraw JSON) |
| **Snapshot & rollback** | No | `read` mode `snapshots` (list / save / restore) |
| **Mermaid conversion** | No | `create` with a `mermaid` string |
| **Design guide** | `read_me` cheat sheet | `read` mode `guide` (colors, sizing, layout, anti-patterns) |
| **Live canvas UI** | Rendered inline in chat | Standalone Excalidraw app synced via WebSocket |
| **Multi-agent** | Single user | Multiple agents can draw on the same canvas concurrently |
| **Browser collaboration** | Hosted app only | Cross-tab scene sync plus native Excalidraw collaborator cursors |
| **Works without MCP** | No | Yes — REST API fallback via agent skill |

**TL;DR** — The official MCP generates diagrams. We give AI agents a full canvas toolkit to build, inspect, and iteratively refine diagrams — including the ability to see what they drew.

## What's New

### v3.0 — Six-Tool Rebuild

- **Collapsed to 6 tools**: `set_room`, `create`, `update`, `delete`, `read`, `screenshot`. Everything the old ~28-tool surface did now lives under these (e.g. align/distribute/group/lock → `update.arrange`; export/snapshots/describe/guide → `read` modes; screenshot/image export → `screenshot`).
- **Deterministic upsert + auto-layout**: reuse an element `id` to create-or-update in place; omit x/y and nodes are auto-placed with no randomness (layered when `from`/`to` edges connect them, else a grid — same input, same coordinates).
- **Declarative live-bound arrows**: give an arrow/line `from`/`to` = a node id and the server both routes the endpoints and writes native Excalidraw `startBinding`/`endBinding` + `boundElements`, so the arrow stays attached and re-routes when the node is dragged in the browser.
- **Headless screenshots**: `screenshot` renders via a sidecar — no browser tab required — with `bbox` / `scale` / `maxDim` framing.
- **Single source of truth**: this repo is now the one MCP; the `npx` stdio install points here, and the heavy frontend-only deps moved to `devDependencies` to keep that install fast.

### v2.0 — Canvas Toolkit

- **Closed feedback loop**: AI can inspect the canvas (`read` mode `describe`) and see it (`screenshot` returns an image) — enabling iterative refinement
- **Design guide**: `read` mode `guide` returns best-practice color palettes, sizing rules, layout patterns, and anti-patterns — dramatically improves AI-generated diagram quality
- **File I/O**: export full `.excalidraw` JSON (`read` mode `export`)
- **Snapshots**: save and restore named canvas states (`read` mode `snapshots`)
- **Skill fallback**: Agent skill auto-detects MCP vs REST API mode, gracefully falls back to HTTP endpoints when MCP server isn't configured
- **Browser collaboration**: ordinary edits use low-latency delta sync between tabs, full-scene imports still use safe whole-scene sync, and websocket pointer updates drive native Excalidraw collaborator cursors

### v1.x

- Agent skill: `skills/excalidraw-skill/` (portable instructions + helper scripts for export/import and repeatable CRUD)
- Better testing loop: MCP Inspector CLI examples + browser screenshot checks (`agent-browser`)
- Bugfixes: batch create now preserves element ids (fixes update/delete after batch); frontend entrypoint fixed (`main.tsx`)

## Quick Start (Local)

Prereqs: Node >= 18, npm

```bash
npm ci
npm run build
```

Terminal 1: start the canvas
```bash
PORT=3000 npm run canvas
```

> **Security note:** The server defaults to binding on `127.0.0.1` only. If you need to expose it on a network interface (e.g. Docker, remote access), set `HOST=0.0.0.0` — but ensure you have network-level access controls in place, as the API has no built-in authentication.

Open `http://127.0.0.1:3000`.

Terminal 2: run the MCP server (stdio)
```bash
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node dist/index.js
```

## Quick Start (Docker)

Canvas server:
```bash
docker run -d -p 3000:3000 --name mcp-excalidraw-canvas ghcr.io/yctimlin/mcp_excalidraw-canvas:latest
```

MCP server (stdio) is typically launched by your MCP client (Claude Desktop/Cursor/etc.). If you want a local container for it, use the image `ghcr.io/yctimlin/mcp_excalidraw:latest` and set `EXPRESS_SERVER_URL` to point at the canvas.

## Configure MCP Clients

The MCP server runs over stdio and can be configured with any MCP-compatible client. Below are configurations for both **local** (requires cloning and building) and **Docker** (pull-and-run) setups.

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `EXPRESS_SERVER_URL` | URL of the canvas server | `http://127.0.0.1:3000` |
| `ROOM_ID` | Optional fallback room id. Omit it for arbitrary-room workflows and use `set_room`, `roomUrl`, or `roomId` per tool call instead. | unset |
| `ENABLE_CANVAS_SYNC` | Enable real-time canvas sync | `true` |
| `MCP_AGENT_CURSOR` | Show MCP tool activity as a collaborator cursor when canvas sync is enabled | `true` |
| `MCP_AGENT_NAME` | Collaborator label for MCP agent cursor presence | `MCP Agent` |
| `MCP_AGENT_COLOR` | Cursor color, either a stroke hex color or JSON with `background`/`stroke` | `#1971c2` stroke |

### Room Targeting

The MCP server can work with arbitrary Zephy rooms without reinstalling the MCP server.

- Call `set_room` with a pasted room URL, such as `https://draw.proklov.dev/r/<roomId>`, to make that room active for later tool calls.
- Or pass `roomUrl` / `roomId` directly to any canvas tool for a one-off operation.
- `ROOM_ID` remains supported only as a fallback default for single-room installs.

### Reading Large Boards

`read` mode `describe` is bounded by default so large canvases do not flood the
model context. The default overview returns a summary, spatial section index,
and prominent text. Page into the board with focused calls (pass these args to
the `read` tool):

```json
{ "mode": "describe", "detail": "overview" }
{ "mode": "describe", "detail": "elements", "sectionIndex": 2, "limit": 60 }
{ "mode": "describe", "detail": "elements", "types": ["text"], "textIncludes": "SESSION" }
{ "mode": "describe", "detail": "connections", "sectionIndex": 4 }
{ "mode": "describe", "detail": "overview", "filePath": "/tmp/board-overview.md" }
{ "mode": "describe", "detail": "full" }
```

Use `detail: "full"` only when you truly need the complete dump. `read` mode
`query` also accepts `filePath` to write results to disk instead of returning a
large inline payload. `screenshot` renders headlessly via the renderer sidecar —
no browser tab required.

---

### Claude Desktop

Config location:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`

**Local (node)**
```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "node",
      "args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"],
      "env": {
        "EXPRESS_SERVER_URL": "http://127.0.0.1:3000",
        "ENABLE_CANVAS_SYNC": "true"
      }
    }
  }
}
```

**Docker**
```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
        "-e", "ENABLE_CANVAS_SYNC=true",
        "ghcr.io/yctimlin/mcp_excalidraw:latest"
      ]
    }
  }
}
```

---

### Claude Code

Use the `claude mcp add` command to register the MCP server.

**Local (node)** - User-level (available across all projects):
```bash
claude mcp add excalidraw --scope user \
  -e EXPRESS_SERVER_URL=http://127.0.0.1:3000 \
  -e ENABLE_CANVAS_SYNC=true \
  -- node /absolute/path/to/mcp_excalidraw/dist/index.js
```

**Local (node)** - Project-level (shared via `.mcp.json`):
```bash
claude mcp add excalidraw --scope project \
  -e EXPRESS_SERVER_URL=http://127.0.0.1:3000 \
  -e ENABLE_CANVAS_SYNC=true \
  -- node /absolute/path/to/mcp_excalidraw/dist/index.js
```

**Docker**
```bash
claude mcp add excalidraw --scope user \
  -- docker run -i --rm \
  -e EXPRESS_SERVER_URL=http://host.docker.internal:3000 \
  -e ENABLE_CANVAS_SYNC=true \
  ghcr.io/yctimlin/mcp_excalidraw:latest
```

**Manage servers:**
```bash
claude mcp list              # List configured servers
claude mcp remove excalidraw # Remove a server
```

---

### Cursor

Config location: `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` for global config)

**Local (node)**
```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "node",
      "args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"],
      "env": {
        "EXPRESS_SERVER_URL": "http://127.0.0.1:3000",
        "ENABLE_CANVAS_SYNC": "true"
      }
    }
  }
}
```

**Docker**
```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
        "-e", "ENABLE_CANVAS_SYNC=true",
        "ghcr.io/yctimlin/mcp_excalidraw:latest"
      ]
    }
  }
}
```

---

### Codex CLI

Use the `codex mcp add` command to register the MCP server.

**Local (node)**
```bash
codex mcp add excalidraw \
  --env EXPRESS_SERVER_URL=http://127.0.0.1:3000 \
  --env ENABLE_CANVAS_SYNC=true \
  -- node /absolute/path/to/mcp_excalidraw/dist/index.js
```

**Docker**
```bash
codex mcp add excalidraw \
  -- docker run -i --rm \
  -e EXPRESS_SERVER_URL=http://host.docker.internal:3000 \
  -e ENABLE_CANVAS_SYNC=true \
  ghcr.io/yctimlin/mcp_excalidraw:latest
```

**Manage servers:**
```bash
codex mcp list              # List configured servers
codex mcp remove excalidraw # Remove a server
```

---

### OpenCode

Config location: `~/.config/opencode/opencode.json` or project-level `opencode.json`

**Local (node)**
```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "excalidraw": {
      "type": "local",
      "command": ["node", "/absolute/path/to/mcp_excalidraw/dist/index.js"],
      "enabled": true,
      "environment": {
        "EXPRESS_SERVER_URL": "http://127.0.0.1:3000",
        "ENABLE_CANVAS_SYNC": "true"
      }
    }
  }
}
```

**Docker**
```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "excalidraw": {
      "type": "local",
      "command": ["docker", "run", "-i", "--rm", "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000", "-e", "ENABLE_CANVAS_SYNC=true", "ghcr.io/yctimlin/mcp_excalidraw:latest"],
      "enabled": true
    }
  }
}
```

---

### Antigravity (Google)

Config location: `~/.gemini/antigravity/mcp_config.json`

**Local (node)**
```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "node",
      "args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"],
      "env": {
        "EXPRESS_SERVER_URL": "http://127.0.0.1:3000",
        "ENABLE_CANVAS_SYNC": "true"
      }
    }
  }
}
```

**Docker**
```json
{
  "mcpServers": {
    "excalidraw": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
        "-e", "ENABLE_CANVAS_SYNC=true",
        "ghcr.io/yctimlin/mcp_excalidraw:latest"
      ]
    }
  }
}
```

---

### Notes

- **Docker networking**: Use `host.docker.internal` to reach the canvas server running on your host machine. On Linux, you may need `--add-host=host.docker.internal:host-gateway` or use `172.17.0.1`.
- **Canvas server**: Must be running before the MCP server connects. Start it with `npm run canvas` (local) or `docker run -d -p 3000:3000 ghcr.io/yctimlin/mcp_excalidraw-canvas:latest` (Docker).
- **Absolute paths**: When using local node setup, replace `/absolute/path/to/mcp_excalidraw` with the actual path where you cloned and built the repo.
- **In-memory storage**: The canvas server stores elements in memory. Restarting the server will clear all elements. Use the export/import scripts if you need persistence.

## Agent Skill (Optional)

This repo includes a skill at `skills/excalidraw-skill/` that provides:

- **Workflow playbook** (`SKILL.md`): step-by-step guidance for drawing, refining, and exporting diagrams
- **Cheatsheet** (`references/cheatsheet.md`): MCP tool and REST API reference
- **Helper scripts** (`scripts/*.cjs`): export, import, clear, healthcheck, CRUD operations

The skill complements the MCP server by giving your AI agent structured workflows to follow.

### Install The Skill (Codex CLI example)

```bash
mkdir -p ~/.codex/skills
cp -R skills/excalidraw-skill ~/.codex/skills/excalidraw-skill
```

To update an existing installation, remove the old folder first (`rm -rf ~/.codex/skills/excalidraw-skill`) then re-copy.

### Install The Skill (Claude Code)

**User-level** (available across all your projects):
```bash
mkdir -p ~/.claude/skills
cp -R skills/excalidraw-skill ~/.claude/skills/excalidraw-skill
```

**Project-level** (scoped to a specific project, can be committed to the repo):
```bash
mkdir -p /path/to/your/project/.claude/skills
cp -R skills/excalidraw-skill /path/to/your/project/.claude/skills/excalidraw-skill
```

Then invoke the skill in Claude Code with `/excalidraw-skill`.

To update an existing installation, remove the old folder first then re-copy.

### Use The Skill Scripts

All scripts respect `EXPRESS_SERVER_URL` (default `http://127.0.0.1:3000`) or accept `--url`.

```bash
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-skill/scripts/healthcheck.cjs
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-skill/scripts/export-elements.cjs --out diagram.elements.json
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-skill/scripts/import-elements.cjs --in diagram.elements.json --mode batch
```

### When The Skill Is Useful

- Repository workflow: export elements as JSON, commit it, and re-import later.
- Reliable refactors: clear + re-import in `sync` mode to make canvas match a file.
- Automated smoke tests: create/update/delete a known element to validate a deployment.
- Repeatable diagrams: keep a library of element JSON snippets and import them.

See `skills/excalidraw-skill/SKILL.md` and `skills/excalidraw-skill/references/cheatsheet.md`.

## MCP Tools (6 Total)

The surface is deliberately small: six powerful, batch-first, deterministic tools.
Reuse an element `id` to upsert (create-or-update); the same input always produces
the same auto-layout coordinates.

| Tool | What it does |
|---|---|
| `set_room` | Pin/switch the active room (full `roomUrl`, or `roomId`+`expressUrl`); call with no args to report the current room. Any tool also accepts a per-call `roomUrl`/`roomId` override. |
| `create` | Create/upsert one or many elements (`elements: [...]` or a single inline element). Connect nodes with `from`/`to` = element id — the server routes endpoints **and writes native Excalidraw bindings** so arrows follow nodes when moved. Omit x/y for deterministic auto-layout (layered when edges connect nodes, else a grid). Also does `mermaid` rendering and `copyOf` duplication. |
| `update` | Select by `id`, `ids`, or `query` (type/filter/bbox), then either apply a `set` of properties or run an `arrange` op: `align` / `distribute` / `group` / `ungroup` / `lock` / `unlock`. |
| `delete` | Remove by `id`, `ids`, or `query`, or `all: true` to clear the canvas. |
| `read` | One reader with `mode`: `describe` (bounded overview + section index), `query`, `get`, `export` (.excalidraw JSON), `snapshots` (list / `save` / `restore`), `guide` (diagram design guide). |
| `screenshot` | Headless PNG/SVG via the renderer sidecar (no browser tab needed). Frame with `bbox` / `scale` / `maxDim`; returns the image inline or to `filePath`. |

Full schemas are discoverable via `tools/list` or in `skills/excalidraw-skill/references/cheatsheet.md`.

## Testing

### Canvas Smoke Test (HTTP)

```bash
curl http://127.0.0.1:3000/health
```

### Local Bind Regression Test

```bash
npm run test:bind
```

### Realtime WebSocket Regression Test

```bash
npm run test:realtime
```

### MCP Smoke Test (MCP Inspector)

List tools:
```bash
npx @modelcontextprotocol/inspector --cli \
  -e EXPRESS_SERVER_URL=http://127.0.0.1:3000 \
  -e ENABLE_CANVAS_SYNC=true -- \
  node dist/index.js --method tools/list
```

Create a rectangle (the `create` tool is batch-first — pass an `elements` array):
```bash
npx @modelcontextprotocol/inspector --cli \
  -e EXPRESS_SERVER_URL=http://127.0.0.1:3000 \
  -e ENABLE_CANVAS_SYNC=true -- \
  node dist/index.js --method tools/call --tool-name create \
  --tool-arg 'elements=[{"type":"rectangle","x":100,"y":100,"width":300,"height":200}]'
```

### Frontend Screenshots (agent-browser)

If you use `agent-browser` for UI checks:
```bash
agent-browser install
agent-browser open http://127.0.0.1:3000
agent-browser wait --load networkidle
agent-browser screenshot /tmp/canvas.png
```

## Troubleshooting

- Canvas not updating: confirm `EXPRESS_SERVER_URL` points at the running canvas server.
- Updates/deletes fail after batch creation: ensure you are on a build that includes the batch id preservation fix (merged via PR #34).

## Known Issues / TODO

All previously listed bugs have been fixed in v2.0. Remaining items:

- [ ] **Persistent storage**: rooms are checkpointed to disk (with a write-ahead log), but for portable backups use `read` mode `export` / `snapshots`.

Contributions welcome!

## Development

```bash
npm run type-check
npm run build
```

Maintenance

ActivityStale
ResponsivenessNo issues