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:
yctimlin
2026-01-26 00:17:18 +08:00
committed by GitHub
co-authored by YC Lin
parent 45af2758cf
commit d73fa53cde
11 changed files with 845 additions and 554 deletions
+323 -549
View File
@@ -1,324 +1,101 @@
# MCP Excalidraw Server: Advanced Live Visual Diagramming with AI Integration
# Excalidraw MCP Server & Agent Skill
[![CI](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml/badge.svg)](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml)
[![Docker Build & Push](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml/badge.svg)](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml)
[![NPM Version](https://img.shields.io/npm/v/mcp-excalidraw-server)](https://www.npmjs.com/package/mcp-excalidraw-server)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
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!**
[![MCP Excalidraw Demo](https://img.youtube.com/vi/RRN7AF7QIew/maxresdefault.jpg)](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
```
+9 -5
View File
@@ -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
+66
View File
@@ -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 shapes `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 whats 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 “dont work”, check:
- Youre pointing to the right `EXPRESS_SERVER_URL`.
- The element id exists on the canvas (use `get_resource` / `GET /api/elements/:id`).
- The element isnt 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);
});