@sanjibdevnath/mcp-excalidraw-local 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/dist/db.d.ts +58 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +379 -0
- package/dist/db.js.map +1 -0
- package/dist/frontend/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/frontend/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/frontend/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/frontend/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/frontend/assets/Tableau10-B-NsZVaP.js +1 -0
- package/dist/frontend/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/dist/frontend/assets/advancedFormat-BvOvfnfC.js +1 -0
- package/dist/frontend/assets/ar-SA-G6X2FPQ2-75HMOOy8.js +10 -0
- package/dist/frontend/assets/arc-D-322MQz.js +1 -0
- package/dist/frontend/assets/array-BKyUJesY.js +1 -0
- package/dist/frontend/assets/az-AZ-76LH7QW2-DPDwkDvh.js +1 -0
- package/dist/frontend/assets/band-dPffDWoQ.js +1 -0
- package/dist/frontend/assets/bg-BG-XCXSNQG7-DrFYc9eo.js +5 -0
- package/dist/frontend/assets/blockDiagram-38ab4fdb-Ch8bwO7g.js +118 -0
- package/dist/frontend/assets/blockDiagram-68f4deed-BVqzkDiu.js +118 -0
- package/dist/frontend/assets/bn-BD-2XOGV67Q-B1Y75Cvj.js +5 -0
- package/dist/frontend/assets/c4Diagram-15b5d702-D5U2mSdf.js +10 -0
- package/dist/frontend/assets/c4Diagram-3d4e48cf-eT2EEN_c.js +10 -0
- package/dist/frontend/assets/ca-ES-6MX7JW3Y-00BTiK3Z.js +8 -0
- package/dist/frontend/assets/channel-CudwHHli.js +1 -0
- package/dist/frontend/assets/classDiagram-70f12bd4-CcNOdQHv.js +2 -0
- package/dist/frontend/assets/classDiagram-d40c83e7-nRIgRTMT.js +2 -0
- package/dist/frontend/assets/classDiagram-v2-d5a6b087-Cfbvao44.js +2 -0
- package/dist/frontend/assets/classDiagram-v2-f2320105-1Sjp5Uqh.js +2 -0
- package/dist/frontend/assets/clone-D_tGm99B.js +1 -0
- package/dist/frontend/assets/createText-2e5e7dd3-Bpmkp1eZ.js +5 -0
- package/dist/frontend/assets/createText-d213de94-3MLB4fd8.js +5 -0
- package/dist/frontend/assets/cs-CZ-2BRQDIVT-R7SCWLLF.js +11 -0
- package/dist/frontend/assets/cytoscape-cose-bilkent-CoIxD6ON.js +331 -0
- package/dist/frontend/assets/da-DK-5WZEPLOC-Db1yebad.js +5 -0
- package/dist/frontend/assets/de-DE-XR44H4JA-HRE-6fuh.js +8 -0
- package/dist/frontend/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
- package/dist/frontend/assets/directory-open-4ed118d0-BzWybGaI.js +1 -0
- package/dist/frontend/assets/edges-332bd1c7-DZAOA9uP.js +4 -0
- package/dist/frontend/assets/edges-e0da2a9e-CP-XTLb4.js +4 -0
- package/dist/frontend/assets/el-GR-BZB4AONW-CfNczSdx.js +10 -0
- package/dist/frontend/assets/elk.bundled-BZDcWavb.js +26 -0
- package/dist/frontend/assets/erDiagram-880f2ed8-Bk96tDga.js +51 -0
- package/dist/frontend/assets/erDiagram-9861fffd-BvkEkcRK.js +51 -0
- package/dist/frontend/assets/es-ES-U4NZUMDT-BBJZ1_wD.js +9 -0
- package/dist/frontend/assets/eu-ES-A7QVB2H4-CCLNmdnk.js +11 -0
- package/dist/frontend/assets/fa-IR-HGAKTJCU-BtKS5FOW.js +8 -0
- package/dist/frontend/assets/fi-FI-Z5N7JZ37-DEQi6vbL.js +6 -0
- package/dist/frontend/assets/file-open-002ab408-DIuFHtCF.js +1 -0
- package/dist/frontend/assets/file-open-7c801643-684qeFg4.js +1 -0
- package/dist/frontend/assets/file-save-3189631c-x92wctJd.js +1 -0
- package/dist/frontend/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
- package/dist/frontend/assets/flowDb-7c981674-JJMg1ttK.js +10 -0
- package/dist/frontend/assets/flowDb-956e92f1-CVVUllPW.js +10 -0
- package/dist/frontend/assets/flowDiagram-66a62f08-wGFuUp6y.js +4 -0
- package/dist/frontend/assets/flowDiagram-cbd28bf7-CXKT_tHC.js +4 -0
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CN4ht1EM.js +1 -0
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CFiBItzu.js +1 -0
- package/dist/frontend/assets/flowchart-elk-definition-36e2d292-Cam5JBwn.js +114 -0
- package/dist/frontend/assets/flowchart-elk-definition-4a651766-BoyD4myW.js +114 -0
- package/dist/frontend/assets/fr-FR-RHASNOE6-_AQjPuKS.js +9 -0
- package/dist/frontend/assets/ganttDiagram-04f9e578-DrDI9_oS.js +257 -0
- package/dist/frontend/assets/ganttDiagram-c361ad54-CKSyNc2k.js +257 -0
- package/dist/frontend/assets/gitGraphDiagram-21fc4d3e-BHBdnwSb.js +70 -0
- package/dist/frontend/assets/gitGraphDiagram-72cf32ee-BHN9qiXg.js +70 -0
- package/dist/frontend/assets/gl-ES-HMX3MZ6V-Bp2h6sBC.js +10 -0
- package/dist/frontend/assets/graph-CRb9j7zI.js +1 -0
- package/dist/frontend/assets/graph-EK5j_nPe.js +1 -0
- package/dist/frontend/assets/he-IL-6SHJWFNN-hsaAKZ5K.js +10 -0
- package/dist/frontend/assets/hi-IN-IWLTKZ5I-sgYSNzoz.js +4 -0
- package/dist/frontend/assets/hu-HU-A5ZG7DT2-DxYZr0yq.js +7 -0
- package/dist/frontend/assets/id-ID-SAP4L64H-z0RzSKPQ.js +10 -0
- package/dist/frontend/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
- package/dist/frontend/assets/index-3862675e-CQPsxwvk.js +1 -0
- package/dist/frontend/assets/index-6079d271-pTR-OMc-.js +1 -0
- package/dist/frontend/assets/index-B9Rh8YyQ.css +1 -0
- package/dist/frontend/assets/index-BcHA28Dx.js +87 -0
- package/dist/frontend/assets/index-DGmpr33w.js +3 -0
- package/dist/frontend/assets/index-DPgZw9ew.js +349 -0
- package/dist/frontend/assets/infoDiagram-4a4f5b27-OIxyK2_N.js +7 -0
- package/dist/frontend/assets/infoDiagram-f8f76790-BTkoanKB.js +7 -0
- package/dist/frontend/assets/init-Gi6I4Gst.js +1 -0
- package/dist/frontend/assets/it-IT-JPQ66NNP-Cu6RM7DP.js +11 -0
- package/dist/frontend/assets/ja-JP-DBVTYXUO-lD7U4Zkf.js +8 -0
- package/dist/frontend/assets/journeyDiagram-29694f62-BS4Xl0A-.js +139 -0
- package/dist/frontend/assets/journeyDiagram-49397b02-BbBAwEfu.js +139 -0
- package/dist/frontend/assets/kaa-6HZHGXH3-DM9LwXUP.js +1 -0
- package/dist/frontend/assets/kab-KAB-ZGHBKWFO-BAojmp2_.js +8 -0
- package/dist/frontend/assets/katex-ChWnQ-fc.js +261 -0
- package/dist/frontend/assets/kk-KZ-P5N5QNE5-Dp0K1W81.js +1 -0
- package/dist/frontend/assets/km-KH-HSX4SM5Z-BzYGKbAg.js +11 -0
- package/dist/frontend/assets/ko-KR-MTYHY66A-DOvEMk4H.js +9 -0
- package/dist/frontend/assets/ku-TR-6OUDTVRD-B6l-ghqp.js +9 -0
- package/dist/frontend/assets/layout-CGydnLJa.js +1 -0
- package/dist/frontend/assets/layout-DbdMIGYe.js +1 -0
- package/dist/frontend/assets/line-CbImtxDK.js +1 -0
- package/dist/frontend/assets/linear-DvIsU3aM.js +1 -0
- package/dist/frontend/assets/lt-LT-XHIRWOB4-BYcRk8Uj.js +3 -0
- package/dist/frontend/assets/lv-LV-5QDEKY6T-DS3krNIe.js +7 -0
- package/dist/frontend/assets/mindmap-definition-ac74a2e8-C0Sp7ICZ.js +95 -0
- package/dist/frontend/assets/mindmap-definition-fc14e90a-BZrjRbkr.js +95 -0
- package/dist/frontend/assets/mr-IN-CRQNXWMA-BfxQL7Vh.js +13 -0
- package/dist/frontend/assets/my-MM-5M5IBNSE-C3EfnOvD.js +1 -0
- package/dist/frontend/assets/nb-NO-T6EIAALU-BIbPZokm.js +10 -0
- package/dist/frontend/assets/nl-NL-IS3SIHDZ-BqQloGBT.js +8 -0
- package/dist/frontend/assets/nn-NO-6E72VCQL-zGR8NYQf.js +8 -0
- package/dist/frontend/assets/oc-FR-POXYY2M6-B8-HsJFE.js +8 -0
- package/dist/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/frontend/assets/pa-IN-N4M65BXN-B2Ta58Tu.js +4 -0
- package/dist/frontend/assets/path-CbwjOpE9.js +1 -0
- package/dist/frontend/assets/pica-DSD-O3at.js +7 -0
- package/dist/frontend/assets/pie-Dk_pQnuO.js +1 -0
- package/dist/frontend/assets/pieDiagram-421022e6-9oAq5fk_.js +35 -0
- package/dist/frontend/assets/pieDiagram-8a3498a8-B5SMrdDh.js +35 -0
- package/dist/frontend/assets/pl-PL-T2D74RX3-rZKvQ0zQ.js +9 -0
- package/dist/frontend/assets/pt-BR-5N22H2LF-ij6wtU6I.js +9 -0
- package/dist/frontend/assets/pt-PT-UZXXM6DQ-BIgtUnbW.js +9 -0
- package/dist/frontend/assets/quadrantDiagram-0957ecba-Cr3mj6c1.js +7 -0
- package/dist/frontend/assets/quadrantDiagram-120e2f19-CQnc4s0f.js +7 -0
- package/dist/frontend/assets/requirementDiagram-23d650b8-Bs7pP1vJ.js +52 -0
- package/dist/frontend/assets/requirementDiagram-deff3bca-G5e-Qxao.js +52 -0
- package/dist/frontend/assets/ro-RO-JPDTUUEW-DPj_79nt.js +11 -0
- package/dist/frontend/assets/roundRect-0PYZxl1G.js +1 -0
- package/dist/frontend/assets/ru-RU-B4JR7IUQ-fdYiaqbX.js +9 -0
- package/dist/frontend/assets/sankeyDiagram-04a897e0-CJogadkF.js +8 -0
- package/dist/frontend/assets/sankeyDiagram-23345273-DKUWMCrX.js +8 -0
- package/dist/frontend/assets/sankeyLinkHorizontal-DgqkLiUE.js +1 -0
- package/dist/frontend/assets/selectAll-tNeSnQY6.js +1 -0
- package/dist/frontend/assets/sequenceDiagram-17ac3bff-DCw9xUbw.js +122 -0
- package/dist/frontend/assets/sequenceDiagram-704730f1-BgClSrOI.js +122 -0
- package/dist/frontend/assets/si-LK-N5RQ5JYF-DfPBk-rU.js +1 -0
- package/dist/frontend/assets/sk-SK-C5VTKIMK-Cbj4yoD_.js +6 -0
- package/dist/frontend/assets/sl-SI-NN7IZMDC-C_rL7eDE.js +6 -0
- package/dist/frontend/assets/stateDiagram-587899a1-DuFGG-SI.js +1 -0
- package/dist/frontend/assets/stateDiagram-9c5f0230-Bwj38hfH.js +1 -0
- package/dist/frontend/assets/stateDiagram-v2-51a3dcff-3c0yKNdL.js +1 -0
- package/dist/frontend/assets/stateDiagram-v2-d93cdb3a-CAaqB4wm.js +1 -0
- package/dist/frontend/assets/styles-2ab5d517-Dxg7wKah.js +116 -0
- package/dist/frontend/assets/styles-5f03d8d2-DD32XMGL.js +160 -0
- package/dist/frontend/assets/styles-6aaf32cf-B5DxK_RW.js +207 -0
- package/dist/frontend/assets/styles-9a916d00-C6L6Mj2P.js +160 -0
- package/dist/frontend/assets/styles-c10674c1-BPM_bB3H.js +116 -0
- package/dist/frontend/assets/styles-edf9a4b0-CbQDxrwP.js +207 -0
- package/dist/frontend/assets/subset-shared.chunk-B_DQsaBC.js +84 -0
- package/dist/frontend/assets/subset-worker.chunk-DL6tLP7M.js +1 -0
- package/dist/frontend/assets/sv-SE-XGPEYMSR-BmmcOaVK.js +10 -0
- package/dist/frontend/assets/svgDrawCommon-08f97a94-aUx8qfJx.js +1 -0
- package/dist/frontend/assets/svgDrawCommon-3ba9043b-1JM8RiLc.js +1 -0
- package/dist/frontend/assets/ta-IN-2NMHFXQM-Kxnb_Mwk.js +9 -0
- package/dist/frontend/assets/th-TH-HPSO5L25-BqTLgxJz.js +2 -0
- package/dist/frontend/assets/timeline-definition-7e6b55e7-BbFhIPTl.js +61 -0
- package/dist/frontend/assets/timeline-definition-85554ec2-C1G9H6m5.js +61 -0
- package/dist/frontend/assets/tr-TR-DEFEU3FU-DhlYP6tL.js +7 -0
- package/dist/frontend/assets/uk-UA-QMV73CPH-pMrN1qBS.js +6 -0
- package/dist/frontend/assets/union-Cu1rbD_D.js +1 -0
- package/dist/frontend/assets/vi-VN-M7AON7JQ-BPMcH84R.js +5 -0
- package/dist/frontend/assets/xml-BOsq7VnW.js +1 -0
- package/dist/frontend/assets/xychartDiagram-b6496bcd-BDm9pYtk.js +7 -0
- package/dist/frontend/assets/xychartDiagram-e933f94c-BlrTBDHC.js +7 -0
- package/dist/frontend/assets/zh-CN-LNUGB5OW-B8kYYibM.js +10 -0
- package/dist/frontend/assets/zh-HK-E62DVLB3-CaI0gehP.js +1 -0
- package/dist/frontend/assets/zh-TW-RAJ6MFWO-DKCVg17j.js +9 -0
- package/dist/frontend/assets/zipObject-iRVIFf6r.js +1 -0
- package/dist/frontend/index.html +420 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2241 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +980 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +108 -0
- package/skills/excalidraw-skill/SKILL.md +370 -0
- package/skills/excalidraw-skill/references/cheatsheet.md +195 -0
- package/skills/excalidraw-skill/scripts/clear-canvas.cjs +38 -0
- package/skills/excalidraw-skill/scripts/create-element.cjs +68 -0
- package/skills/excalidraw-skill/scripts/delete-element.cjs +48 -0
- package/skills/excalidraw-skill/scripts/export-elements.cjs +53 -0
- package/skills/excalidraw-skill/scripts/healthcheck.cjs +35 -0
- package/skills/excalidraw-skill/scripts/import-elements.cjs +81 -0
- package/skills/excalidraw-skill/scripts/update-element.cjs +70 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: excalidraw-skill
|
|
3
|
+
description: Programmatic canvas toolkit for creating, editing, and refining Excalidraw diagrams via MCP tools (32 tools) or REST API with real-time canvas sync, multi-tenant workspace isolation, SQLite persistence, project management, full-text search, and element version history. Use when an agent needs to draw or lay out diagrams on a live canvas, iteratively refine diagrams using screenshots, manage workspaces/tenants and projects, export/import .excalidraw files or PNG/SVG images, search elements, view change history, save/restore canvas snapshots, or perform element-level CRUD. Canvas server port is configurable via CANVAS_PORT env var (default 3000).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Excalidraw Skill
|
|
7
|
+
|
|
8
|
+
## Step 0: Detect Connection Mode
|
|
9
|
+
|
|
10
|
+
Run these checks **in order**:
|
|
11
|
+
|
|
12
|
+
1. **MCP Server** (best): If tools like `batch_create_elements` are available → use MCP mode.
|
|
13
|
+
2. **REST API** (fallback): `curl -s http://localhost:3000/health` returns `{"status":"ok"}` → use REST API mode.
|
|
14
|
+
3. **Nothing works**: Guide user to install (clone `sanjibdevnathlabs/mcp-excalidraw-local`, build, configure MCP).
|
|
15
|
+
|
|
16
|
+
See `references/cheatsheet.md` for the full MCP-vs-REST mapping and REST API gotchas.
|
|
17
|
+
|
|
18
|
+
## Core Principles (Read Before Any Diagram)
|
|
19
|
+
|
|
20
|
+
These principles were learned through extensive iterative use. Violating them produces bad diagrams.
|
|
21
|
+
|
|
22
|
+
### 1. Never Trust Blind Output — Use the Write-Check-Review Cycle
|
|
23
|
+
|
|
24
|
+
Every diagram iteration follows this mandatory loop:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
WRITE (create/update elements)
|
|
28
|
+
→ CHECK (screenshot to see actual rendering)
|
|
29
|
+
→ REVIEW (critically evaluate against Quality Checklist)
|
|
30
|
+
→ FIX (if issues found, fix and re-screenshot)
|
|
31
|
+
→ only proceed when ALL checks pass
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Screenshot strategy**: `get_canvas_screenshot` may return empty images. When it fails, use Chrome DevTools MCP (`take_screenshot` after `navigate_page` to canvas URL) as a reliable fallback.
|
|
35
|
+
|
|
36
|
+
### 2. Use batch_create_elements, Not Mermaid
|
|
37
|
+
|
|
38
|
+
The `create_from_mermaid` tool produces **low-quality output**: overlapping text, poor spacing, unreadable labels. It is a quick preview tool, not a production tool.
|
|
39
|
+
|
|
40
|
+
For quality diagrams, **always use `batch_create_elements`** with precise coordinates, explicit sizing, and color coding. The extra planning time pays for itself in fewer fix iterations.
|
|
41
|
+
|
|
42
|
+
### 3. Shapes First, Arrows Second — Two Separate Batches
|
|
43
|
+
|
|
44
|
+
Create shapes in one batch, then arrows in a separate batch. Arrow binding (`startElementId`/`endElementId`) requires shapes to already exist in the scene. Mixing both in one call can work but often produces binding errors.
|
|
45
|
+
|
|
46
|
+
### 4. Multiple Diagrams on One Canvas
|
|
47
|
+
|
|
48
|
+
**Never clear the canvas** between diagrams. Place them side-by-side or in a grid:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Diagram 1: x=0 to ~1100
|
|
52
|
+
Diagram 2: x=1400 onward (300px gap)
|
|
53
|
+
— or —
|
|
54
|
+
Row 1: y=0 to ~800
|
|
55
|
+
Row 2: y=1100 onward (300px gap)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use a title text element above each diagram to label it.
|
|
59
|
+
|
|
60
|
+
### 5. Set roughness: 0 for Clean Diagrams
|
|
61
|
+
|
|
62
|
+
Excalidraw defaults to hand-drawn style (roughness > 0). For professional, readable diagrams, always set `"roughness": 0` on every element. Also use `"strokeWidth": 2` for arrows to ensure visibility.
|
|
63
|
+
|
|
64
|
+
## Sizing Rules (Critical — Prevents Truncation)
|
|
65
|
+
|
|
66
|
+
Excalidraw's Virgil font is ~30% wider than standard fonts. These rules account for that.
|
|
67
|
+
|
|
68
|
+
### Rectangles
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
width: max(200, characterCount * 11)
|
|
72
|
+
height: 70 (1 line), 80 (2 lines), 100 (3 lines)
|
|
73
|
+
fontSize: 16-20
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Diamonds (Decision Nodes)
|
|
77
|
+
|
|
78
|
+
Diamond usable text area is ~50% of the bounding box. **Double your width estimate.**
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
width: max(400, longestLineChars * 18)
|
|
82
|
+
height: max(160, lineCount * 50)
|
|
83
|
+
fontSize: 16
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A diamond with text "Behavioral guideline\nor project standard?" (20 chars) needs at least 400x160.
|
|
87
|
+
|
|
88
|
+
### Ellipses
|
|
89
|
+
|
|
90
|
+
Ellipse text area is ~60% of bounding box. Size generously.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
width: max(280, characterCount * 14)
|
|
94
|
+
height: max(65, lineCount * 35)
|
|
95
|
+
fontSize: 16-18
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Text Elements (Standalone Titles)
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
fontSize: 24-28 for diagram titles
|
|
102
|
+
fontSize: 16-20 for annotations
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Arrow Visibility Rules (Critical — Prevents Invisible Arrows)
|
|
106
|
+
|
|
107
|
+
When arrows are bound to shapes via `startElementId`/`endElementId`, the actual rendered arrow length equals the **gap between shape edges minus binding padding (8px each side)**. If shapes are too close, arrows shrink to 0px and become invisible.
|
|
108
|
+
|
|
109
|
+
### Minimum Gap Between Connected Shapes
|
|
110
|
+
|
|
111
|
+
| Connection Direction | Minimum Gap | Recommended Gap |
|
|
112
|
+
|---------------------|-------------|-----------------|
|
|
113
|
+
| Vertical (top-down flow) | 80px | 120px |
|
|
114
|
+
| Horizontal (left-right) | 100px | 140px |
|
|
115
|
+
|
|
116
|
+
### Calculating Vertical Gap for Flowcharts
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
gap = nextShapeY - (currentShapeY + currentShapeHeight)
|
|
120
|
+
|
|
121
|
+
Example (diamonds h=160, gap needed ≥ 120):
|
|
122
|
+
Q1: y=260, h=160 → bottom edge = 420
|
|
123
|
+
Q2: y=540 → gap = 540 - 420 = 120px ✓
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If the gap is < 80px, arrows will be too short to see — especially with labels like "YES"/"NO".
|
|
127
|
+
|
|
128
|
+
## Workflow: Draw A Diagram
|
|
129
|
+
|
|
130
|
+
### Phase 1: Plan
|
|
131
|
+
|
|
132
|
+
Before writing any JSON, plan on paper:
|
|
133
|
+
|
|
134
|
+
1. **List all elements**: shapes, labels, connections
|
|
135
|
+
2. **Choose layout direction**: top-down (flowcharts), left-right (timelines), grid (architecture)
|
|
136
|
+
3. **Assign coordinates**: use the sizing rules above to compute widths/heights, then lay out with proper gaps
|
|
137
|
+
4. **Assign IDs**: every shape needs a custom `id` so arrows can reference it
|
|
138
|
+
|
|
139
|
+
### Phase 2: Create Shapes (Batch 1)
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{"elements": [
|
|
143
|
+
{"id": "title", "type": "text", "x": 100, "y": 0,
|
|
144
|
+
"text": "MY DIAGRAM", "fontSize": 28, "strokeColor": "#1e1e1e"},
|
|
145
|
+
{"id": "box-a", "type": "rectangle", "x": 0, "y": 80,
|
|
146
|
+
"width": 200, "height": 70, "text": "Service A",
|
|
147
|
+
"backgroundColor": "#a5d8ff", "strokeColor": "#1971c2",
|
|
148
|
+
"roughness": 0, "fontSize": 18},
|
|
149
|
+
{"id": "box-b", "type": "rectangle", "x": 0, "y": 280,
|
|
150
|
+
"width": 200, "height": 70, "text": "Service B",
|
|
151
|
+
"backgroundColor": "#b2f2bb", "strokeColor": "#2f9e44",
|
|
152
|
+
"roughness": 0, "fontSize": 18}
|
|
153
|
+
]}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Phase 3: Create Arrows (Batch 2)
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{"elements": [
|
|
160
|
+
{"type": "arrow", "x": 100, "y": 150,
|
|
161
|
+
"startElementId": "box-a", "endElementId": "box-b",
|
|
162
|
+
"width": 0, "height": 130, "text": "calls",
|
|
163
|
+
"strokeColor": "#1e1e1e", "roughness": 0, "strokeWidth": 2,
|
|
164
|
+
"endArrowhead": "arrow"}
|
|
165
|
+
]}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Phase 4: Check (MANDATORY)
|
|
169
|
+
|
|
170
|
+
1. `set_viewport` with `scrollToContent: true`
|
|
171
|
+
2. Wait 1-2 seconds for render
|
|
172
|
+
3. Take screenshot (MCP `get_canvas_screenshot` or Chrome DevTools `take_screenshot`)
|
|
173
|
+
4. **Critically evaluate** against the Quality Checklist below
|
|
174
|
+
5. Fix any issues, re-screenshot, repeat until clean
|
|
175
|
+
|
|
176
|
+
## Quality Checklist
|
|
177
|
+
|
|
178
|
+
After EVERY batch of elements, verify ALL of these:
|
|
179
|
+
|
|
180
|
+
| Check | What to Look For | Fix |
|
|
181
|
+
|-------|-----------------|-----|
|
|
182
|
+
| **Text truncation** | Any label cut off or hidden? | Increase shape width/height |
|
|
183
|
+
| **Invisible arrows** | Can you see arrows between all connected shapes? | Increase gap between shapes to ≥ 120px |
|
|
184
|
+
| **Arrow labels** | Do YES/NO/labels overlap with shapes? | Shorten labels or increase gap |
|
|
185
|
+
| **Overlap** | Do any elements share space? | Reposition with more spacing |
|
|
186
|
+
| **Readability** | Can all text be read at 50-70% zoom? | Increase fontSize to ≥ 16 |
|
|
187
|
+
| **Spacing** | At least 40px gap between unconnected elements? | Spread elements apart |
|
|
188
|
+
|
|
189
|
+
### If ANY Check Fails
|
|
190
|
+
|
|
191
|
+
**STOP.** Do not add more elements. Fix the issue first:
|
|
192
|
+
|
|
193
|
+
1. Use `update_element` to resize/reposition
|
|
194
|
+
2. Or `delete_element` + recreate with better coordinates
|
|
195
|
+
3. Re-screenshot to verify the fix
|
|
196
|
+
4. Only proceed when ALL checks pass
|
|
197
|
+
|
|
198
|
+
### How to Honestly Evaluate a Screenshot
|
|
199
|
+
|
|
200
|
+
- Zoom into different regions — don't just glance at the overview
|
|
201
|
+
- Check every label individually for truncation
|
|
202
|
+
- Trace every arrow path for visibility
|
|
203
|
+
- **If you see ANY issue, say "I see [issue], fixing it"** — never say "looks great" unless it truly is
|
|
204
|
+
|
|
205
|
+
## Color Palette
|
|
206
|
+
|
|
207
|
+
Use consistent colors from this palette:
|
|
208
|
+
|
|
209
|
+
| Role | Fill | Stroke | Use For |
|
|
210
|
+
|------|------|--------|---------|
|
|
211
|
+
| Primary | #a5d8ff | #1971c2 | Main flow, services |
|
|
212
|
+
| Success | #b2f2bb | #2f9e44 | Approved, healthy, YES paths |
|
|
213
|
+
| Warning | #ffd8a8 | #e8590c | Attention, agents |
|
|
214
|
+
| Error | #ffc9c9 | #e03131 | Critical, NO paths, failures |
|
|
215
|
+
| Purple | #eebefa | #9c36b5 | Rules, governance |
|
|
216
|
+
| Cyan | #99e9f2 | #0c8599 | Data stores, MCP |
|
|
217
|
+
| Neutral | #e9ecef | #868e96 | Secondary, annotations |
|
|
218
|
+
| Default | #ffffff | #1e1e1e | Decisions, generic |
|
|
219
|
+
|
|
220
|
+
## Flowchart Template (Tested & Verified)
|
|
221
|
+
|
|
222
|
+
This template produces clean, readable decision flowcharts:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
Layout:
|
|
226
|
+
Diamonds: w=400, h=160, fontSize=16, gap=120px vertical
|
|
227
|
+
Answer boxes: w=300, h=80, fontSize=20, offset 130px right of diamonds
|
|
228
|
+
Start ellipse: w=340, h=70, fontSize=18
|
|
229
|
+
Title: fontSize=28
|
|
230
|
+
Arrows: strokeWidth=2, roughness=0
|
|
231
|
+
YES arrows: strokeColor=#2f9e44 (green), horizontal right
|
|
232
|
+
NO arrows: strokeColor=#e03131 (red), vertical down
|
|
233
|
+
All elements: roughness=0
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Architecture Diagram Template
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
Layout:
|
|
240
|
+
Zones: large rectangles, backgroundColor=#e9ecef, opacity=30
|
|
241
|
+
Services: w=200, h=70, fontSize=18, spaced 60px apart
|
|
242
|
+
Data stores: w=180, h=60, fontSize=16, strokeColor=#0c8599
|
|
243
|
+
Arrows: solid for sync, dashed (strokeStyle="dashed") for async
|
|
244
|
+
Title: fontSize=24 above each zone
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Workflow: Iterative Refinement
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
create shapes (batch 1)
|
|
251
|
+
→ create arrows (batch 2)
|
|
252
|
+
→ set_viewport(scrollToContent: true)
|
|
253
|
+
→ wait 1-2s
|
|
254
|
+
→ screenshot
|
|
255
|
+
→ evaluate quality checklist
|
|
256
|
+
→ issues? fix → re-screenshot → re-evaluate
|
|
257
|
+
→ clean? proceed to next diagram section
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
For multi-diagram canvases, offset each new diagram by 300px+ from the previous one's bounding box.
|
|
261
|
+
|
|
262
|
+
## Workflow: Multi-Tenancy (Workspaces)
|
|
263
|
+
|
|
264
|
+
The MCP is multi-tenant. Each Cursor workspace automatically gets its own tenant (identified by a SHA-256 hash of the workspace path). All elements, projects, and snapshots are scoped to the active tenant.
|
|
265
|
+
|
|
266
|
+
### Automatic Tenant Detection
|
|
267
|
+
|
|
268
|
+
On MCP startup, the server:
|
|
269
|
+
1. Creates a tenant from `process.cwd()` (initial guess)
|
|
270
|
+
2. After connecting, calls `server.listRoots()` to get the real workspace path from Cursor
|
|
271
|
+
3. If different, re-creates/switches to the correct tenant and notifies the canvas
|
|
272
|
+
|
|
273
|
+
This means globally-configured MCPs (`~/.cursor/mcp.json`) correctly detect the per-window workspace — no manual setup needed.
|
|
274
|
+
|
|
275
|
+
### Tenant Operations
|
|
276
|
+
|
|
277
|
+
| Task | Tool | Notes |
|
|
278
|
+
|------|------|-------|
|
|
279
|
+
| See all workspaces | `list_tenants` | Returns id, name, workspace_path, created_at |
|
|
280
|
+
| Switch workspace | `switch_tenant` with `tenantId` | Canvas reloads that tenant's elements via WebSocket |
|
|
281
|
+
| Check current tenant | (from describe_scene or frontend header) | Shows "Workspace: [name]" in canvas |
|
|
282
|
+
|
|
283
|
+
### Multiple Cursor Instances
|
|
284
|
+
|
|
285
|
+
Each instance sends its own `X-Tenant-Id` header on every HTTP/MCP request. SQLite uses `busy_timeout` for concurrent write safety. No state conflicts between windows.
|
|
286
|
+
|
|
287
|
+
## Workflow: Projects (Within a Tenant)
|
|
288
|
+
|
|
289
|
+
Projects group diagrams within a tenant. Each tenant has a "Default Project" created automatically. Use projects to organize different diagram sets (e.g., "Architecture", "User Flows", "Sprint Planning").
|
|
290
|
+
|
|
291
|
+
| Task | Tool | Notes |
|
|
292
|
+
|------|------|-------|
|
|
293
|
+
| List projects | `list_projects` | Shows all projects in active tenant |
|
|
294
|
+
| Switch project | `switch_project` with `projectId` | Elements change to that project's set |
|
|
295
|
+
| Create new project | `switch_project` with `createName` | Creates and switches in one call |
|
|
296
|
+
|
|
297
|
+
## Workflow: Search & History
|
|
298
|
+
|
|
299
|
+
### Full-Text Search
|
|
300
|
+
|
|
301
|
+
`search_elements` with `query` — searches across element labels and text content in the active project. Useful for finding specific elements in large diagrams.
|
|
302
|
+
|
|
303
|
+
### Element Version History
|
|
304
|
+
|
|
305
|
+
`element_history` — view create/update/delete operations for:
|
|
306
|
+
- A specific element: pass `elementId`
|
|
307
|
+
- Entire active project: omit `elementId`
|
|
308
|
+
- Control result count with `limit` (default 50)
|
|
309
|
+
|
|
310
|
+
Use history to debug unexpected changes or audit what was modified.
|
|
311
|
+
|
|
312
|
+
## Workflow: Refine An Existing Diagram
|
|
313
|
+
|
|
314
|
+
1. `describe_scene` to understand current state
|
|
315
|
+
2. Identify targets by `id` or label text (or use `search_elements` for text search)
|
|
316
|
+
3. `update_element` to move/resize/recolor
|
|
317
|
+
4. Screenshot to verify
|
|
318
|
+
5. If updates fail: check element id exists (`get_element`), element isn't locked
|
|
319
|
+
6. Use `element_history` to see what changed if something looks wrong
|
|
320
|
+
|
|
321
|
+
## Workflow: File I/O
|
|
322
|
+
|
|
323
|
+
- Export: `export_scene` (optional `filePath`)
|
|
324
|
+
- Import: `import_scene` with `mode: "replace"` or `"merge"`
|
|
325
|
+
- Image export: `export_to_image` with `format: "png"` or `"svg"` (requires browser)
|
|
326
|
+
|
|
327
|
+
## Workflow: Snapshots
|
|
328
|
+
|
|
329
|
+
1. `snapshot_scene` with a name before risky changes
|
|
330
|
+
2. Make changes, screenshot to evaluate
|
|
331
|
+
3. `restore_snapshot` to rollback if needed
|
|
332
|
+
|
|
333
|
+
**Note**: Snapshot restore may not always reload elements into the active view. If the canvas appears empty after restore, re-fetch elements or recreate.
|
|
334
|
+
|
|
335
|
+
## Workflow: Viewport Control
|
|
336
|
+
|
|
337
|
+
- `scrollToContent: true` — auto-fit all elements
|
|
338
|
+
- `scrollToElementId: "my-element"` — center on specific element
|
|
339
|
+
- `zoom: 0.7, offsetX: 100, offsetY: 50` — manual camera for close-up review
|
|
340
|
+
|
|
341
|
+
## Anti-Patterns (Common Mistakes)
|
|
342
|
+
|
|
343
|
+
| Mistake | Why It Fails | Do This Instead |
|
|
344
|
+
|---------|-------------|-----------------|
|
|
345
|
+
| Using `create_from_mermaid` for final diagrams | Overlapping text, poor layout | Use `batch_create_elements` with coordinates |
|
|
346
|
+
| Shapes too small for text | Truncation, especially in diamonds | Use sizing formulas above |
|
|
347
|
+
| No gap between connected shapes | Arrows become invisible (0px length) | Maintain 120px+ vertical gap |
|
|
348
|
+
| Clearing canvas between diagrams | Loses previous work | Place diagrams side-by-side |
|
|
349
|
+
| Skipping screenshot verification | Invisible defects compound | Screenshot after EVERY batch |
|
|
350
|
+
| Shapes + arrows in one batch | Binding errors | Shapes first, arrows second |
|
|
351
|
+
| Default roughness (hand-drawn look) | Unprofessional for technical diagrams | Set `roughness: 0` on all elements |
|
|
352
|
+
| Trusting MCP screenshot alone | May return empty image | Use Chrome DevTools as fallback |
|
|
353
|
+
|
|
354
|
+
## MCP Tool Quick Reference (32 Tools)
|
|
355
|
+
|
|
356
|
+
| Category | Tools |
|
|
357
|
+
|----------|-------|
|
|
358
|
+
| Element CRUD (9) | `create_element`, `get_element`, `update_element`, `delete_element`, `query_elements`, `batch_create_elements`, `duplicate_elements`, `search_elements`, `element_history` |
|
|
359
|
+
| Layout (6) | `align_elements`, `distribute_elements`, `group_elements`, `ungroup_elements`, `lock_elements`, `unlock_elements` |
|
|
360
|
+
| Scene (4) | `describe_scene`, `get_canvas_screenshot`, `get_resource`, `read_diagram_guide` |
|
|
361
|
+
| File I/O (4) | `export_scene`, `import_scene`, `export_to_image`, `export_to_excalidraw_url` |
|
|
362
|
+
| State (3) | `clear_canvas`, `snapshot_scene`, `restore_snapshot` |
|
|
363
|
+
| Viewport (1) | `set_viewport` |
|
|
364
|
+
| Tenants (2) | `list_tenants`, `switch_tenant` |
|
|
365
|
+
| Projects (2) | `list_projects`, `switch_project` |
|
|
366
|
+
| Conversion (1) | `create_from_mermaid` (⚠ low quality — use `batch_create_elements` instead) |
|
|
367
|
+
|
|
368
|
+
## References
|
|
369
|
+
|
|
370
|
+
- `references/cheatsheet.md`: Complete MCP tool list (32 tools) + REST API endpoints + payload shapes + env vars
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Excalidraw Skill Cheatsheet
|
|
2
|
+
|
|
3
|
+
## Defaults
|
|
4
|
+
|
|
5
|
+
- Canvas base URL: configurable via `CANVAS_PORT` env var (default `3000`), resolves to `http://localhost:<CANVAS_PORT>`
|
|
6
|
+
- Canvas health: `GET /health`
|
|
7
|
+
- Data persistence: SQLite database at `~/.excalidraw-mcp/excalidraw.db`
|
|
8
|
+
- Multi-tenancy: each Cursor workspace auto-creates a tenant (hash of workspace path)
|
|
9
|
+
|
|
10
|
+
## MCP Tools (32 total)
|
|
11
|
+
|
|
12
|
+
### Element CRUD
|
|
13
|
+
|
|
14
|
+
| Tool | Description | Required params |
|
|
15
|
+
|------|-------------|-----------------|
|
|
16
|
+
| `create_element` | Create shape/text/arrow/line | `type`, `x`, `y` |
|
|
17
|
+
| `get_element` | Get single element by ID | `id` |
|
|
18
|
+
| `update_element` | Update element properties | `id` |
|
|
19
|
+
| `delete_element` | Delete element | `id` |
|
|
20
|
+
| `query_elements` | Query by type | (optional) `type` |
|
|
21
|
+
| `batch_create_elements` | Create many at once (recommended) | `elements[]` |
|
|
22
|
+
| `duplicate_elements` | Clone with offset | `elementIds[]`, (optional) `offsetX`, `offsetY` |
|
|
23
|
+
| `search_elements` | Full-text search over labels/text | `query` |
|
|
24
|
+
| `element_history` | View version history (create/update/delete ops) | (optional) `elementId`, `limit` (default 50) |
|
|
25
|
+
|
|
26
|
+
### Layout & Organization
|
|
27
|
+
|
|
28
|
+
| Tool | Description | Required params |
|
|
29
|
+
|------|-------------|-----------------|
|
|
30
|
+
| `align_elements` | Align to left/center/right/top/middle/bottom | `elementIds[]`, `alignment` |
|
|
31
|
+
| `distribute_elements` | Even spacing horizontal/vertical | `elementIds[]`, `direction` |
|
|
32
|
+
| `group_elements` | Group elements | `elementIds[]` |
|
|
33
|
+
| `ungroup_elements` | Ungroup | `groupId` |
|
|
34
|
+
| `lock_elements` | Lock elements | `elementIds[]` |
|
|
35
|
+
| `unlock_elements` | Unlock elements | `elementIds[]` |
|
|
36
|
+
|
|
37
|
+
### Scene Awareness (Iterative Refinement)
|
|
38
|
+
|
|
39
|
+
| Tool | Description | Required params |
|
|
40
|
+
|------|-------------|-----------------|
|
|
41
|
+
| `describe_scene` | AI-readable scene description (types, positions, labels, connections, bounding box) | (none) |
|
|
42
|
+
| `get_canvas_screenshot` | Returns PNG image of canvas for visual verification (may return empty — use Chrome DevTools as fallback) | (optional) `background` |
|
|
43
|
+
| `get_resource` | Get scene/library/theme/elements | `resource` |
|
|
44
|
+
| `read_diagram_guide` | Get design best practices (colors, sizing, layout, anti-patterns) | (none) |
|
|
45
|
+
|
|
46
|
+
### File I/O & Export
|
|
47
|
+
|
|
48
|
+
| Tool | Description | Required params |
|
|
49
|
+
|------|-------------|-----------------|
|
|
50
|
+
| `export_scene` | Export to .excalidraw JSON | (optional) `filePath` |
|
|
51
|
+
| `import_scene` | Import from .excalidraw JSON | `mode` ("replace"\|"merge"), `filePath` or `data` |
|
|
52
|
+
| `export_to_image` | Export to PNG/SVG (needs browser) | `format` ("png"\|"svg"), (optional) `filePath`, `background` |
|
|
53
|
+
| `export_to_excalidraw_url` | Upload & get shareable excalidraw.com URL (may fail if org blocks excalidraw.com) | (none) |
|
|
54
|
+
|
|
55
|
+
### State Management
|
|
56
|
+
|
|
57
|
+
| Tool | Description | Required params |
|
|
58
|
+
|------|-------------|-----------------|
|
|
59
|
+
| `clear_canvas` | Remove all elements from active project | (none) |
|
|
60
|
+
| `snapshot_scene` | Save named snapshot of current canvas state | `name` |
|
|
61
|
+
| `restore_snapshot` | Restore from snapshot (may not reload into view — re-fetch if canvas appears empty) | `name` |
|
|
62
|
+
|
|
63
|
+
### Viewport & Camera
|
|
64
|
+
|
|
65
|
+
| Tool | Description | Required params |
|
|
66
|
+
|------|-------------|-----------------|
|
|
67
|
+
| `set_viewport` | Control camera: zoom-to-fit, center on element, manual zoom/scroll (needs browser) | (optional) `scrollToContent`, `scrollToElementId`, `zoom`, `offsetX`, `offsetY` |
|
|
68
|
+
|
|
69
|
+
### Multi-Tenancy (Workspaces)
|
|
70
|
+
|
|
71
|
+
| Tool | Description | Required params |
|
|
72
|
+
|------|-------------|-----------------|
|
|
73
|
+
| `list_tenants` | List all tenants (workspaces). Each tenant maps to a Cursor workspace. | (none) |
|
|
74
|
+
| `switch_tenant` | Switch active tenant. All later operations use that tenant's projects/elements. | `tenantId` |
|
|
75
|
+
|
|
76
|
+
### Projects (Within a Tenant)
|
|
77
|
+
|
|
78
|
+
| Tool | Description | Required params |
|
|
79
|
+
|------|-------------|-----------------|
|
|
80
|
+
| `list_projects` | List all diagram projects in the active tenant | (none) |
|
|
81
|
+
| `switch_project` | Switch active project or create a new one | (optional) `projectId`, `createName`, `createDescription` |
|
|
82
|
+
|
|
83
|
+
### Conversion
|
|
84
|
+
|
|
85
|
+
| Tool | Description | Required params |
|
|
86
|
+
|------|-------------|-----------------|
|
|
87
|
+
| `create_from_mermaid` | Mermaid diagram to Excalidraw (⚠ produces low-quality output — use `batch_create_elements` for production diagrams) | `mermaidDiagram` |
|
|
88
|
+
|
|
89
|
+
## Key Notes
|
|
90
|
+
|
|
91
|
+
### MCP vs REST API Format Differences
|
|
92
|
+
|
|
93
|
+
| Concept | MCP Tool Format | REST API Format |
|
|
94
|
+
|---------|----------------|-----------------|
|
|
95
|
+
| Shape labels | `"text": "My Label"` (auto-converts) | `"label": {"text": "My Label"}` |
|
|
96
|
+
| Arrow binding | `"startElementId": "id"` / `"endElementId": "id"` | `"start": {"id": "id"}` / `"end": {"id": "id"}` |
|
|
97
|
+
| `fontFamily` | String `"1"` or omit | String `"1"` or omit (never a number) |
|
|
98
|
+
| Tenant scoping | Auto (uses active tenant) | Include `X-Tenant-Id` header on every request |
|
|
99
|
+
|
|
100
|
+
### Element Creation Best Practices
|
|
101
|
+
|
|
102
|
+
- **Always set `roughness: 0`** for clean, professional diagrams (default is hand-drawn).
|
|
103
|
+
- **Always set `strokeWidth: 2`** on arrows for visibility.
|
|
104
|
+
- **Create shapes first, arrows second** (two separate `batch_create_elements` calls).
|
|
105
|
+
- **Assign custom `id`** to every shape so arrows can reference it.
|
|
106
|
+
- **Size shapes for their text** — Virgil font is ~30% wider than standard. Use sizing formulas from SKILL.md.
|
|
107
|
+
- `points` accepts both `[[x,y]]` tuples and `[{x,y}]` objects — normalized automatically.
|
|
108
|
+
- **Curved arrows**: Use `"roundness": {"type": 2}` with 3+ points. **Elbowed arrows**: Use `"elbowed": true`.
|
|
109
|
+
|
|
110
|
+
### Multi-Tenancy Architecture
|
|
111
|
+
|
|
112
|
+
- **Tenant**: Maps to a Cursor workspace. Auto-created on MCP startup from workspace path hash.
|
|
113
|
+
- **Project**: Groups diagrams within a tenant. Default project created per tenant.
|
|
114
|
+
- **Elements**: Belong to the active project within the active tenant.
|
|
115
|
+
- **Hierarchy**: Tenant → Project → Elements
|
|
116
|
+
- **Concurrent instances**: Multiple Cursor windows each send `X-Tenant-Id` header for isolation. SQLite `busy_timeout` handles concurrent writes.
|
|
117
|
+
- **Frontend workspace switcher**: Dropdown in the canvas UI header labeled "Workspace: [name] ▾" with search filter.
|
|
118
|
+
|
|
119
|
+
## Canvas REST API (HTTP)
|
|
120
|
+
|
|
121
|
+
All endpoints accept an optional `X-Tenant-Id` header to scope operations to a specific tenant.
|
|
122
|
+
|
|
123
|
+
### Elements
|
|
124
|
+
|
|
125
|
+
| Method | Endpoint | Description |
|
|
126
|
+
|--------|----------|-------------|
|
|
127
|
+
| `GET` | `/api/elements` | List all elements in active project |
|
|
128
|
+
| `GET` | `/api/elements/:id` | Get element by ID |
|
|
129
|
+
| `POST` | `/api/elements` | Create element |
|
|
130
|
+
| `PUT` | `/api/elements/:id` | Update element |
|
|
131
|
+
| `DELETE` | `/api/elements/:id` | Delete element |
|
|
132
|
+
| `DELETE` | `/api/elements/clear` | Clear all elements |
|
|
133
|
+
| `GET` | `/api/elements/search?type=...` | Search with filters |
|
|
134
|
+
| `POST` | `/api/elements/batch` | Batch create |
|
|
135
|
+
| `POST` | `/api/elements/sync` | Full sync (clear + write all elements) |
|
|
136
|
+
| `POST` | `/api/elements/from-mermaid` | Mermaid conversion via frontend |
|
|
137
|
+
|
|
138
|
+
### Tenants
|
|
139
|
+
|
|
140
|
+
| Method | Endpoint | Description |
|
|
141
|
+
|--------|----------|-------------|
|
|
142
|
+
| `GET` | `/api/tenants` | List all tenants |
|
|
143
|
+
| `GET` | `/api/tenant/active` | Get active tenant |
|
|
144
|
+
| `PUT` | `/api/tenant/active` | Switch active tenant `{"tenantId": "..."}` (broadcasts to all WebSocket clients) |
|
|
145
|
+
|
|
146
|
+
### Export
|
|
147
|
+
|
|
148
|
+
| Method | Endpoint | Description |
|
|
149
|
+
|--------|----------|-------------|
|
|
150
|
+
| `POST` | `/api/export/image` | Request image export (needs browser) |
|
|
151
|
+
| `POST` | `/api/export/image/result` | Frontend posts export result back |
|
|
152
|
+
|
|
153
|
+
### Viewport
|
|
154
|
+
|
|
155
|
+
| Method | Endpoint | Description |
|
|
156
|
+
|--------|----------|-------------|
|
|
157
|
+
| `POST` | `/api/viewport` | Set viewport/camera (needs browser) |
|
|
158
|
+
| `POST` | `/api/viewport/result` | Frontend posts viewport result back |
|
|
159
|
+
|
|
160
|
+
### Snapshots
|
|
161
|
+
|
|
162
|
+
| Method | Endpoint | Description |
|
|
163
|
+
|--------|----------|-------------|
|
|
164
|
+
| `POST` | `/api/snapshots` | Save snapshot `{"name": "..."}` |
|
|
165
|
+
| `GET` | `/api/snapshots` | List all snapshots |
|
|
166
|
+
| `GET` | `/api/snapshots/:name` | Get snapshot by name |
|
|
167
|
+
|
|
168
|
+
### System
|
|
169
|
+
|
|
170
|
+
| Method | Endpoint | Description |
|
|
171
|
+
|--------|----------|-------------|
|
|
172
|
+
| `GET` | `/health` | Health check |
|
|
173
|
+
| `GET` | `/api/sync/status` | Element count and WebSocket stats |
|
|
174
|
+
|
|
175
|
+
## Skill Scripts
|
|
176
|
+
|
|
177
|
+
All scripts accept `--url <canvasUrl>` (defaults to `EXPRESS_SERVER_URL`).
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
node scripts/healthcheck.cjs
|
|
181
|
+
node scripts/clear-canvas.cjs
|
|
182
|
+
node scripts/export-elements.cjs --out diagram.elements.json
|
|
183
|
+
node scripts/import-elements.cjs --in diagram.elements.json --mode batch|sync
|
|
184
|
+
node scripts/create-element.cjs --data '{...}'
|
|
185
|
+
node scripts/update-element.cjs --id <id> --data '{...}'
|
|
186
|
+
node scripts/delete-element.cjs --id <id>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Environment Variables
|
|
190
|
+
|
|
191
|
+
| Variable | Default | Description |
|
|
192
|
+
|----------|---------|-------------|
|
|
193
|
+
| `CANVAS_PORT` | `3000` | Port the canvas server listens on |
|
|
194
|
+
| `EXPRESS_SERVER_URL` | `http://localhost:3000` | Full canvas URL (derived from CANVAS_PORT if not set) |
|
|
195
|
+
| `EXCALIDRAW_EXPORT_DIR` | `process.cwd()` | Allowed base directory for file exports (path traversal protection) |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const out = { url: DEFAULT_URL };
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const a = argv[i];
|
|
10
|
+
if (a === "--url") out.url = argv[++i];
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
if (typeof fetch !== "function") {
|
|
17
|
+
throw new Error("This script requires Node 18+ (global fetch).");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { url } = parseArgs(process.argv.slice(2));
|
|
21
|
+
const baseUrl = url.replace(/\/$/, "");
|
|
22
|
+
|
|
23
|
+
const res = await fetch(`${baseUrl}/api/elements/clear`, {
|
|
24
|
+
method: "DELETE",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const json = await res.json().catch(() => null);
|
|
28
|
+
if (!res.ok || !json || json.success !== true) {
|
|
29
|
+
throw new Error(`Failed to clear canvas: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`Cleared canvas (${json.count} elements removed)`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main().catch((err) => {
|
|
36
|
+
console.error(err?.stack || String(err));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
console.error(
|
|
10
|
+
[
|
|
11
|
+
"Usage:",
|
|
12
|
+
" node scripts/create-element.cjs (--data <json> | --file <path>) [--url <canvasUrl>]",
|
|
13
|
+
"",
|
|
14
|
+
"Examples:",
|
|
15
|
+
' node scripts/create-element.cjs --data \'{"type":"rectangle","x":100,"y":100,"width":300,"height":200}\'',
|
|
16
|
+
" node scripts/create-element.cjs --file element.json",
|
|
17
|
+
].join("\n"),
|
|
18
|
+
);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const out = { url: DEFAULT_URL, data: null, file: null };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "--url") out.url = argv[++i];
|
|
27
|
+
else if (a === "--data") out.data = argv[++i];
|
|
28
|
+
else if (a === "--file") out.file = argv[++i];
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson({ data, file }) {
|
|
34
|
+
if (data) return JSON.parse(data);
|
|
35
|
+
if (file) return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
36
|
+
usage();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
if (typeof fetch !== "function") {
|
|
41
|
+
throw new Error("This script requires Node 18+ (global fetch).");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const args = parseArgs(process.argv.slice(2));
|
|
45
|
+
const payload = readJson(args);
|
|
46
|
+
|
|
47
|
+
const baseUrl = args.url.replace(/\/$/, "");
|
|
48
|
+
const res = await fetch(`${baseUrl}/api/elements`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const json = await res.json().catch(() => null);
|
|
55
|
+
if (!res.ok || !json || json.success !== true) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to create element: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.stdout.write(JSON.stringify(json.element, null, 2) + "\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch((err) => {
|
|
65
|
+
console.error(err?.stack || String(err));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
|