
Excalidraw Skill
- 1.2k installs
- 255 repo stars
- Updated February 27, 2026
- lingzhi227/agent-research-skills
excalidraw-skill is an agent skill that programmatically creates, edits, layouts, and queries Excalidraw diagrams and UI mockups for developers who need architecture or wireframe visuals generated from code sessions.
About
excalidraw-skill is an agent skill that connects coding agents to an Excalidraw canvas through 26 MCP tools for element CRUD, batch creation, duplication, layout, and canvas queries. A local Express server (default http://localhost:3000) exposes endpoints like GET /health and tools such as create_element, update_element, query_elements, and batch_create_elements. Developers reach for excalidraw-skill when they want agents to produce architecture diagrams, flowcharts, or UI mockups without manual drag-and-drop. The skill targets programmatic diagram workflows where shapes, text, arrows, and lines are created and arranged via agent tool calls rather than hand editing.
- 26 MCP tools covering full element CRUD, layout commands, and scene awareness
- Supports create_element, batch_create_elements, update_element, delete_element and query_elements
- Layout tools include align_elements, distribute_elements, group_elements, lock_elements
- Scene awareness via describe_scene that returns AI-readable descriptions of types, positions, labels, connections and bo
- Works with any Excalidraw-compatible EXPRESS_SERVER_URL endpoint
Excalidraw Skill by the numbers
- 1,172 all-time installs (skills.sh)
- +35 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #350 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lingzhi227/agent-research-skills --skill excalidraw-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 255 |
| Security audit | 2 / 3 scanners passed |
| Last updated | February 27, 2026 |
| Repository | lingzhi227/agent-research-skills ↗ |
How do agents programmatically create Excalidraw diagrams?
Let their coding agent programmatically create, edit, layout, and query diagrams and UI mockups on an Excalidraw canvas.
Who is it for?
Developers who want coding agents to generate architecture diagrams, flowcharts, or wireframes on an Excalidraw canvas without manual drawing.
Skip if: Developers who only need static PNG exports or prefer Mermaid, PlantUML, or hand-drawn design tools without a programmatic canvas API.
When should I use this skill?
A user asks the agent to create, edit, layout, or query diagrams or UI mockups on an Excalidraw canvas programmatically.
What you get
Excalidraw canvas elements, layouted diagrams, and queryable UI mockup artifacts on a running canvas server.
- Excalidraw diagram elements
- Layouted canvas mockups
By the numbers
- Bundles 26 MCP tools for Excalidraw canvas operations
- Defaults canvas server to http://localhost:3000 via EXPRESS_SERVER_URL
Files
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)
mcp-cli tools | grep excalidrawIf you see tools like excalidraw/batch_create_elements → use MCP mode. Call MCP tools directly.
Check 2: REST API (Fallback — works without MCP server)
curl -s http://localhost:3000/healthIf 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_excalidraw2. Build: npm ci && npm run build3. Start canvas: HOST=0.0.0.0 PORT=3000 npm run canvas4. Open http://localhost:3000 in a browser5. (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": "<base64>"}. 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. Run Step 0 above to detect your connection mode. 2. Open the canvas URL in a browser (required for image export/screenshot). 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
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 shapes AND arrows in one call. 5. Assign custom `id` to shapes (e.g. "id": "auth-svc"). Set text field to label shapes. 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):
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)
Bind arrows to shapes for auto-routed edges. The format differs between MCP and REST API:
MCP Mode — use startElementId / endElementId:
{"elements": [
{"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "text": "Service A"},
{"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 120, "height": 60, "text": "Service B"},
{"type": "arrow", "x": 0, "y": 0, "startElementId": "svc-a", "endElementId": "svc-b", "text": "calls"}
]}REST API Mode — use start: {id} / end: {id} and label: {text}:
{"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"}, "label": {"text": "calls"}}
]}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:
{
"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):
{
"type": "arrow", "x": 100, "y": 100,
"points": [[0, 0], [0, -50], [200, -50], [200, 0]],
"elbowed": true,
"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. Each iteration MUST include a quality check.
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.
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):
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 iterationWorkflow: Refine An Existing Diagram
1. describe_scene to understand current state. 2. Identify targets by id, type, or label text (not x/y coordinates). 3. update_element to move/resize/recolor, delete_element to remove. 4. get_canvas_screenshot to verify changes visually. 5. If updates fail: check element id exists (get_element), element isn't locked (unlock_elements).
Workflow: File I/O (Diagrams-as-Code)
- Export to .excalidraw format:
export_scenewith optionalfilePath. - Import from .excalidraw:
import_scenewithmode: "replace"or"merge". - Export to image:
export_to_imagewithformat: "png"or"svg"(requires browser open). - CLI export:
node scripts/export-elements.cjs --out diagram.elements.json - CLI import:
node scripts/import-elements.cjs --in diagram.elements.json --mode batch|sync
Workflow: Snapshots (Save/Restore Canvas State)
1. snapshot_scene with a name before risky changes. 2. Make changes, describe_scene / get_canvas_screenshot to evaluate. 3. restore_snapshot to rollback if needed.
Workflow: Duplication
duplicate_elementswithelementIdsand optionaloffsetX/offsetY(default 20,20).- Useful for creating repeated patterns or copying existing layouts.
Points Format for Arrows/Lines
The points field accepts both formats:
- Tuple:
[[0, 0], [100, 50]] - Object:
[{"x": 0, "y": 0}, {"x": 100, "y": 50}]
Both are normalized to tuples automatically.
Workflow: Share Diagram (excalidraw.com URL)
1. Create your diagram using any of the above workflows. 2. export_to_excalidraw_url — uploads encrypted scene, returns a shareable URL. 3. Share the URL — anyone can open it in excalidraw.com to view and edit.
Workflow: Viewport Control
set_viewportwithscrollToContent: true— auto-fit all elements (zoom-to-fit).set_viewportwithscrollToElementId: "my-element"— center view on a specific element.set_viewportwithzoom: 1.5, offsetX: 100, offsetY: 200— manual camera control.
References
references/cheatsheet.md: Complete MCP tool list (26 tools) + REST API endpoints + payload shapes.
Related Skills
- See also: figure-generation, algorithm-design, slide-generation
Excalidraw Skill Cheatsheet
Defaults
- Canvas base URL:
EXPRESS_SERVER_URL(defaulthttp://localhost:3000) - Canvas health:
GET /health
MCP Tools (26 total)
Element CRUD
| Tool | Description | Required params |
|---|---|---|
create_element | Create shape/text/arrow/line | type, x, y |
get_element | Get single element by ID | id |
update_element | Update element properties | id |
delete_element | Delete element | id |
query_elements | Query by type/filters | (optional) type, filter |
batch_create_elements | Create many at once | elements[] |
duplicate_elements | Clone with offset | elementIds[], (optional) offsetX, offsetY |
Layout & Organization
| Tool | Description | Required params |
|---|---|---|
align_elements | Align to left/center/right/top/middle/bottom | elementIds[], alignment |
distribute_elements | Even spacing horizontal/vertical | elementIds[], direction |
group_elements | Group elements | elementIds[] |
ungroup_elements | Ungroup | groupId |
lock_elements | Lock elements | elementIds[] |
unlock_elements | Unlock elements | elementIds[] |
Scene Awareness (Iterative Refinement)
| Tool | Description | Required params |
|---|---|---|
describe_scene | AI-readable scene description (types, positions, labels, connections, bounding box) | (none) |
get_canvas_screenshot | Returns PNG image of canvas for visual verification | (optional) background |
get_resource | Get scene/library/theme/elements | resource |
File I/O & Export
| Tool | Description | Required params |
|---|---|---|
export_scene | Export to .excalidraw JSON | (optional) filePath |
import_scene | Import from .excalidraw JSON | mode ("replace"\ |
export_to_image | Export to PNG/SVG (needs browser) | format ("png"\ |
export_to_excalidraw_url | Upload & get shareable excalidraw.com URL | (none) |
State Management
| Tool | Description | Required params |
|---|---|---|
clear_canvas | Remove all elements | (none) |
snapshot_scene | Save named snapshot | name |
restore_snapshot | Restore from snapshot | name |
Viewport & Camera
| Tool | Description | Required params |
|---|---|---|
set_viewport | Control camera: zoom-to-fit, center on element, manual zoom/scroll (needs browser) | (optional) scrollToContent, scrollToElementId, zoom, offsetX, offsetY |
Design Guide
| Tool | Description | Required params |
|---|---|---|
read_diagram_guide | Get design best practices (colors, sizing, layout, anti-patterns) | (none) |
Conversion
| Tool | Description | Required params |
|---|---|---|
create_from_mermaid | Mermaid diagram to Excalidraw | mermaidDiagram |
Notes:
- MCP tools: Set
textfield on shapes to label them (auto-converts tolabel.text). UsestartElementId/endElementIdon arrows. - REST API: Use
"label": {"text": "..."}for shape labels. Use"start": {"id": "..."}/"end": {"id": "..."}for arrow binding. (Different format from MCP!) fontFamilymust be a string (e.g."1") or omit it entirely — do NOT pass a number.pointsaccepts both[[x,y]]tuples and[{x,y}]objects.- Curved arrows: Use
"roundness": {"type": 2}with 3+ points for smooth curves. Use"elbowed": truefor right-angle routing. - Prefer creating shapes first, then arrows, then alignment/grouping.
Canvas REST API (HTTP)
Elements
| Method | Endpoint | Description |
|---|---|---|
GET | /api/elements | List all elements |
GET | /api/elements/:id | Get element by ID |
POST | /api/elements | Create element |
PUT | /api/elements/:id | Update element |
DELETE | /api/elements/:id | Delete element |
DELETE | /api/elements/clear | Clear all elements |
GET | /api/elements/search?type=... | Search with filters |
POST | /api/elements/batch | Batch create |
POST | /api/elements/sync | Overwrite import (clear + write) |
POST | /api/elements/from-mermaid | Mermaid conversion via frontend |
Export
| Method | Endpoint | Description |
|---|---|---|
POST | /api/export/image | Request image export (needs frontend) |
POST | /api/export/image/result | Frontend posts export result back |
Viewport
| Method | Endpoint | Description |
|---|---|---|
POST | /api/viewport | Set viewport/camera (needs frontend) |
POST | /api/viewport/result | Frontend posts viewport result back |
Snapshots
| Method | Endpoint | Description |
|---|---|---|
POST | /api/snapshots | Save snapshot {name} |
GET | /api/snapshots | List snapshots |
GET | /api/snapshots/:name | Get snapshot by name |
System
| Method | Endpoint | Description |
|---|---|---|
GET | /health | Health check |
GET | /api/sync/status | Memory/WebSocket stats |
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>#!/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(/\/$/, "");
const res = await fetch(`${baseUrl}/api/elements/clear`, {
method: "DELETE",
});
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 (${json.count} elements removed)`);
}
main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(1);
});
#!/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);
});
#!/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);
});
#!/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);
});
#!/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);
});
#!/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);
});
#!/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);
});
Related skills
How it compares
Choose excalidraw-skill when you need live, editable Excalidraw canvases manipulated by agents rather than static diagram code in Mermaid or PlantUML.
FAQ
How many MCP tools does excalidraw-skill provide?
excalidraw-skill bundles 26 MCP tools covering element CRUD, batch creation, duplication, layout, and canvas queries. Tools include create_element, get_element, update_element, delete_element, query_elements, batch_create_elements, and duplicate_elements.
What server does excalidraw-skill connect to?
excalidraw-skill targets a local Express canvas server configured via EXPRESS_SERVER_URL, defaulting to http://localhost:3000. Agents verify readiness with GET /health before creating or editing canvas elements.
Is Excalidraw Skill safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.