diff --git a/README.md b/README.md index b6ebf19..8d333a9 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5e1c78e..da2ee53 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 \ No newline at end of file +export default App diff --git a/skills/excalidraw-mcp/SKILL.md b/skills/excalidraw-mcp/SKILL.md new file mode 100644 index 0000000..74d9236 --- /dev/null +++ b/skills/excalidraw-mcp/SKILL.md @@ -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 diff --git a/skills/excalidraw-mcp/references/cheatsheet.md b/skills/excalidraw-mcp/references/cheatsheet.md new file mode 100644 index 0000000..05fc8fe --- /dev/null +++ b/skills/excalidraw-mcp/references/cheatsheet.md @@ -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 ` (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 --data '{...}'` +- `node scripts/delete-element.cjs --id ` diff --git a/skills/excalidraw-mcp/scripts/clear-canvas.cjs b/skills/excalidraw-mcp/scripts/clear-canvas.cjs new file mode 100644 index 0000000..718d9d6 --- /dev/null +++ b/skills/excalidraw-mcp/scripts/clear-canvas.cjs @@ -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); +}); diff --git a/skills/excalidraw-mcp/scripts/create-element.cjs b/skills/excalidraw-mcp/scripts/create-element.cjs new file mode 100644 index 0000000..54b82f3 --- /dev/null +++ b/skills/excalidraw-mcp/scripts/create-element.cjs @@ -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 | --file ) [--url ]", + "", + "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); +}); + diff --git a/skills/excalidraw-mcp/scripts/delete-element.cjs b/skills/excalidraw-mcp/scripts/delete-element.cjs new file mode 100644 index 0000000..cb5c245 --- /dev/null +++ b/skills/excalidraw-mcp/scripts/delete-element.cjs @@ -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 [--url ]"); + 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); +}); + diff --git a/skills/excalidraw-mcp/scripts/export-elements.cjs b/skills/excalidraw-mcp/scripts/export-elements.cjs new file mode 100644 index 0000000..ae61fb7 --- /dev/null +++ b/skills/excalidraw-mcp/scripts/export-elements.cjs @@ -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); +}); diff --git a/skills/excalidraw-mcp/scripts/healthcheck.cjs b/skills/excalidraw-mcp/scripts/healthcheck.cjs new file mode 100644 index 0000000..58d4e06 --- /dev/null +++ b/skills/excalidraw-mcp/scripts/healthcheck.cjs @@ -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); +}); diff --git a/skills/excalidraw-mcp/scripts/import-elements.cjs b/skills/excalidraw-mcp/scripts/import-elements.cjs new file mode 100644 index 0000000..1334d2e --- /dev/null +++ b/skills/excalidraw-mcp/scripts/import-elements.cjs @@ -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 [--mode batch|sync] [--url ]", + "", + "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); +}); diff --git a/skills/excalidraw-mcp/scripts/update-element.cjs b/skills/excalidraw-mcp/scripts/update-element.cjs new file mode 100644 index 0000000..240b03b --- /dev/null +++ b/skills/excalidraw-mcp/scripts/update-element.cjs @@ -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 (--data | --file ) [--url ]", + "", + "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); +}); +