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/ci.yml)
|
||||||
[](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml)
|
[](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml)
|
||||||
[](https://www.npmjs.com/package/mcp-excalidraw-server)
|
[](https://www.npmjs.com/package/mcp-excalidraw-server)
|
||||||
[](LICENSE)
|
[](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 |
|
## Table of Contents
|
||||||
|-----------|-------|--------|--------|
|
|
||||||
| **Canvas Server** | ✅ Fully Working | ✅ Fully Working | **Production Ready** |
|
|
||||||
| **MCP Server** | ✅ Fully Working | ✅ Fully Working | **Production Ready** |
|
|
||||||
| **NPM Published** | 🔧 In Progress | N/A | Development testing |
|
|
||||||
|
|
||||||
### **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)
|
This repo contains two separate processes:
|
||||||
2. **MCP Server** - Connects to Claude Desktop/Claude Code/Cursor IDE
|
|
||||||
|
|
||||||
**You can choose any combination:**
|
- Canvas server: web UI + REST API + WebSocket updates (default `http://localhost:3000`)
|
||||||
- Canvas: Local OR Docker
|
- MCP server: exposes MCP tools over stdio; syncs to the canvas via `EXPRESS_SERVER_URL`
|
||||||
- MCP Server: Local OR Docker
|
|
||||||
|
|
||||||
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
|
## Quick Start (Local)
|
||||||
- **🤖 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
|
|
||||||
|
|
||||||
## 🎥 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
|
```bash
|
||||||
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start Canvas Server**
|
Terminal 1: start the canvas
|
||||||
```bash
|
```bash
|
||||||
# Production mode (recommended)
|
HOST=0.0.0.0 PORT=3000 npm run canvas
|
||||||
npm run canvas
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Access the Canvas**
|
Open `http://localhost:3000`.
|
||||||
```
|
|
||||||
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
|
```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
|
docker run -d -p 3000:3000 --name mcp-excalidraw-canvas ghcr.io/yctimlin/mcp_excalidraw-canvas:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option B2: Build Locally**
|
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.
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Access the Canvas**
|
## Configure MCP Clients
|
||||||
```
|
|
||||||
http://localhost:3000
|
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.
|
Config location:
|
||||||
|
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
#### **Setup Combinations**
|
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
||||||
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
|
|
||||||
|
|
||||||
|
**Local (node)**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
@@ -334,22 +111,15 @@ Edit your `claude_desktop_config.json` file:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:** Replace `/absolute/path/to/mcp_excalidraw` with your actual installation path.
|
**Docker**
|
||||||
|
|
||||||
### **Format 2: Docker MCP Server** ✅ Fully Working
|
|
||||||
|
|
||||||
**Using Pre-built Image from GHCR** (Recommended):
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"excalidraw": {
|
"excalidraw": {
|
||||||
"command": "docker",
|
"command": "docker",
|
||||||
"args": [
|
"args": [
|
||||||
"run",
|
"run", "-i", "--rm",
|
||||||
"-i",
|
"-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
|
||||||
"--rm",
|
|
||||||
"--network", "host",
|
|
||||||
"-e", "EXPRESS_SERVER_URL=http://localhost:3000",
|
|
||||||
"-e", "ENABLE_CANVAS_SYNC=true",
|
"-e", "ENABLE_CANVAS_SYNC=true",
|
||||||
"ghcr.io/yctimlin/mcp_excalidraw:latest"
|
"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
|
**Local (node)** - User-level (available across all projects):
|
||||||
|
|
||||||
```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**:
|
|
||||||
```bash
|
```bash
|
||||||
cd mcp_excalidraw
|
claude mcp add excalidraw --scope user \
|
||||||
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 \
|
|
||||||
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||||
-e ENABLE_CANVAS_SYNC=true \
|
-e ENABLE_CANVAS_SYNC=true \
|
||||||
mcp-excalidraw
|
-- node /absolute/path/to/mcp_excalidraw/dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
# User-scoped (available across all projects)
|
**Local (node)** - Project-level (shared via `.mcp.json`):
|
||||||
claude mcp add --scope user --transport stdio excalidraw \
|
```bash
|
||||||
-- docker run -i --rm --network host \
|
claude mcp add excalidraw --scope project \
|
||||||
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
-e EXPRESS_SERVER_URL=http://localhost:3000 \
|
||||||
-e ENABLE_CANVAS_SYNC=true \
|
-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`:
|
Config location: `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` for global config)
|
||||||
|
|
||||||
### **Format 1: Local MCP Server** ⭐ Recommended
|
|
||||||
|
|
||||||
|
**Local (node)**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
@@ -462,20 +187,15 @@ Edit `.cursor/mcp.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Format 2: Docker MCP Server** ✅ Fully Working
|
**Docker**
|
||||||
|
|
||||||
**Using Pre-built Image from GHCR** (Recommended):
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"excalidraw": {
|
"excalidraw": {
|
||||||
"command": "docker",
|
"command": "docker",
|
||||||
"args": [
|
"args": [
|
||||||
"run",
|
"run", "-i", "--rm",
|
||||||
"-i",
|
"-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000",
|
||||||
"--rm",
|
|
||||||
"--network", "host",
|
|
||||||
"-e", "EXPRESS_SERVER_URL=http://localhost:3000",
|
|
||||||
"-e", "ENABLE_CANVAS_SYNC=true",
|
"-e", "ENABLE_CANVAS_SYNC=true",
|
||||||
"ghcr.io/yctimlin/mcp_excalidraw:latest"
|
"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
|
```bash
|
||||||
cd mcp_excalidraw
|
codex mcp add excalidraw \
|
||||||
docker build -f Dockerfile -t mcp-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 |
|
Config location: `~/.config/opencode/opencode.json` or project-level `opencode.json`
|
||||||
|---------|---------|----------|
|
|
||||||
| `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 |
|
|
||||||
|
|
||||||
**Canvas is optional**: The MCP server works without the canvas in API-only mode (for programmatic access only).
|
**Local (node)**
|
||||||
|
```json
|
||||||
## 🛠️ Environment Variables
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
| Variable | Default | Description |
|
"mcp": {
|
||||||
|----------|---------|-------------|
|
"excalidraw": {
|
||||||
| `EXPRESS_SERVER_URL` | `http://localhost:3000` | Canvas server URL for MCP sync |
|
"type": "local",
|
||||||
| `ENABLE_CANVAS_SYNC` | `true` | Enable/disable canvas synchronization |
|
"command": ["node", "/absolute/path/to/mcp_excalidraw/dist/index.js"],
|
||||||
| `LOG_FILE_PATH` | `excalidraw.log` | Path to the log file |
|
"enabled": true,
|
||||||
| `DEBUG` | `false` | Enable debug logging |
|
"environment": {
|
||||||
| `PORT` | `3000` | Canvas server port |
|
"EXPRESS_SERVER_URL": "http://localhost:3000",
|
||||||
| `HOST` | `localhost` | Canvas server host |
|
"ENABLE_CANVAS_SYNC": "true"
|
||||||
|
}
|
||||||
## 📊 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔮 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
|
**Local (node)**
|
||||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
```json
|
||||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
{
|
||||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
"mcpServers": {
|
||||||
5. Open a Pull Request
|
"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
|
- **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`.
|
||||||
- **MCP Community** - For the Model Context Protocol specification
|
- **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) {
|
if (data.elements && data.elements.length > 0) {
|
||||||
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
|
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
|
||||||
const validatedElements = validateAndFixBindings(cleanedElements)
|
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({
|
excalidrawAPI.updateScene({
|
||||||
elements: convertedElements,
|
elements: convertedElements,
|
||||||
captureUpdate: CaptureUpdateAction.NEVER
|
captureUpdate: CaptureUpdateAction.NEVER
|
||||||
@@ -239,7 +240,8 @@ function App(): JSX.Element {
|
|||||||
case 'element_created':
|
case 'element_created':
|
||||||
if (data.element) {
|
if (data.element) {
|
||||||
const cleanedNewElement = cleanElementForExcalidraw(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]
|
const updatedElementsAfterCreate = [...currentElements, ...newElement]
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: updatedElementsAfterCreate,
|
elements: updatedElementsAfterCreate,
|
||||||
@@ -251,7 +253,8 @@ function App(): JSX.Element {
|
|||||||
case 'element_updated':
|
case 'element_updated':
|
||||||
if (data.element) {
|
if (data.element) {
|
||||||
const cleanedUpdatedElement = cleanElementForExcalidraw(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 =>
|
const updatedElements = currentElements.map(el =>
|
||||||
el.id === data.element!.id ? convertedUpdatedElement : el
|
el.id === data.element!.id ? convertedUpdatedElement : el
|
||||||
)
|
)
|
||||||
@@ -275,7 +278,8 @@ function App(): JSX.Element {
|
|||||||
case 'elements_batch_created':
|
case 'elements_batch_created':
|
||||||
if (data.elements) {
|
if (data.elements) {
|
||||||
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
|
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]
|
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: updatedElementsAfterBatch,
|
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