From 913f9b89b7de51aa8e1874064f3f9459df4474e5 Mon Sep 17 00:00:00 2001 From: yctimlin Date: Fri, 13 Feb 2026 00:20:01 +0800 Subject: [PATCH] Updates to Excalidraw MCP server and documentation (#43) --- README.md | 20 +- skills/excalidraw-skill/SKILL.md | 218 ++++++++++++++++-- .../excalidraw-skill/references/cheatsheet.md | 5 +- src/index.ts | 3 + src/server.ts | 6 + 5 files changed, 224 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f9f903b..4fbf598 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Keywords: Excalidraw agent skill, Excalidraw MCP server, AI diagramming, Claude - [OpenCode](#opencode) - [Antigravity (Google)](#antigravity-google) - [Agent Skill (Optional)](#agent-skill-optional) -- [MCP Tools (23 Total)](#mcp-tools-23-total) +- [MCP Tools (26 Total)](#mcp-tools-26-total) - [Testing](#testing) - [Troubleshooting](#troubleshooting) - [Known Issues / TODO](#known-issues--todo) @@ -53,7 +53,7 @@ Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mc | | Official Excalidraw MCP | This Project | |---|---|---| -| **Approach** | Prompt in, diagram out (one-shot) | Programmatic element-level control (23 tools) | +| **Approach** | Prompt in, diagram out (one-shot) | Programmatic element-level control (26 tools) | | **State** | Stateless — each call is independent | Persistent live canvas with real-time sync | | **Element CRUD** | No | Full create / read / update / delete per element | | **AI sees the canvas** | No | `describe_scene` (structured text) + `get_canvas_screenshot` (image) | @@ -62,8 +62,12 @@ Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mc | **File I/O** | No | `export_scene` / `import_scene` (.excalidraw JSON) | | **Snapshot & rollback** | No | `snapshot_scene` / `restore_snapshot` | | **Mermaid conversion** | No | `create_from_mermaid` | +| **Shareable URLs** | Yes | Yes — `export_to_excalidraw_url` | +| **Design guide** | `read_me` cheat sheet | `read_diagram_guide` (colors, sizing, layout, anti-patterns) | +| **Viewport control** | Camera animations | `set_viewport` (zoom-to-fit, center on element, manual zoom) | | **Live canvas UI** | Rendered inline in chat | Standalone Excalidraw app synced via WebSocket | | **Multi-agent** | Single user | Multiple agents can draw on the same canvas concurrently | +| **Works without MCP** | No | Yes — REST API fallback via agent skill | **TL;DR** — The official MCP generates diagrams. We give AI agents a full canvas toolkit to build, inspect, and iteratively refine diagrams — including the ability to see what they drew. @@ -71,10 +75,14 @@ Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mc ### v2.0 — Canvas Toolkit -- 10 new MCP tools: `get_element`, `clear_canvas`, `export_scene`, `import_scene`, `export_to_image`, `duplicate_elements`, `snapshot_scene`, `restore_snapshot`, `describe_scene`, `get_canvas_screenshot` (23 tools total) +- 13 new MCP tools (26 total): `get_element`, `clear_canvas`, `export_scene`, `import_scene`, `export_to_image`, `duplicate_elements`, `snapshot_scene`, `restore_snapshot`, `describe_scene`, `get_canvas_screenshot`, `read_diagram_guide`, `export_to_excalidraw_url`, `set_viewport` - **Closed feedback loop**: AI can now inspect the canvas (`describe_scene`) and see it (`get_canvas_screenshot` returns an image) — enabling iterative refinement +- **Design guide**: `read_diagram_guide` returns best-practice color palettes, sizing rules, layout patterns, and anti-patterns — dramatically improves AI-generated diagram quality +- **Shareable URLs**: `export_to_excalidraw_url` encrypts and uploads the scene to excalidraw.com, returns a shareable link anyone can open +- **Viewport control**: `set_viewport` with `scrollToContent`, `scrollToElementId`, or manual zoom/offset — agents can auto-fit diagrams after creation - **File I/O**: export/import full `.excalidraw` JSON files - **Snapshots**: save and restore named canvas states +- **Skill fallback**: Agent skill auto-detects MCP vs REST API mode, gracefully falls back to HTTP endpoints when MCP server isn't configured - Fixed all previously known issues: `align_elements` / `distribute_elements` fully implemented, points type normalization, removed invalid `label` type, removed HTTP transport dead code, `ungroup_elements` now errors on failure ### v1.x @@ -413,15 +421,17 @@ EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-skill/scripts/im See `skills/excalidraw-skill/SKILL.md` and `skills/excalidraw-skill/references/cheatsheet.md`. -## MCP Tools (23 Total) +## MCP Tools (26 Total) | Category | Tools | |---|---| | **Element CRUD** | `create_element`, `get_element`, `update_element`, `delete_element`, `query_elements`, `batch_create_elements`, `duplicate_elements` | | **Layout** | `align_elements`, `distribute_elements`, `group_elements`, `ungroup_elements`, `lock_elements`, `unlock_elements` | | **Scene Awareness** | `describe_scene`, `get_canvas_screenshot` | -| **File I/O** | `export_scene`, `import_scene`, `export_to_image`, `create_from_mermaid` | +| **File I/O** | `export_scene`, `import_scene`, `export_to_image`, `export_to_excalidraw_url`, `create_from_mermaid` | | **State Management** | `clear_canvas`, `snapshot_scene`, `restore_snapshot` | +| **Viewport** | `set_viewport` | +| **Design Guide** | `read_diagram_guide` | | **Resources** | `get_resource` | Full schemas are discoverable via `tools/list` or in `skills/excalidraw-skill/references/cheatsheet.md`. diff --git a/skills/excalidraw-skill/SKILL.md b/skills/excalidraw-skill/SKILL.md index 09259f7..91c2142 100644 --- a/skills/excalidraw-skill/SKILL.md +++ b/skills/excalidraw-skill/SKILL.md @@ -5,28 +5,143 @@ description: Programmatic canvas toolkit for creating, editing, and refining Exc # Excalidraw Skill +## Step 0: Detect Connection Mode + +Before doing anything, determine which mode is available. Run these checks **in order**: + +### Check 1: MCP Server (Best experience) +```bash +mcp-cli tools | grep excalidraw +``` +If you see tools like `excalidraw/batch_create_elements` → **use MCP mode**. Call MCP tools directly. + +### Check 2: REST API (Fallback — works without MCP server) +```bash +curl -s http://localhost:3000/health +``` +If you get `{"status":"ok"}` → **use REST API mode**. Use HTTP endpoints (`curl` / `fetch`) from the cheatsheet. + +### Check 3: Nothing works → Guide user to install +If neither works, tell the user: +> The Excalidraw canvas server is not running. To set up: +> 1. Clone: `git clone https://github.com/yctimlin/mcp_excalidraw && cd mcp_excalidraw` +> 2. Build: `npm ci && npm run build` +> 3. Start canvas: `HOST=0.0.0.0 PORT=3000 npm run canvas` +> 4. Open `http://localhost:3000` in a browser +> 5. (Recommended) Install the MCP server for the best experience: +> ``` +> claude mcp add excalidraw -s user -e EXPRESS_SERVER_URL=http://localhost:3000 -- node /path/to/mcp_excalidraw/dist/index.js +> ``` + +### MCP vs REST API Quick Reference + +| Operation | MCP Tool | REST API Equivalent | +|-----------|----------|-------------------| +| Create elements | `batch_create_elements` | `POST /api/elements/batch` with `{"elements": [...]}` | +| Get all elements | `query_elements` | `GET /api/elements` | +| Get one element | `get_element` | `GET /api/elements/:id` | +| Update element | `update_element` | `PUT /api/elements/:id` | +| Delete element | `delete_element` | `DELETE /api/elements/:id` | +| Clear canvas | `clear_canvas` | `DELETE /api/elements/clear` | +| Describe scene | `describe_scene` | `GET /api/elements` (parse manually) | +| Export scene | `export_scene` | `GET /api/elements` (save to file) | +| Import scene | `import_scene` | `POST /api/elements/sync` with `{"elements": [...]}` | +| Snapshot | `snapshot_scene` | `POST /api/snapshots` with `{"name": "..."}` | +| Restore snapshot | `restore_snapshot` | `GET /api/snapshots/:name` then `POST /api/elements/sync` | +| Screenshot | `get_canvas_screenshot` | Only via MCP (needs browser) | +| Design guide | `read_diagram_guide` | Not available — see cheatsheet for guidelines | +| Viewport | `set_viewport` | `POST /api/viewport` (needs browser) | +| Export image | `export_to_image` | `POST /api/export/image` (needs browser) | +| Export URL | `export_to_excalidraw_url` | Only via MCP | + +### REST API Gotchas (Critical — read before using REST API) + +1. **Labels**: Use `"label": {"text": "My Label"}` (not `"text": "My Label"`). MCP tools auto-convert, REST API does not. +2. **Arrow binding**: Use `"start": {"id": "svc-a"}, "end": {"id": "svc-b"}` (not `"startElementId"`/`"endElementId"`). MCP tools accept `startElementId` and convert, REST API requires the `start`/`end` object format directly. +3. **fontFamily**: Must be a string (e.g. `"1"`) or omit it entirely. Do NOT pass a number like `1`. +4. **Updating labels**: When updating a shape via `PUT /api/elements/:id`, include the full `label` in the update body to preserve it. Omitting `label` from the update won't delete it, but re-sending ensures it renders correctly. +5. **Screenshot in REST mode**: `POST /api/export/image` returns `{"data": ""}`. Save to file and read it back for visual verification. Requires browser open. + +## Quality Gate (MANDATORY — read before creating any diagram) + +**After EVERY iteration (each batch of elements added), you MUST run a quality check before proceeding. NEVER say "looks great" unless ALL checks pass.** + +### Quality Checklist — verify ALL before adding more elements: +1. **Text truncation**: Is ALL text fully visible? Labels must fit inside their shapes. If text is cut off or wrapping badly → increase `width` and/or `height`. +2. **Overlap**: Do ANY elements overlap each other? Check that no rectangles, ellipses, or text elements share the same space. Background zones must fully contain their children with padding. +3. **Arrow crossing**: Do arrows cross through unrelated elements or overlap with text labels? If yes → **use curved/elbowed arrows with waypoints** to route around obstacles (see "Arrow Routing" section). Never accept crossing arrows. +4. **Arrow-text overlap**: Do any arrow labels ("charge", "event", etc.) overlap with shapes? Arrow labels are positioned at the midpoint — if they overlap, either remove the label, shorten it, or adjust the arrow path. +5. **Spacing**: Is there at least 40px gap between elements? Cramped layouts are unreadable. +6. **Readability**: Can all labels be read at normal zoom? Font size >= 16 for body text, >= 20 for titles. + +### If ANY issue is found: +- **STOP adding new elements** +- Fix the issue first (resize, reposition, delete and recreate) +- Re-verify with a new screenshot +- Only proceed to next iteration after ALL checks pass + +### Sizing Rules (prevent truncation): +- **Shape width**: `max(160, labelTextLength * 9)` pixels. For multi-word labels like "API Gateway (Kong)", count all characters. +- **Shape height**: 60px for single line, 80px for 2 lines, 100px for 3 lines. +- **Background zones**: Add 50px padding on ALL sides around contained elements. +- **Element spacing**: 60px vertical between tiers, 40px horizontal between siblings. +- **Side panels**: Place at least 80px away from main diagram elements. +- **Arrow labels**: Keep labels short (1-2 words). Long arrow labels overlap with other elements. + +### Layout Planning (prevent overlap): +Before creating elements, **plan your coordinate grid** on paper first: +- Tier 1 (y=50-130): Client apps +- Tier 2 (y=200-280): Gateway/Edge +- Tier 3 (y=350-440): Services (spread wide: each service ~180px apart) +- Tier 4 (y=510-590): Data stores +- Side panels: x < 0 (left) or x > mainDiagramRight + 80 (right) + +**Do NOT place side panels (observability, external APIs) at the same x-range as the main diagram — they WILL overlap.** + ## Quick Start -1. Ensure canvas server is reachable at `EXPRESS_SERVER_URL` (default `http://localhost:3000`). +1. Run **Step 0** above to detect your connection mode. 2. Open the canvas URL in a browser (required for image export/screenshot). -3. Use MCP tools for all diagram operations; use `scripts/*.cjs` for CLI workflows. +3. **MCP mode**: Use MCP tools for all operations. **REST mode**: Use HTTP endpoints from cheatsheet. 4. For full tool/endpoint reference, read `references/cheatsheet.md`. ## Workflow: Draw A Diagram -1. **Call `read_diagram_guide`** first to load design best practices (colors, sizing, layout, anti-patterns). -2. Confirm canvas: `node scripts/healthcheck.cjs` or `GET /health`. +### MCP Mode +1. **Call `read_diagram_guide`** first to load design best practices. +2. **Plan your coordinate grid** (see Quality Gate → Layout Planning) before writing any JSON. 3. Optional: `clear_canvas` to start fresh. -4. Use `batch_create_elements` with all shapes AND arrows in one call. +4. Use `batch_create_elements` with shapes AND arrows in one call. 5. **Assign custom `id` to shapes** (e.g. `"id": "auth-svc"`). Set `text` field to label shapes. -6. **Bind arrows to shapes** using `startElementId` / `endElementId` — arrows auto-route to element edges. -7. `align_elements` / `distribute_elements` after rough placement. -8. `set_viewport` with `scrollToContent: true` to auto-fit the diagram in view. -9. `describe_scene` to verify layout. `get_canvas_screenshot` to visually check. +6. **Size shapes for their text** — use `width: max(160, textLength * 9)`. +7. **Bind arrows** using `startElementId` / `endElementId` — arrows auto-route. +8. `set_viewport` with `scrollToContent: true` to auto-fit the diagram. +9. **Run Quality Checklist** — `get_canvas_screenshot` and critically evaluate. Fix issues before proceeding. + +### REST API Mode +1. Read `references/cheatsheet.md` for design guidelines. +2. **Plan your coordinate grid** (see Quality Gate → Layout Planning) before writing any JSON. +3. Optional: `curl -X DELETE http://localhost:3000/api/elements/clear` +4. Create elements in one call (use `@file.json` for large payloads): + ```bash + curl -X POST http://localhost:3000/api/elements/batch \ + -H "Content-Type: application/json" \ + -d '{"elements": [ + {"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 160, "height": 60, "label": {"text": "Service A"}}, + {"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 160, "height": 60, "label": {"text": "Service B"}}, + {"type": "arrow", "x": 0, "y": 0, "start": {"id": "svc-a"}, "end": {"id": "svc-b"}} + ]}' + ``` +5. **Use `"label": {"text": "..."}` for shape labels** (not `"text": "..."`). +6. **Bind arrows with `"start": {"id": "..."}` / `"end": {"id": "..."}`** — server auto-routes edges. +7. **Size shapes for their text** — use `width: max(160, labelTextLength * 9)`. +8. **Run Quality Checklist** — take screenshot, critically evaluate. Fix issues before adding more elements. ### Arrow Binding (Recommended) -Use `startElementId` and `endElementId` on arrows to bind them to shapes. The server automatically calculates edge-to-edge routing with proper gaps. Example: +Bind arrows to shapes for auto-routed edges. The format differs between MCP and REST API: + +**MCP Mode** — use `startElementId` / `endElementId`: ```json {"elements": [ {"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "text": "Service A"}, @@ -34,24 +149,83 @@ Use `startElementId` and `endElementId` on arrows to bind them to shapes. The se {"type": "arrow", "x": 0, "y": 0, "startElementId": "svc-a", "endElementId": "svc-b", "text": "calls"} ]} ``` -Arrows without `startElementId`/`endElementId` use manual `x`, `y`, `points` coordinates. + +**REST API Mode** — use `start: {id}` / `end: {id}` and `label: {text}`: +```json +{"elements": [ + {"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "label": {"text": "Service A"}}, + {"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 120, "height": 60, "label": {"text": "Service B"}}, + {"type": "arrow", "x": 0, "y": 0, "start": {"id": "svc-a"}, "end": {"id": "svc-b"}} +]} +``` + +Arrows without binding use manual `x`, `y`, `points` coordinates. + +### Arrow Routing — Avoid Overlaps (Critical for complex diagrams) + +Straight arrows (2-point) cause crossing and overlap in complex diagrams. **Use curved or elbowed arrows instead:** + +**Option 1: Curved arrows** — add intermediate waypoints + `roundness`: +```json +{ + "type": "arrow", "x": 100, "y": 100, + "points": [[0, 0], [50, -40], [200, 0]], + "roundness": {"type": 2}, + "strokeColor": "#1971c2" +} +``` +The waypoint `[50, -40]` pushes the arrow upward to arc over elements. `roundness: {type: 2}` makes it a smooth curve. + +**Option 2: Elbowed arrows** — right-angle routing (L-shaped or Z-shaped): +```json +{ + "type": "arrow", "x": 100, "y": 100, + "points": [[0, 0], [0, -50], [200, -50], [200, 0]], + "roundness": {"type": 2}, + "strokeColor": "#1971c2" +} +``` + +**When to use which:** +- **Fan-out arrows** (one source → many targets): Use curved arrows with waypoints spread vertically to avoid overlapping each other. +- **Cross-lane arrows** (connecting to side panels): Use elbowed arrows that route around the main diagram — go UP first, then ACROSS, then DOWN. +- **Inter-service arrows** (horizontal connections): Use curved arrows with a slight vertical offset to avoid crossing through adjacent elements. + +**Rule of thumb:** If an arrow would cross through an unrelated element, add a waypoint to route around it. Never accept crossing arrows — always fix them. ## Workflow: Iterative Refinement (Key Differentiator) -The feedback loop that makes this skill unique: +The feedback loop that makes this skill unique. **Each iteration MUST include a quality check.** -1. `describe_scene` -- read what's on the canvas (types, positions, labels, connections). -2. Decide what to change based on the description. -3. Apply changes (`update_element`, `align_elements`, `create_element`, etc.). -4. `get_canvas_screenshot` -- visually verify the result (returns PNG to multimodal AI). -5. Repeat until satisfied. +### MCP Mode (full feedback loop) +1. Add elements (`batch_create_elements`, `create_element`). +2. `set_viewport` with `scrollToContent: true`. +3. `get_canvas_screenshot` — **critically evaluate** against the Quality Checklist. +4. **If issues found** → fix them (`update_element`, `delete_element`, resize, reposition). +5. `get_canvas_screenshot` again — re-verify fix. +6. **Only proceed to next iteration when ALL quality checks pass.** -Example flow: +### REST API Mode (partial feedback loop) +1. Add elements via `POST /api/elements/batch`. +2. `POST /api/viewport` with `{"scrollToContent": true}`. +3. Take screenshot: `POST /api/export/image` → save PNG → **critically evaluate** against Quality Checklist. +4. **If issues found** → fix via `PUT /api/elements/:id` or delete and recreate. +5. Re-screenshot and re-verify. +6. **Only proceed to next iteration when ALL quality checks pass.** + +### How to critically evaluate a screenshot: +- Look at EVERY label — is any text cut off or overflowing its container? +- Look at EVERY arrow — does any arrow pass through an unrelated element? +- Look at ALL element pairs — do any overlap or touch? +- Look at spacing — is anything crammed together? +- **Be honest.** If you see ANY issue, say "I see [issue], fixing it" — not "looks great". + +Example flow (MCP): ``` -create_element (rectangles, arrows) → describe_scene → "layout is cramped" -→ distribute_elements → get_canvas_screenshot → "arrow misaligned" -→ update_element → get_canvas_screenshot → "looks good" -→ export_scene --filePath architecture.excalidraw +batch_create_elements → get_canvas_screenshot → "text truncated on 2 shapes" +→ update_element (increase widths) → get_canvas_screenshot → "overlap between X and Y" +→ update_element (reposition) → get_canvas_screenshot → "all checks pass" +→ proceed to next iteration ``` ## Workflow: Refine An Existing Diagram diff --git a/skills/excalidraw-skill/references/cheatsheet.md b/skills/excalidraw-skill/references/cheatsheet.md index c3f1c17..fffa913 100644 --- a/skills/excalidraw-skill/references/cheatsheet.md +++ b/skills/excalidraw-skill/references/cheatsheet.md @@ -74,8 +74,11 @@ | `create_from_mermaid` | Mermaid diagram to Excalidraw | `mermaidDiagram` | Notes: -- For shapes, set `text` field to place text inside (backend converts to `label.text`). +- **MCP tools**: Set `text` field on shapes to label them (auto-converts to `label.text`). Use `startElementId`/`endElementId` on arrows. +- **REST API**: Use `"label": {"text": "..."}` for shape labels. Use `"start": {"id": "..."}` / `"end": {"id": "..."}` for arrow binding. (Different format from MCP!) +- `fontFamily` must be a string (e.g. `"1"`) or omit it entirely — do NOT pass a number. - `points` accepts both `[[x,y]]` tuples and `[{x,y}]` objects. +- **Curved arrows**: Use `"roundness": {"type": 2}` with 3+ points for smooth curves. Use `"elbowed": true` for right-angle routing. - Prefer creating shapes first, then arrows, then alignment/grouping. ## Canvas REST API (HTTP) diff --git a/src/index.ts b/src/index.ts index 4ed861d..38569d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -229,6 +229,9 @@ const ElementSchema = z.object({ groupIds: z.array(z.string()).optional(), locked: z.boolean().optional(), strokeStyle: z.string().optional(), + roundness: z.object({ type: z.number(), value: z.number().optional() }).nullable().optional(), + fillStyle: z.string().optional(), + elbowed: z.boolean().optional(), startElementId: z.string().optional(), endElementId: z.string().optional(), endArrowhead: z.string().optional(), diff --git a/src/server.ts b/src/server.ts index 02beff1..4ae1a33 100644 --- a/src/server.ts +++ b/src/server.ts @@ -111,12 +111,15 @@ const CreateElementSchema = z.object({ fontFamily: z.string().optional(), groupIds: z.array(z.string()).optional(), locked: z.boolean().optional(), + roundness: z.object({ type: z.number(), value: z.number().optional() }).nullable().optional(), + fillStyle: z.string().optional(), // Arrow-specific properties points: z.any().optional(), start: z.object({ id: z.string() }).optional(), end: z.object({ id: z.string() }).optional(), startArrowhead: z.string().nullable().optional(), endArrowhead: z.string().nullable().optional(), + elbowed: z.boolean().optional(), }); const UpdateElementSchema = z.object({ @@ -140,6 +143,8 @@ const UpdateElementSchema = z.object({ fontFamily: z.string().optional(), groupIds: z.array(z.string()).optional(), locked: z.boolean().optional(), + roundness: z.object({ type: z.number(), value: z.number().optional() }).nullable().optional(), + fillStyle: z.string().optional(), points: z.array(z.union([ z.tuple([z.number(), z.number()]), z.object({ x: z.number(), y: z.number() }) @@ -148,6 +153,7 @@ const UpdateElementSchema = z.object({ end: z.object({ id: z.string() }).optional(), startArrowhead: z.string().nullable().optional(), endArrowhead: z.string().nullable().optional(), + elbowed: z.boolean().optional(), }); // API Routes