Feat/excalidraw skill (#38)
* feat(skill): add excalidraw-mcp skill with export/import helpers * fix(skill): correct url trailing-slash normalization in scripts * feat(skill): add create/update/delete helpers and document CRUD test * docs: restructure README and document skill usage * docs: make README skill guidance tool-agnostic and SEO-friendly * docs: add Claude Code skill installation instructions * feat: Introduce skill creation tools and an agent-browser skill with templates and reference documentation. * fix(frontend): keep server element ids for WS updates Ensure WS delete/update events match scene element IDs by disabling ID regeneration when converting server payloads. * feat: Add Zod library and OpenCode AI SDK dependencies, and introduce new Excalidraw skill scripts for element deletion and health checks. * chore: Install project dependencies including Zod and OpenCode AI SDK, and add an Excalidraw reference cheatsheet. --------- Co-authored-by: YC Lin <yclin@YCdeMacBook-Air.local>
This commit is contained in:
@@ -1,324 +1,101 @@
|
||||
# MCP Excalidraw Server: Advanced Live Visual Diagramming with AI Integration
|
||||
# Excalidraw MCP Server & Agent Skill
|
||||
|
||||
[](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml)
|
||||
[](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml)
|
||||
[](https://www.npmjs.com/package/mcp-excalidraw-server)
|
||||
[](LICENSE)
|
||||
|
||||
A comprehensive **TypeScript-based** system that combines **Excalidraw's powerful drawing capabilities** with **Model Context Protocol (MCP)** integration, enabling AI agents to create and manipulate diagrams in real-time on a live canvas.
|
||||
Run a live Excalidraw canvas and control it from AI agents. This repo provides:
|
||||
|
||||
## 🚦 Current Status & Version Information
|
||||
- **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
|
||||
|
||||
> **📋 Choose Your Installation Method**
|
||||
Keywords: Excalidraw agent skill, Excalidraw MCP server, AI diagramming, Claude Code skill, Codex CLI skill, Claude Desktop MCP, Cursor MCP, Mermaid to Excalidraw.
|
||||
|
||||
| Component | Local | Docker | Status |
|
||||
|-----------|-------|--------|--------|
|
||||
| **Canvas Server** | ✅ Fully Working | ✅ Fully Working | **Production Ready** |
|
||||
| **MCP Server** | ✅ Fully Working | ✅ Fully Working | **Production Ready** |
|
||||
| **NPM Published** | 🔧 In Progress | N/A | Development testing |
|
||||
## Table of Contents
|
||||
|
||||
### **Important: Canvas and MCP Server Run Separately**
|
||||
- [What It Is](#what-it-is)
|
||||
- [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 (High Level)](#mcp-tools-high-level)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Known Issues / TODO](#known-issues--todo)
|
||||
- [Development](#development)
|
||||
|
||||
This system consists of **two independent components**:
|
||||
## What It Is
|
||||
|
||||
1. **Canvas Server** - Runs the live Excalidraw canvas (web interface)
|
||||
2. **MCP Server** - Connects to Claude Desktop/Claude Code/Cursor IDE
|
||||
This repo contains two separate processes:
|
||||
|
||||
**You can choose any combination:**
|
||||
- Canvas: Local OR Docker
|
||||
- MCP Server: Local OR Docker
|
||||
- Canvas server: web UI + REST API + WebSocket updates (default `http://localhost:3000`)
|
||||
- MCP server: exposes MCP tools over stdio; syncs to the canvas via `EXPRESS_SERVER_URL`
|
||||
|
||||
Both local and Docker setups are **fully working** and production-ready!
|
||||
## What's New
|
||||
|
||||
## 🚀 What This System Does
|
||||
- Agent skill: `skills/excalidraw-mcp/` (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`)
|
||||
|
||||
- **🎨 Live Canvas**: Real-time Excalidraw canvas accessible via web browser
|
||||
- **🤖 AI Integration**: MCP server allows AI agents (like Claude) to create visual diagrams
|
||||
- **⚡ Real-time Sync**: Elements created via MCP API appear instantly on the canvas
|
||||
- **🔄 WebSocket Updates**: Live synchronization across multiple connected clients
|
||||
- **🏗️ Production Ready**: Clean, minimal UI suitable for end users
|
||||
## Quick Start (Local)
|
||||
|
||||
## 🎥 Demo Video
|
||||
Prereqs: Node >= 18, npm
|
||||
|
||||
> **See MCP Excalidraw in Action!**
|
||||
|
||||
[](https://youtu.be/RRN7AF7QIew)
|
||||
|
||||
*Watch how AI agents create and manipulate diagrams in real-time on the live canvas*
|
||||
|
||||
## 🏛️ Architecture Overview
|
||||
|
||||
### **Two Independent Components**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Component 1 │
|
||||
│ 🎨 CANVAS SERVER │
|
||||
│ (Runs Independently) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Canvas Server │◀───────▶│ Frontend │ │
|
||||
│ │ (src/server.js) │ │ (React + WS) │ │
|
||||
│ │ Port 3000 │ │ Excalidraw UI │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ 📍 Start: npm run canvas OR docker run (canvas) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
▲
|
||||
│ HTTP API
|
||||
│ (Optional)
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Component 2 │
|
||||
│ 🤖 MCP SERVER │
|
||||
│ (Runs Independently) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ AI Agent │◀───────▶│ MCP Server │ │
|
||||
│ │ (Claude) │ │ (src/index.js) │ │
|
||||
│ │ Desktop/Code │ stdio │ MCP Protocol │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ 📍 Configure in: claude_desktop_config.json OR .mcp.json │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
🎯 Key Points:
|
||||
• Canvas and MCP server are SEPARATE processes
|
||||
• Canvas can run locally OR in Docker
|
||||
• MCP server can run locally OR in Docker
|
||||
• Canvas provides the visual interface (optional)
|
||||
• MCP server connects Claude to the canvas (via HTTP API)
|
||||
```
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
### **Modern TypeScript Architecture**
|
||||
- **Full TypeScript Migration**: Complete type safety for backend and frontend
|
||||
- **Comprehensive Type Definitions**: Excalidraw elements, API responses, WebSocket messages
|
||||
- **Strict Type Checking**: Enhanced development experience and compile-time error detection
|
||||
- **Type-Safe React Components**: TSX components with proper props typing
|
||||
|
||||
### **🎨 Mermaid Diagram Support (NEW!)**
|
||||
- **Mermaid to Excalidraw**: Convert Mermaid diagrams directly to Excalidraw elements
|
||||
- **MCP Tool Integration**: Use `create_from_mermaid` tool from Claude
|
||||
- **Browser-based Conversion**: Leverages DOM access in the frontend for accurate rendering
|
||||
- **Multiple Diagram Types**: Supports flowcharts, sequence diagrams, class diagrams, and more
|
||||
- **Test Button**: Quick test functionality directly from the canvas UI
|
||||
|
||||
### **Real-time Canvas Integration**
|
||||
- Elements created via MCP appear instantly on the live canvas
|
||||
- WebSocket-based real-time synchronization
|
||||
- Multi-client support with live updates
|
||||
|
||||
### **Production-Ready Interface**
|
||||
- Clean, minimal UI with connection status
|
||||
- Simple "Clear Canvas" functionality
|
||||
- No development clutter or debug information
|
||||
|
||||
### **Comprehensive MCP API**
|
||||
- **Element Creation**: rectangles, ellipses, diamonds, arrows, text, lines
|
||||
- **Element Management**: update, delete, query with filters
|
||||
- **Batch Operations**: create multiple elements in one call
|
||||
- **Advanced Features**: grouping, alignment, distribution, locking
|
||||
|
||||
### **Robust Architecture**
|
||||
- TypeScript-based Express.js backend with REST API + WebSocket
|
||||
- React frontend with official Excalidraw package and TypeScript
|
||||
- Dual-path element loading for reliability
|
||||
- Auto-reconnection and error handling
|
||||
|
||||
## 📦 Installation & Setup
|
||||
|
||||
### **Step 1: Choose Your Canvas Server Setup**
|
||||
|
||||
The canvas server provides the live Excalidraw interface.
|
||||
|
||||
#### **Option A: Local Canvas Server**
|
||||
|
||||
1. **Clone and Install**
|
||||
```bash
|
||||
git clone https://github.com/yctimlin/mcp_excalidraw.git
|
||||
cd mcp_excalidraw
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Build the Project**
|
||||
```bash
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. **Start Canvas Server**
|
||||
Terminal 1: start the canvas
|
||||
```bash
|
||||
# Production mode (recommended)
|
||||
npm run canvas
|
||||
HOST=0.0.0.0 PORT=3000 npm run canvas
|
||||
```
|
||||
|
||||
4. **Access the Canvas**
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
Open `http://localhost:3000`.
|
||||
|
||||
#### **Option B: Docker Canvas Server**
|
||||
Terminal 2: run the MCP server (stdio)
|
||||
```bash
|
||||
EXPRESS_SERVER_URL=http://localhost:3000 node dist/index.js
|
||||
```
|
||||
|
||||
**Option B1: Use Pre-built Image from GHCR** (Recommended)
|
||||
## Quick Start (Docker)
|
||||
|
||||
Canvas server:
|
||||
```bash
|
||||
docker pull ghcr.io/yctimlin/mcp_excalidraw-canvas:latest
|
||||
docker run -d -p 3000:3000 --name mcp-excalidraw-canvas ghcr.io/yctimlin/mcp_excalidraw-canvas:latest
|
||||
```
|
||||
|
||||
**Option B2: Build Locally**
|
||||
```bash
|
||||
git clone https://github.com/yctimlin/mcp_excalidraw.git
|
||||
cd mcp_excalidraw
|
||||
docker build -f Dockerfile.canvas -t mcp-excalidraw-canvas .
|
||||
docker run -d -p 3000:3000 --name mcp-excalidraw-canvas mcp-excalidraw-canvas
|
||||
```
|
||||
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.
|
||||
|
||||
3. **Access the Canvas**
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
## 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://localhost:3000` |
|
||||
| `ENABLE_CANVAS_SYNC` | Enable real-time canvas sync | `true` |
|
||||
|
||||
---
|
||||
|
||||
### **Step 2: Configure MCP Server in Your IDE**
|
||||
### Claude Desktop
|
||||
|
||||
The MCP server connects your AI assistant (Claude) to the canvas. **Choose local OR Docker format** based on your preference.
|
||||
|
||||
#### **Setup Combinations**
|
||||
|
||||
You can mix and match any combination:
|
||||
|
||||
| Canvas Server | MCP Server | Status |
|
||||
|---------------|------------|--------|
|
||||
| ✅ Local | ✅ Local | Recommended |
|
||||
| ✅ Local | ✅ Docker | Fully Working |
|
||||
| ✅ Docker | ✅ Local | Fully Working |
|
||||
| ✅ Docker | ✅ Docker | Fully Working |
|
||||
|
||||
Configuration examples are provided in the next section for:
|
||||
- Claude Desktop
|
||||
- Claude Code
|
||||
- Cursor IDE
|
||||
|
||||
## 🔧 Available Scripts
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `npm start` | Build and start MCP server (`dist/index.js`) |
|
||||
| `npm run canvas` | Build and start canvas server (`dist/server.js`) |
|
||||
| `npm run build` | Build both frontend and TypeScript backend |
|
||||
| `npm run build:frontend` | Build React frontend only |
|
||||
| `npm run build:server` | Compile TypeScript backend to JavaScript |
|
||||
| `npm run dev` | Start TypeScript watch mode + Vite dev server |
|
||||
| `npm run type-check` | Run TypeScript type checking without compilation |
|
||||
| `npm run production` | Build + start in production mode |
|
||||
|
||||
## 🎯 Usage Guide
|
||||
|
||||
### **For End Users**
|
||||
1. Open the canvas at `http://localhost:3000`
|
||||
2. Check connection status (should show "Connected")
|
||||
3. AI agents can now create diagrams that appear in real-time
|
||||
4. Use "Clear Canvas" to remove all elements
|
||||
|
||||
### **For AI Agents (via MCP)**
|
||||
The MCP server provides these tools for creating visual diagrams:
|
||||
|
||||
#### **Basic Element Creation**
|
||||
```javascript
|
||||
// Create a rectangle
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"backgroundColor": "#e3f2fd",
|
||||
"strokeColor": "#1976d2",
|
||||
"strokeWidth": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### **Create Text Elements**
|
||||
```javascript
|
||||
{
|
||||
"type": "text",
|
||||
"x": 150,
|
||||
"y": 125,
|
||||
"text": "Process Step",
|
||||
"fontSize": 16,
|
||||
"strokeColor": "#333333"
|
||||
}
|
||||
```
|
||||
|
||||
#### **Create Arrows & Lines**
|
||||
```javascript
|
||||
{
|
||||
"type": "arrow",
|
||||
"x": 300,
|
||||
"y": 130,
|
||||
"width": 100,
|
||||
"height": 0,
|
||||
"strokeColor": "#666666",
|
||||
"strokeWidth": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### **Batch Creation for Complex Diagrams**
|
||||
```javascript
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 120,
|
||||
"height": 60,
|
||||
"backgroundColor": "#fff3e0",
|
||||
"strokeColor": "#ff9800"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 130,
|
||||
"y": 125,
|
||||
"text": "Start",
|
||||
"fontSize": 16
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔌 MCP Server Configuration for IDEs
|
||||
|
||||
### **Prerequisites**
|
||||
✅ Ensure your **canvas server is running** (from Step 1):
|
||||
- Local: `npm run canvas`
|
||||
- Docker: `docker run -d -p 3000:3000 mcp-excalidraw-canvas`
|
||||
|
||||
Canvas should be accessible at http://localhost:3000
|
||||
|
||||
### **Quick Reference**
|
||||
|
||||
Choose your configuration based on IDE and preference:
|
||||
|
||||
| IDE | Config File | Format Options |
|
||||
|-----|-------------|----------------|
|
||||
| **Claude Desktop** | `claude_desktop_config.json` | Local ⭐ / Docker ✅ |
|
||||
| **Claude Code** | `.mcp.json` (project root) | Local ⭐ / Docker ✅ |
|
||||
| **Cursor** | `.cursor/mcp.json` | Local ⭐ / Docker ✅ |
|
||||
|
||||
⭐ = Recommended | ✅ = Fully Working
|
||||
|
||||
---
|
||||
|
||||
## **Configuration for Claude Desktop**
|
||||
|
||||
Edit your `claude_desktop_config.json` file:
|
||||
|
||||
### **Format 1: Local MCP Server** ⭐ Recommended
|
||||
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": {
|
||||
@@ -334,22 +111,15 @@ Edit your `claude_desktop_config.json` file:
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Replace `/absolute/path/to/mcp_excalidraw` with your actual installation path.
|
||||
|
||||
### **Format 2: Docker MCP Server** ✅ Fully Working
|
||||
|
||||
**Using Pre-built Image from GHCR** (Recommended):
|
||||
**Docker**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--network", "host",
|
||||
"-e", "EXPRESS_SERVER_URL=http://localhost:3000",
|
||||
"run", "-i", "--rm",
|
||||
"-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
|
||||
"-e", "ENABLE_CANVAS_SYNC=true",
|
||||
"ghcr.io/yctimlin/mcp_excalidraw:latest"
|
||||
]
|
||||
@@ -358,95 +128,50 @@ Edit your `claude_desktop_config.json` file:
|
||||
}
|
||||
```
|
||||
|
||||
**OR Build Locally**:
|
||||
```bash
|
||||
cd mcp_excalidraw
|
||||
docker build -f Dockerfile -t mcp-excalidraw .
|
||||
```
|
||||
|
||||
Then use `mcp-excalidraw` as the image name in the configuration above.
|
||||
|
||||
---
|
||||
|
||||
## **Configuration for Claude Code**
|
||||
### Claude Code
|
||||
|
||||
Create or edit `.mcp.json` in your project root:
|
||||
Use the `claude mcp add` command to register the MCP server.
|
||||
|
||||
### **Format 1: Local MCP Server** ⭐ Recommended
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"],
|
||||
"env": {
|
||||
"EXPRESS_SERVER_URL": "http://localhost:3000",
|
||||
"ENABLE_CANVAS_SYNC": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Replace `/absolute/path/to/mcp_excalidraw` with your actual installation path.
|
||||
|
||||
### **Format 2: Docker MCP Server** ✅ Fully Working
|
||||
|
||||
**Using Pre-built Image from GHCR** (Recommended):
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--network", "host",
|
||||
"-e", "EXPRESS_SERVER_URL=http://localhost:3000",
|
||||
"-e", "ENABLE_CANVAS_SYNC=true",
|
||||
"ghcr.io/yctimlin/mcp_excalidraw:latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**OR Build Locally**:
|
||||
**Local (node)** - User-level (available across all projects):
|
||||
```bash
|
||||
cd mcp_excalidraw
|
||||
docker build -f Dockerfile -t mcp-excalidraw .
|
||||
```
|
||||
|
||||
Then use `mcp-excalidraw` as the image name in the configuration above.
|
||||
|
||||
### **Alternative: Using Claude CLI**
|
||||
|
||||
```bash
|
||||
# Project-scoped (recommended)
|
||||
claude mcp add --scope project --transport stdio excalidraw \
|
||||
-- docker run -i --rm --network host \
|
||||
claude mcp add excalidraw --scope user \
|
||||
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||
-e ENABLE_CANVAS_SYNC=true \
|
||||
mcp-excalidraw
|
||||
-- node /absolute/path/to/mcp_excalidraw/dist/index.js
|
||||
```
|
||||
|
||||
# User-scoped (available across all projects)
|
||||
claude mcp add --scope user --transport stdio excalidraw \
|
||||
-- docker run -i --rm --network host \
|
||||
**Local (node)** - Project-level (shared via `.mcp.json`):
|
||||
```bash
|
||||
claude mcp add excalidraw --scope project \
|
||||
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||
-e ENABLE_CANVAS_SYNC=true \
|
||||
mcp-excalidraw
|
||||
-- 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Configuration for Cursor IDE**
|
||||
### Cursor
|
||||
|
||||
Edit `.cursor/mcp.json`:
|
||||
|
||||
### **Format 1: Local MCP Server** ⭐ Recommended
|
||||
Config location: `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` for global config)
|
||||
|
||||
**Local (node)**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
@@ -462,20 +187,15 @@ Edit `.cursor/mcp.json`:
|
||||
}
|
||||
```
|
||||
|
||||
### **Format 2: Docker MCP Server** ✅ Fully Working
|
||||
|
||||
**Using Pre-built Image from GHCR** (Recommended):
|
||||
**Docker**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--network", "host",
|
||||
"-e", "EXPRESS_SERVER_URL=http://localhost:3000",
|
||||
"run", "-i", "--rm",
|
||||
"-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
|
||||
"-e", "ENABLE_CANVAS_SYNC=true",
|
||||
"ghcr.io/yctimlin/mcp_excalidraw:latest"
|
||||
]
|
||||
@@ -484,195 +204,249 @@ Edit `.cursor/mcp.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**OR Build Locally**:
|
||||
---
|
||||
|
||||
### Codex CLI
|
||||
|
||||
Use the `codex mcp add` command to register the MCP server.
|
||||
|
||||
**Local (node)**
|
||||
```bash
|
||||
cd mcp_excalidraw
|
||||
docker build -f Dockerfile -t mcp-excalidraw .
|
||||
codex mcp add excalidraw \
|
||||
--env EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||
--env ENABLE_CANVAS_SYNC=true \
|
||||
-- node /absolute/path/to/mcp_excalidraw/dist/index.js
|
||||
```
|
||||
|
||||
Then use `mcp-excalidraw` as the image name in the configuration above.
|
||||
**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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Important Configuration Notes**
|
||||
### OpenCode
|
||||
|
||||
| Setting | Purpose | Required |
|
||||
|---------|---------|----------|
|
||||
| `EXPRESS_SERVER_URL` | Canvas server URL | Yes (default: http://localhost:3000) |
|
||||
| `ENABLE_CANVAS_SYNC` | Enable real-time canvas sync | Yes (set to "true") |
|
||||
| `--network host` | Docker access to localhost | Required for Docker |
|
||||
| `-i` flag | Interactive stdin/stdout | Required for Docker |
|
||||
Config location: `~/.config/opencode/opencode.json` or project-level `opencode.json`
|
||||
|
||||
**Canvas is optional**: The MCP server works without the canvas in API-only mode (for programmatic access only).
|
||||
|
||||
## 🛠️ Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `EXPRESS_SERVER_URL` | `http://localhost:3000` | Canvas server URL for MCP sync |
|
||||
| `ENABLE_CANVAS_SYNC` | `true` | Enable/disable canvas synchronization |
|
||||
| `LOG_FILE_PATH` | `excalidraw.log` | Path to the log file |
|
||||
| `DEBUG` | `false` | Enable debug logging |
|
||||
| `PORT` | `3000` | Canvas server port |
|
||||
| `HOST` | `localhost` | Canvas server host |
|
||||
|
||||
## 📊 API Endpoints
|
||||
|
||||
The canvas server provides these REST endpoints:
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/api/elements` | Get all elements |
|
||||
| `POST` | `/api/elements` | Create new element |
|
||||
| `PUT` | `/api/elements/:id` | Update element |
|
||||
| `DELETE` | `/api/elements/:id` | Delete element |
|
||||
| `POST` | `/api/elements/batch` | Create multiple elements |
|
||||
| `GET` | `/health` | Server health check |
|
||||
|
||||
## 🎨 MCP Tools Available
|
||||
|
||||
### **Element Management**
|
||||
- `create_element` - Create any type of Excalidraw element
|
||||
- `update_element` - Modify existing elements
|
||||
- `delete_element` - Remove elements
|
||||
- `query_elements` - Search elements with filters
|
||||
|
||||
### **Batch Operations**
|
||||
- `batch_create_elements` - Create complex diagrams in one call
|
||||
|
||||
### **Element Organization**
|
||||
- `group_elements` - Group multiple elements
|
||||
- `ungroup_elements` - Ungroup element groups
|
||||
- `align_elements` - Align elements (left, center, right, top, middle, bottom)
|
||||
- `distribute_elements` - Distribute elements evenly
|
||||
- `lock_elements` / `unlock_elements` - Lock/unlock elements
|
||||
|
||||
### **Resource Access**
|
||||
- `get_resource` - Access scene, library, theme, or elements data
|
||||
|
||||
## 🏗️ Development Architecture
|
||||
|
||||
### **Frontend** (`frontend/src/`)
|
||||
- **React + TypeScript**: Modern TSX components with full type safety
|
||||
- **Vite Build System**: Fast development and optimized production builds
|
||||
- **Official Excalidraw**: `@excalidraw/excalidraw` package with TypeScript types
|
||||
- **WebSocket Client**: Type-safe real-time element synchronization
|
||||
- **Clean UI**: Production-ready interface with proper TypeScript typing
|
||||
|
||||
### **Canvas Server** (`src/server.ts` → `dist/server.js`)
|
||||
- **TypeScript + Express.js**: Fully typed REST API + static file serving
|
||||
- **WebSocket**: Type-safe real-time client communication
|
||||
- **Element Storage**: In-memory with comprehensive type definitions
|
||||
- **CORS**: Cross-origin support with proper typing
|
||||
|
||||
### **MCP Server** (`src/index.ts` → `dist/index.js`)
|
||||
- **TypeScript MCP Protocol**: Type-safe Model Context Protocol implementation
|
||||
- **Canvas Sync**: Strongly typed HTTP requests to canvas server
|
||||
- **Element Management**: Full CRUD operations with comprehensive type checking
|
||||
- **Batch Support**: Type-safe complex diagram creation
|
||||
|
||||
### **Type System** (`src/types.ts`)
|
||||
- **Excalidraw Element Types**: Complete type definitions for all element types
|
||||
- **API Response Types**: Strongly typed REST API interfaces
|
||||
- **WebSocket Message Types**: Type-safe real-time communication
|
||||
- **Server Element Types**: Enhanced element types with metadata
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### **Canvas Not Loading**
|
||||
- Ensure `npm run build` completed successfully
|
||||
- Check that `dist/index.html` and `dist/frontend/` directory exist
|
||||
- Verify canvas server is running on port 3000
|
||||
- Check if port 3000 is already in use: `lsof -i :3000` (macOS/Linux) or `netstat -ano | findstr :3000` (Windows)
|
||||
|
||||
### **Elements Not Syncing**
|
||||
- Confirm canvas server is running and accessible at http://localhost:3000
|
||||
- Check `ENABLE_CANVAS_SYNC=true` in MCP server environment configuration
|
||||
- Verify `EXPRESS_SERVER_URL` points to correct canvas server URL
|
||||
- Check browser console for WebSocket connection errors
|
||||
- For Docker: Ensure `--network host` flag is used
|
||||
|
||||
### **WebSocket Connection Issues**
|
||||
- Check browser console for WebSocket errors (F12 → Console tab)
|
||||
- Ensure no firewall blocking WebSocket connections on port 3000
|
||||
- Try refreshing the browser page
|
||||
- Verify canvas server is running: `curl http://localhost:3000/health`
|
||||
|
||||
### **Docker Issues**
|
||||
|
||||
**Canvas Container:**
|
||||
- Check if container is running: `docker ps | grep canvas`
|
||||
- View logs: `docker logs mcp-excalidraw-canvas`
|
||||
- Ensure port 3000 is not already in use
|
||||
|
||||
**MCP Container:**
|
||||
- For Docker MCP server, ensure `--network host` is used (required to access localhost:3000)
|
||||
- Verify `-i` flag is present (required for MCP stdin/stdout protocol)
|
||||
- Check environment variables are properly set
|
||||
|
||||
### **Build Errors**
|
||||
- Delete `node_modules` and `dist/` directories, then run `npm install && npm run build`
|
||||
- Check Node.js version (requires 16+): `node --version`
|
||||
- Run `npm run type-check` to identify TypeScript issues
|
||||
- Verify `dist/` directory contains both `index.js`, `server.js`, and `frontend/` after build
|
||||
|
||||
### **NPM Package Issues**
|
||||
- **Status**: NPM package is under development
|
||||
- **Recommendation**: Use local or Docker installation methods for production use
|
||||
|
||||
## 📋 Project Structure
|
||||
|
||||
```
|
||||
mcp_excalidraw/
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── App.tsx # Main React component (TypeScript)
|
||||
│ │ └── main.tsx # React entry point (TypeScript)
|
||||
│ └── index.html # HTML template
|
||||
├── src/ (TypeScript Source)
|
||||
│ ├── index.ts # MCP server (TypeScript)
|
||||
│ ├── server.ts # Canvas server (Express + WebSocket, TypeScript)
|
||||
│ ├── types.ts # Comprehensive type definitions
|
||||
│ └── utils/
|
||||
│ └── logger.ts # Logging utility (TypeScript)
|
||||
├── dist/ (Compiled Output)
|
||||
│ ├── index.js # Compiled MCP server
|
||||
│ ├── server.js # Compiled Canvas server
|
||||
│ ├── types.js # Compiled type definitions
|
||||
│ ├── utils/
|
||||
│ │ └── logger.js # Compiled logging utility
|
||||
│ └── frontend/ # Built React frontend
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── vite.config.js # Vite build configuration
|
||||
├── package.json # Dependencies and scripts
|
||||
└── README.md # This file
|
||||
**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://localhost:3000",
|
||||
"ENABLE_CANVAS_SYNC": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔮 Development Roadmap
|
||||
**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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- ✅ **TypeScript Migration**: Complete type safety for enhanced development experience
|
||||
- ✅ **Docker Deployment**: Both Canvas and MCP server fully working in Docker
|
||||
- 🔧 **NPM Package**: Resolving MCP tool registration issues
|
||||
- 🎯 **Enhanced Features**: Additional MCP tools and capabilities
|
||||
- 🎯 **Performance Optimization**: Real-time sync improvements
|
||||
- 🎯 **Advanced TypeScript Features**: Stricter type checking and advanced type utilities
|
||||
- 🎯 **Container Registry**: Publishing to GitHub Container Registry (GHCR)
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
### Antigravity (Google)
|
||||
|
||||
We welcome contributions! If you're experiencing issues with the NPM package or Docker version, please:
|
||||
Config location: `~/.gemini/antigravity/mcp_config.json`
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
**Local (node)**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw": {
|
||||
"command": "node",
|
||||
"args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"],
|
||||
"env": {
|
||||
"EXPRESS_SERVER_URL": "http://localhost:3000",
|
||||
"ENABLE_CANVAS_SYNC": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 License
|
||||
**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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
### Notes
|
||||
|
||||
- **Excalidraw Team** - For the amazing drawing library
|
||||
- **MCP Community** - For the Model Context Protocol specification
|
||||
- **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-mcp/` 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-mcp ~/.codex/skills/excalidraw-mcp
|
||||
```
|
||||
|
||||
To update an existing installation, remove the old folder first (`rm -rf ~/.codex/skills/excalidraw-mcp`) then re-copy.
|
||||
|
||||
### Install The Skill (Claude Code)
|
||||
|
||||
**User-level** (available across all your projects):
|
||||
```bash
|
||||
mkdir -p ~/.claude/skills
|
||||
cp -R skills/excalidraw-mcp ~/.claude/skills/excalidraw-mcp
|
||||
```
|
||||
|
||||
**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-mcp /path/to/your/project/.claude/skills/excalidraw-mcp
|
||||
```
|
||||
|
||||
Then invoke the skill in Claude Code with `/excalidraw-mcp`.
|
||||
|
||||
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://localhost:3000`) or accept `--url`.
|
||||
|
||||
```bash
|
||||
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-mcp/scripts/healthcheck.cjs
|
||||
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-mcp/scripts/export-elements.cjs --out diagram.elements.json
|
||||
EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-mcp/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-mcp/SKILL.md` and `skills/excalidraw-mcp/references/cheatsheet.md`.
|
||||
|
||||
## MCP Tools (High Level)
|
||||
|
||||
The MCP server exposes tools such as:
|
||||
|
||||
- `create_element`, `update_element`, `delete_element`
|
||||
- `query_elements`, `get_resource`
|
||||
- `batch_create_elements`
|
||||
- `align_elements`, `distribute_elements`
|
||||
- `group_elements`, `ungroup_elements`
|
||||
- `lock_elements`, `unlock_elements`
|
||||
- `create_from_mermaid` (frontend converts Mermaid to Excalidraw elements)
|
||||
|
||||
The full tool list and schemas are discoverable via MCP Inspector (`tools/list`) or by reading `src/index.ts`.
|
||||
|
||||
## Testing
|
||||
|
||||
### Canvas Smoke Test (HTTP)
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
### MCP Smoke Test (MCP Inspector)
|
||||
|
||||
List tools:
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector --cli \
|
||||
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||
-e ENABLE_CANVAS_SYNC=true -- \
|
||||
node dist/index.js --method tools/list
|
||||
```
|
||||
|
||||
Create a rectangle:
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector --cli \
|
||||
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||
-e ENABLE_CANVAS_SYNC=true -- \
|
||||
node dist/index.js --method tools/call --tool-name create_element \
|
||||
--tool-arg type=rectangle --tool-arg x=100 --tool-arg y=100 \
|
||||
--tool-arg width=300 --tool-arg 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
|
||||
|
||||
The following issues are known and tracked for future improvement:
|
||||
|
||||
- [ ] **`align_elements` / `distribute_elements` are stubs**: These tools log and return success but do not actually move elements. Implementation needed in `src/index.ts:782-806`.
|
||||
- [ ] **`points` type mismatch**: The Zod schema expects `[{x, y}]` objects but Excalidraw expects `[[x, y]]` tuples. This may cause issues when creating arrows/lines via MCP. See `src/index.ts:187` vs `src/types.ts:64`.
|
||||
- [ ] **`label` element type incomplete**: The `label` type is defined in `EXCALIDRAW_ELEMENT_TYPES` but has no corresponding interface in the type union. See `src/types.ts:109,118`.
|
||||
- [ ] **HTTP transport mode placeholder**: `MCP_TRANSPORT_MODE=http` is accepted but falls back to stdio. See `src/index.ts:989-996`.
|
||||
- [ ] **`ungroup_elements` silent success**: Returns success even when no elements are ungrouped (e.g., elements not found). See `src/index.ts:765-769`.
|
||||
|
||||
Contributions welcome!
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run type-check
|
||||
npm run build
|
||||
```
|
||||
|
||||
@@ -228,7 +228,8 @@ function App(): JSX.Element {
|
||||
if (data.elements && data.elements.length > 0) {
|
||||
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
|
||||
const validatedElements = validateAndFixBindings(cleanedElements)
|
||||
const convertedElements = convertToExcalidrawElements(validatedElements)
|
||||
// Preserve server IDs so later update/delete websocket events can match by id.
|
||||
const convertedElements = convertToExcalidrawElements(validatedElements, { regenerateIds: false })
|
||||
excalidrawAPI.updateScene({
|
||||
elements: convertedElements,
|
||||
captureUpdate: CaptureUpdateAction.NEVER
|
||||
@@ -239,7 +240,8 @@ function App(): JSX.Element {
|
||||
case 'element_created':
|
||||
if (data.element) {
|
||||
const cleanedNewElement = cleanElementForExcalidraw(data.element)
|
||||
const newElement = convertToExcalidrawElements([cleanedNewElement])
|
||||
// Preserve server IDs so later update/delete websocket events can match by id.
|
||||
const newElement = convertToExcalidrawElements([cleanedNewElement], { regenerateIds: false })
|
||||
const updatedElementsAfterCreate = [...currentElements, ...newElement]
|
||||
excalidrawAPI.updateScene({
|
||||
elements: updatedElementsAfterCreate,
|
||||
@@ -251,7 +253,8 @@ function App(): JSX.Element {
|
||||
case 'element_updated':
|
||||
if (data.element) {
|
||||
const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
|
||||
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement])[0]
|
||||
// Preserve server IDs so we can replace the existing element by id.
|
||||
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement], { regenerateIds: false })[0]
|
||||
const updatedElements = currentElements.map(el =>
|
||||
el.id === data.element!.id ? convertedUpdatedElement : el
|
||||
)
|
||||
@@ -275,7 +278,8 @@ function App(): JSX.Element {
|
||||
case 'elements_batch_created':
|
||||
if (data.elements) {
|
||||
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
|
||||
const batchElements = convertToExcalidrawElements(cleanedBatchElements)
|
||||
// Preserve server IDs so later update/delete websocket events can match by id.
|
||||
const batchElements = convertToExcalidrawElements(cleanedBatchElements, { regenerateIds: false })
|
||||
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
|
||||
excalidrawAPI.updateScene({
|
||||
elements: updatedElementsAfterBatch,
|
||||
@@ -491,4 +495,4 @@ function App(): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: excalidraw-mcp
|
||||
description: Create, edit, and export live Excalidraw diagrams using mcp-excalidraw-server (MCP tools + canvas REST API). Use when an agent needs to draw/lay out diagrams, convert Mermaid to Excalidraw, query/update/delete elements, or export/import elements from a running canvas server (EXPRESS_SERVER_URL, default http://localhost:3000).
|
||||
---
|
||||
|
||||
# Excalidraw MCP
|
||||
|
||||
## Overview
|
||||
|
||||
Create and refine diagrams on a live Excalidraw canvas via MCP tools, with helper scripts for export/import workflows.
|
||||
|
||||
## Quick Start
|
||||
|
||||
- Ensure the canvas server is reachable at `EXPRESS_SERVER_URL` (default `http://localhost:3000`).
|
||||
- Use MCP tools for interactive diagram edits; use `scripts/*.cjs` for file-ish workflows (export/import/clear/health).
|
||||
- For detailed endpoint/tool reference, read `references/cheatsheet.md`.
|
||||
|
||||
## Workflow: Draw A Diagram (From Empty Canvas)
|
||||
|
||||
1. Confirm canvas is up:
|
||||
- Run `node scripts/healthcheck.cjs` (or GET `/health`).
|
||||
2. Optional: clear the canvas:
|
||||
- Run `node scripts/clear-canvas.cjs`.
|
||||
3. Create shapes first (rectangles/diamonds/ellipses), using `create_element`.
|
||||
4. Put text on shapes by setting the shape’s `text` field (do not create a separate text element unless you need standalone text).
|
||||
5. Create arrows/lines after both endpoints exist.
|
||||
6. Use `align_elements` / `distribute_elements` after rough placement; group only after layout stabilizes.
|
||||
|
||||
## Workflow: Refine An Existing Diagram
|
||||
|
||||
1. Discover what’s already there:
|
||||
- Prefer `get_resource` with `resource: "elements"` or `query_elements`.
|
||||
2. Identify targets by stable signals (id, type, label text), not by exact x/y.
|
||||
3. Update with `update_element` (move/resize/colors/text) or delete with `delete_element`.
|
||||
4. If deletes/updates “don’t work”, check:
|
||||
- You’re pointing to the right `EXPRESS_SERVER_URL`.
|
||||
- The element id exists on the canvas (use `get_resource` / `GET /api/elements/:id`).
|
||||
- The element isn’t locked (use `unlock_elements` first).
|
||||
|
||||
## Workflow: Export / Import (Repository-Friendly)
|
||||
|
||||
- Export current elements to a JSON file:
|
||||
- `node scripts/export-elements.cjs --out diagram.elements.json`
|
||||
- Import elements (append) using batch create:
|
||||
- `node scripts/import-elements.cjs --in diagram.elements.json --mode batch`
|
||||
- Import elements (overwrite canvas) using sync:
|
||||
- `node scripts/import-elements.cjs --in diagram.elements.json --mode sync`
|
||||
|
||||
Notes:
|
||||
- `--mode sync` clears the canvas and then writes the provided elements (good for “make canvas match this file”).
|
||||
- If you want stable ids across updates, keep ids in the exported JSON; if you want fresh ids, regenerate before importing.
|
||||
|
||||
## Workflow: CRUD Smoke Test (Create → Update → Delete)
|
||||
|
||||
1. Clear:
|
||||
- `node scripts/clear-canvas.cjs`
|
||||
2. Create a large visible rectangle + label:
|
||||
- Use `node scripts/create-element.cjs` twice (rectangle + text).
|
||||
3. Update:
|
||||
- Move the rectangle with `node scripts/update-element.cjs`.
|
||||
4. Delete:
|
||||
- Remove both with `node scripts/delete-element.cjs`.
|
||||
|
||||
## References
|
||||
|
||||
- `references/cheatsheet.md`: MCP tool list + REST API endpoints + payload shapes
|
||||
@@ -0,0 +1,51 @@
|
||||
# Excalidraw MCP Cheatsheet
|
||||
|
||||
## Defaults
|
||||
|
||||
- Canvas base URL: `EXPRESS_SERVER_URL` (default `http://localhost:3000`)
|
||||
- Canvas health: `GET /health`
|
||||
|
||||
## MCP Tools (Server-Side)
|
||||
|
||||
Tool names are defined in `src/index.ts`.
|
||||
|
||||
- `create_element`: create a shape/text/arrow/line
|
||||
- `update_element`: update an element by `id`
|
||||
- `delete_element`: delete an element by `id`
|
||||
- `query_elements`: query by `type` and/or exact-match filters
|
||||
- `get_resource`: `scene` | `library` | `theme` | `elements`
|
||||
- `group_elements` / `ungroup_elements`
|
||||
- `align_elements` / `distribute_elements`
|
||||
- `lock_elements` / `unlock_elements`
|
||||
- `create_from_mermaid`: send Mermaid diagram to canvas for frontend conversion
|
||||
- `batch_create_elements`: create many elements in one call
|
||||
|
||||
Notes:
|
||||
- For shapes, set the `text` field to place text inside the shape (backend converts to `label.text`).
|
||||
- Prefer creating shapes first, then arrows, then alignment/grouping.
|
||||
|
||||
## Canvas REST API (HTTP)
|
||||
|
||||
Read/write primitives (used by the MCP server and helper scripts):
|
||||
|
||||
- `GET /api/elements` -> `{ success, elements, count }`
|
||||
- `GET /api/elements/:id` -> `{ success, element }`
|
||||
- `POST /api/elements` -> `{ success, element }`
|
||||
- `PUT /api/elements/:id` -> `{ success, element }`
|
||||
- `DELETE /api/elements/:id` -> `{ success, message }`
|
||||
- `GET /api/elements/search?type=...&key=value` -> `{ success, elements, count }`
|
||||
- `POST /api/elements/batch` -> `{ success, elements, count }`
|
||||
- `POST /api/elements/from-mermaid` -> triggers websocket conversion on the frontend
|
||||
- `POST /api/elements/sync` -> clears stored elements, then writes provided ones (overwrite import)
|
||||
|
||||
## Skill Scripts
|
||||
|
||||
All scripts accept `--url <canvasUrl>` (defaults to `EXPRESS_SERVER_URL`).
|
||||
|
||||
- `node scripts/healthcheck.cjs`
|
||||
- `node scripts/clear-canvas.cjs`
|
||||
- `node scripts/export-elements.cjs --out diagram.elements.json`
|
||||
- `node scripts/import-elements.cjs --in diagram.elements.json --mode batch|sync`
|
||||
- `node scripts/create-element.cjs --data '{...}'`
|
||||
- `node scripts/update-element.cjs --id <id> --data '{...}'`
|
||||
- `node scripts/delete-element.cjs --id <id>`
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const { url } = parseArgs(process.argv.slice(2));
|
||||
const baseUrl = url.replace(/\/$/, "");
|
||||
|
||||
// Use the sync endpoint as a fast "clear" primitive (clears server storage).
|
||||
const res = await fetch(`${baseUrl}/api/elements/sync`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ elements: [], timestamp: new Date().toISOString() }),
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!res.ok || !json || json.success !== true) {
|
||||
throw new Error(`Failed to clear canvas: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`);
|
||||
}
|
||||
|
||||
console.log("Cleared canvas");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require("node:fs");
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function usage() {
|
||||
console.error(
|
||||
[
|
||||
"Usage:",
|
||||
" node scripts/create-element.cjs (--data <json> | --file <path>) [--url <canvasUrl>]",
|
||||
"",
|
||||
"Examples:",
|
||||
' node scripts/create-element.cjs --data \'{"type":"rectangle","x":100,"y":100,"width":300,"height":200}\'',
|
||||
" node scripts/create-element.cjs --file element.json",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL, data: null, file: null };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
else if (a === "--data") out.data = argv[++i];
|
||||
else if (a === "--file") out.file = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readJson({ data, file }) {
|
||||
if (data) return JSON.parse(data);
|
||||
if (file) return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
usage();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const payload = readJson(args);
|
||||
|
||||
const baseUrl = args.url.replace(/\/$/, "");
|
||||
const res = await fetch(`${baseUrl}/api/elements`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!res.ok || !json || json.success !== true) {
|
||||
throw new Error(
|
||||
`Failed to create element: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(json.element, null, 2) + "\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function usage() {
|
||||
console.error("Usage: node scripts/delete-element.cjs --id <id> [--url <canvasUrl>]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL, id: null };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
else if (a === "--id") out.id = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (!args.id) usage();
|
||||
|
||||
const baseUrl = args.url.replace(/\/$/, "");
|
||||
const res = await fetch(`${baseUrl}/api/elements/${encodeURIComponent(args.id)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!res.ok || !json || json.success !== true) {
|
||||
throw new Error(
|
||||
`Failed to delete element: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Deleted ${args.id}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL, outFile: null };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
else if (a === "--out") out.outFile = argv[++i];
|
||||
else if (a === "-o") out.outFile = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const { url, outFile } = parseArgs(process.argv.slice(2));
|
||||
const res = await fetch(`${url.replace(/\/$/, "")}/api/elements`);
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok || !json || json.success !== true) {
|
||||
throw new Error(`Failed to export elements: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
expressServerUrl: url,
|
||||
elements: json.elements || [],
|
||||
};
|
||||
|
||||
const text = JSON.stringify(payload, null, 2);
|
||||
if (!outFile) {
|
||||
process.stdout.write(text + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||||
fs.writeFileSync(outFile, text + "\n");
|
||||
console.log(`Wrote ${payload.elements.length} elements to ${outFile}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const { url } = parseArgs(process.argv.slice(2));
|
||||
const res = await fetch(`${url.replace(/\/$/, "")}/health`);
|
||||
const text = await res.text();
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(text);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require("node:fs");
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function usage() {
|
||||
console.error(
|
||||
[
|
||||
"Usage:",
|
||||
" node scripts/import-elements.cjs --in <file> [--mode batch|sync] [--url <canvasUrl>]",
|
||||
"",
|
||||
"Modes:",
|
||||
" batch POST /api/elements/batch (append; creates elements)",
|
||||
" sync POST /api/elements/sync (overwrite; clears then writes)",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL, inFile: null, mode: "batch" };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
else if (a === "--in") out.inFile = argv[++i];
|
||||
else if (a === "--mode") out.mode = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readElementsFromFile(inFile) {
|
||||
const raw = fs.readFileSync(inFile, "utf8");
|
||||
const data = JSON.parse(raw);
|
||||
if (Array.isArray(data)) return data;
|
||||
if (data && Array.isArray(data.elements)) return data.elements;
|
||||
throw new Error('Input file must be an array, or an object with an "elements" array');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const { url, inFile, mode } = parseArgs(process.argv.slice(2));
|
||||
if (!inFile) usage();
|
||||
if (mode !== "batch" && mode !== "sync") usage();
|
||||
|
||||
const elements = readElementsFromFile(inFile);
|
||||
const baseUrl = url.replace(/\/$/, "");
|
||||
|
||||
let endpoint;
|
||||
let body;
|
||||
if (mode === "batch") {
|
||||
endpoint = `${baseUrl}/api/elements/batch`;
|
||||
body = { elements };
|
||||
} else {
|
||||
endpoint = `${baseUrl}/api/elements/sync`;
|
||||
body = { elements, timestamp: new Date().toISOString() };
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!res.ok || !json || json.success !== true) {
|
||||
throw new Error(`Failed to import elements: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`);
|
||||
}
|
||||
|
||||
const count = json.count ?? json.elements?.length ?? elements.length;
|
||||
console.log(`Imported ${count} elements (${mode})`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require("node:fs");
|
||||
|
||||
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
||||
|
||||
function usage() {
|
||||
console.error(
|
||||
[
|
||||
"Usage:",
|
||||
" node scripts/update-element.cjs --id <id> (--data <json> | --file <path>) [--url <canvasUrl>]",
|
||||
"",
|
||||
"Examples:",
|
||||
' node scripts/update-element.cjs --id abc --data \'{"x":200,"y":250,"backgroundColor":"#ffeeee"}\'',
|
||||
" node scripts/update-element.cjs --id abc --file updates.json",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { url: DEFAULT_URL, id: null, data: null, file: null };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "--url") out.url = argv[++i];
|
||||
else if (a === "--id") out.id = argv[++i];
|
||||
else if (a === "--data") out.data = argv[++i];
|
||||
else if (a === "--file") out.file = argv[++i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readJson({ data, file }) {
|
||||
if (data) return JSON.parse(data);
|
||||
if (file) return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
usage();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (typeof fetch !== "function") {
|
||||
throw new Error("This script requires Node 18+ (global fetch).");
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (!args.id) usage();
|
||||
const payload = readJson(args);
|
||||
|
||||
const baseUrl = args.url.replace(/\/$/, "");
|
||||
const res = await fetch(`${baseUrl}/api/elements/${encodeURIComponent(args.id)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!res.ok || !json || json.success !== true) {
|
||||
throw new Error(
|
||||
`Failed to update element: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(json.element, null, 2) + "\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user