interactive-drawer 0.4.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.
Files changed (124) hide show
  1. package/README.md +255 -0
  2. package/dist/checkpoint-store.d.ts +39 -0
  3. package/dist/index.js +1606 -0
  4. package/dist/mcp-app.html +124 -0
  5. package/dist/server.d.ts +12 -0
  6. package/dist/server.js +698 -0
  7. package/dist/shared.d.ts +32 -0
  8. package/dist/viewer/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
  9. package/dist/viewer/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
  10. package/dist/viewer/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
  11. package/dist/viewer/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
  12. package/dist/viewer/assets/Tableau10-B-NsZVaP.js +1 -0
  13. package/dist/viewer/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  14. package/dist/viewer/assets/ar-SA-G6X2FPQ2-DadQCH2-.js +10 -0
  15. package/dist/viewer/assets/arc-CQgrOt5x.js +1 -0
  16. package/dist/viewer/assets/array-BKyUJesY.js +1 -0
  17. package/dist/viewer/assets/az-AZ-76LH7QW2-DQfUuqqi.js +1 -0
  18. package/dist/viewer/assets/bg-BG-XCXSNQG7-BB40UBFK.js +5 -0
  19. package/dist/viewer/assets/blockDiagram-c4efeb88-BaF5AmVI.js +118 -0
  20. package/dist/viewer/assets/bn-BD-2XOGV67Q-uzDHfiqC.js +5 -0
  21. package/dist/viewer/assets/c4Diagram-c83219d4-hWhr-GlJ.js +10 -0
  22. package/dist/viewer/assets/ca-ES-6MX7JW3Y-BW7AckRl.js +8 -0
  23. package/dist/viewer/assets/channel-CXejbLtU.js +1 -0
  24. package/dist/viewer/assets/classDiagram-beda092f-CymCu8CT.js +2 -0
  25. package/dist/viewer/assets/classDiagram-v2-2358418a-DJAISyxA.js +2 -0
  26. package/dist/viewer/assets/clone-CfbodbqL.js +1 -0
  27. package/dist/viewer/assets/createText-1719965b-BnEANXd0.js +7 -0
  28. package/dist/viewer/assets/cs-CZ-2BRQDIVT-DuVk-qPr.js +11 -0
  29. package/dist/viewer/assets/da-DK-5WZEPLOC-DFahLFHr.js +5 -0
  30. package/dist/viewer/assets/de-DE-XR44H4JA-DoRzoyVr.js +8 -0
  31. package/dist/viewer/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
  32. package/dist/viewer/assets/directory-open-4ed118d0-CunoC1EB.js +1 -0
  33. package/dist/viewer/assets/edges-96097737-BaZooScF.js +4 -0
  34. package/dist/viewer/assets/el-GR-BZB4AONW--pwLECF8.js +10 -0
  35. package/dist/viewer/assets/erDiagram-0228fc6a-57itH3DJ.js +51 -0
  36. package/dist/viewer/assets/es-ES-U4NZUMDT-C1NjAcMt.js +9 -0
  37. package/dist/viewer/assets/eu-ES-A7QVB2H4-Ck1DCJa4.js +11 -0
  38. package/dist/viewer/assets/fa-IR-HGAKTJCU-tLLl_5WD.js +8 -0
  39. package/dist/viewer/assets/fi-FI-Z5N7JZ37-DVtDP0gn.js +6 -0
  40. package/dist/viewer/assets/file-open-002ab408-DIuFHtCF.js +1 -0
  41. package/dist/viewer/assets/file-open-7c801643-684qeFg4.js +1 -0
  42. package/dist/viewer/assets/file-save-3189631c-C1wFhQhH.js +1 -0
  43. package/dist/viewer/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
  44. package/dist/viewer/assets/flowDb-c6c81e3f-C7AEsokK.js +10 -0
  45. package/dist/viewer/assets/flowDiagram-50d868cf-CssTUsiu.js +4 -0
  46. package/dist/viewer/assets/flowDiagram-v2-4f6560a1-D0GG9NIT.js +1 -0
  47. package/dist/viewer/assets/flowchart-elk-definition-6af322e1-CRyPEh1i.js +139 -0
  48. package/dist/viewer/assets/fr-FR-RHASNOE6-T_qZ0zEK.js +9 -0
  49. package/dist/viewer/assets/ganttDiagram-a2739b55-DsyTeswu.js +257 -0
  50. package/dist/viewer/assets/gitGraphDiagram-82fe8481-D85sePVV.js +70 -0
  51. package/dist/viewer/assets/gl-ES-HMX3MZ6V-IW3daBzE.js +10 -0
  52. package/dist/viewer/assets/graph-i-2SK2w1.js +1 -0
  53. package/dist/viewer/assets/he-IL-6SHJWFNN-CUxTwbdR.js +10 -0
  54. package/dist/viewer/assets/hi-IN-IWLTKZ5I-DvQFf43e.js +4 -0
  55. package/dist/viewer/assets/hu-HU-A5ZG7DT2-CO4Q4G1z.js +7 -0
  56. package/dist/viewer/assets/id-ID-SAP4L64H-Bf_UXdp6.js +10 -0
  57. package/dist/viewer/assets/image-blob-reduce.esm-D6s-rqMO.js +7 -0
  58. package/dist/viewer/assets/index-5325376f-BRzpdHqA.js +1 -0
  59. package/dist/viewer/assets/index-BDC2daNC.js +275 -0
  60. package/dist/viewer/assets/index-BcAUh2Nm.css +1 -0
  61. package/dist/viewer/assets/index-rEzfVlKD.js +97 -0
  62. package/dist/viewer/assets/infoDiagram-8eee0895-BkRxMuec.js +7 -0
  63. package/dist/viewer/assets/init-Gi6I4Gst.js +1 -0
  64. package/dist/viewer/assets/it-IT-JPQ66NNP-AwjkJc7y.js +11 -0
  65. package/dist/viewer/assets/ja-JP-DBVTYXUO-07sucMhN.js +8 -0
  66. package/dist/viewer/assets/journeyDiagram-c64418c1-6-ZVFaXG.js +139 -0
  67. package/dist/viewer/assets/kaa-6HZHGXH3-CXk_Wo6h.js +1 -0
  68. package/dist/viewer/assets/kab-KAB-ZGHBKWFO-D88OOW68.js +8 -0
  69. package/dist/viewer/assets/katex-BkgLa5T_.js +261 -0
  70. package/dist/viewer/assets/kk-KZ-P5N5QNE5-TzX_GmjC.js +1 -0
  71. package/dist/viewer/assets/km-KH-HSX4SM5Z-Dy-luINx.js +11 -0
  72. package/dist/viewer/assets/ko-KR-MTYHY66A-Bno63g5m.js +9 -0
  73. package/dist/viewer/assets/ku-TR-6OUDTVRD-Daw-B1bN.js +9 -0
  74. package/dist/viewer/assets/layout-BSQSdjji.js +1 -0
  75. package/dist/viewer/assets/line-DajA25FK.js +1 -0
  76. package/dist/viewer/assets/linear-BUWo07xp.js +1 -0
  77. package/dist/viewer/assets/lt-LT-XHIRWOB4-C3_9a2Cz.js +3 -0
  78. package/dist/viewer/assets/lv-LV-5QDEKY6T-3H9zX9pi.js +7 -0
  79. package/dist/viewer/assets/mindmap-definition-8da855dc-DIeJ0MUj.js +425 -0
  80. package/dist/viewer/assets/mr-IN-CRQNXWMA-DLf6m35n.js +13 -0
  81. package/dist/viewer/assets/my-MM-5M5IBNSE-CW_RWZBw.js +1 -0
  82. package/dist/viewer/assets/nb-NO-T6EIAALU-B0ZpuoZJ.js +10 -0
  83. package/dist/viewer/assets/nl-NL-IS3SIHDZ-nZ0JmIIM.js +8 -0
  84. package/dist/viewer/assets/nn-NO-6E72VCQL-C_dDeYAp.js +8 -0
  85. package/dist/viewer/assets/oc-FR-POXYY2M6-CsidHxkc.js +8 -0
  86. package/dist/viewer/assets/ordinal-Cboi1Yqb.js +1 -0
  87. package/dist/viewer/assets/pa-IN-N4M65BXN-DuEUGA-r.js +4 -0
  88. package/dist/viewer/assets/path-CbwjOpE9.js +1 -0
  89. package/dist/viewer/assets/pica-B90j7hbh.js +7 -0
  90. package/dist/viewer/assets/pieDiagram-a8764435-CY0HndHR.js +35 -0
  91. package/dist/viewer/assets/pl-PL-T2D74RX3-D87DC8D9.js +9 -0
  92. package/dist/viewer/assets/pt-BR-5N22H2LF-BtSoEeex.js +9 -0
  93. package/dist/viewer/assets/pt-PT-UZXXM6DQ-D4bk4PLe.js +9 -0
  94. package/dist/viewer/assets/quadrantDiagram-1e28029f-DGki2tT-.js +7 -0
  95. package/dist/viewer/assets/requirementDiagram-08caed73-BkzKWnbZ.js +52 -0
  96. package/dist/viewer/assets/ro-RO-JPDTUUEW-Ra2U-BVb.js +11 -0
  97. package/dist/viewer/assets/roundRect-0PYZxl1G.js +1 -0
  98. package/dist/viewer/assets/ru-RU-B4JR7IUQ-DAMFn0BJ.js +9 -0
  99. package/dist/viewer/assets/sankeyDiagram-a04cb91d-C1WQeKAS.js +8 -0
  100. package/dist/viewer/assets/sequenceDiagram-c5b8d532-BQy1AIyh.js +122 -0
  101. package/dist/viewer/assets/si-LK-N5RQ5JYF-DtRsuwQ6.js +1 -0
  102. package/dist/viewer/assets/sk-SK-C5VTKIMK-Dph_9bOP.js +6 -0
  103. package/dist/viewer/assets/sl-SI-NN7IZMDC-CzYryWZd.js +6 -0
  104. package/dist/viewer/assets/stateDiagram-1ecb1508-f7QEpJSj.js +1 -0
  105. package/dist/viewer/assets/stateDiagram-v2-c2b004d7-B8MjoGqH.js +1 -0
  106. package/dist/viewer/assets/styles-b4e223ce-DfLT12Nx.js +160 -0
  107. package/dist/viewer/assets/styles-ca3715f6-CnuYZzQT.js +207 -0
  108. package/dist/viewer/assets/styles-d45a18b0-Ci9tlpxY.js +116 -0
  109. package/dist/viewer/assets/subset-shared.chunk-B9IoriSA.js +84 -0
  110. package/dist/viewer/assets/subset-worker.chunk-Bpe7TPYu.js +1 -0
  111. package/dist/viewer/assets/sv-SE-XGPEYMSR-B4Y9n4LB.js +10 -0
  112. package/dist/viewer/assets/svgDrawCommon-b86b1483-4HzjGm6B.js +1 -0
  113. package/dist/viewer/assets/ta-IN-2NMHFXQM-DE7pdoSM.js +9 -0
  114. package/dist/viewer/assets/th-TH-HPSO5L25-Dd8iADEt.js +2 -0
  115. package/dist/viewer/assets/timeline-definition-faaaa080-DYgcS_Y5.js +61 -0
  116. package/dist/viewer/assets/tr-TR-DEFEU3FU-DTOmyIrt.js +7 -0
  117. package/dist/viewer/assets/uk-UA-QMV73CPH-B05zHRYx.js +6 -0
  118. package/dist/viewer/assets/vi-VN-M7AON7JQ-ChW6Q5sn.js +5 -0
  119. package/dist/viewer/assets/xychartDiagram-f5964ef8-BrW97HI1.js +7 -0
  120. package/dist/viewer/assets/zh-CN-LNUGB5OW-DF3LdZpE.js +10 -0
  121. package/dist/viewer/assets/zh-HK-E62DVLB3-C3QgGFS0.js +1 -0
  122. package/dist/viewer/assets/zh-TW-RAJ6MFWO-ciUClBxf.js +9 -0
  123. package/dist/viewer/index.html +13 -0
  124. package/package.json +67 -0
package/dist/server.js ADDED
@@ -0,0 +1,698 @@
1
+ // src/server.ts
2
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { deflateSync } from "node:zlib";
8
+ import { z } from "zod/v4";
9
+
10
+ // src/shared.ts
11
+ import crypto from "node:crypto";
12
+ var MAX_INPUT_BYTES = 5 * 1024 * 1024;
13
+ var RECALL_CHEAT_SHEET = `# Excalidraw Element Format
14
+
15
+ Thanks for calling read_me! Do NOT call it again in this conversation \u2014 you will not see anything new. Now use create_view to draw.
16
+
17
+ ## Basic Workflow
18
+
19
+ Follow these steps in order:
20
+
21
+ 1. **create_session** \u2014 Call this first. You will get a session key and a viewer URL (using the server base URL shown above).
22
+ **IMPORTANT: Tell the user to open the viewer URL in their browser NOW**, before you start drawing. This way they can watch the diagram appear in real time.
23
+ Always use the exact viewer URL returned by create_session \u2014 never guess or construct URLs yourself.
24
+
25
+ 2. **read_me** \u2014 You already called this (you're reading it now). No need to call again.
26
+
27
+ 3. **create_view** \u2014 Send elements JSON with the session key to draw the diagram. The viewer page updates automatically.
28
+ You can call create_view multiple times to update the diagram (use restoreCheckpoint for incremental edits).
29
+
30
+ ## Drawing Style (ASK BEFORE DRAWING)
31
+
32
+ Before your first create_view call, **ask the user** which font and sloppiness (roughness) they prefer. Present the combinations below and let them choose. If they have no preference, use the defaults (Excalifont + Artist).
33
+
34
+ ### Fonts (fontFamily)
35
+ | Value | Name | Style |
36
+ |-------|------|-------|
37
+ | \`1\` | **Excalifont** (default) | Hand-drawn, casual \u2014 matches Excalidraw's sketchy aesthetic |
38
+ | \`2\` | **Nunito** | Clean, rounded sans-serif \u2014 polished and modern |
39
+ | \`3\` | **Comic Shanns** | Comic/playful \u2014 informal and fun |
40
+
41
+ Set \`fontFamily\` on text elements and in \`label\` objects: \`"fontFamily": 2\`
42
+
43
+ ### Sloppiness (roughness)
44
+ | Value | Name | Style |
45
+ |-------|------|-------|
46
+ | \`0\` | **Architect** | Precise, clean lines \u2014 technical / formal |
47
+ | \`1\` | **Artist** (default) | Slightly rough \u2014 natural hand-drawn feel |
48
+ | \`2\` | **Cartoonist** | Very rough, wobbly \u2014 playful / sketch-like |
49
+
50
+ Set \`roughness\` on shape and arrow elements: \`"roughness": 0\`
51
+
52
+ ### Recommended Combinations
53
+ | Combination | Font | Roughness | Best For |
54
+ |-------------|------|-----------|----------|
55
+ | **Sketch** (default) | Excalifont (1) | Artist (1) | General diagrams, brainstorming |
56
+ | **Clean** | Nunito (2) | Architect (0) | Technical docs, presentations |
57
+ | **Playful** | Comic Shanns (3) | Cartoonist (2) | Informal, fun, comics |
58
+ | **Hand-drawn formal** | Excalifont (1) | Architect (0) | Hand-written notes, precise sketches |
59
+ | **Polished casual** | Nunito (2) | Artist (1) | Blog posts, explainers |
60
+ | **Comic precise** | Comic Shanns (3) | Architect (0) | Clean comic panels, game design |
61
+
62
+ Once the user picks a style, apply the chosen \`fontFamily\` and \`roughness\` **consistently to all elements** throughout the diagram. Do not mix styles unless the user explicitly requests it.
63
+
64
+ ## Color Palette (use consistently across all tools)
65
+
66
+ ### Primary Colors
67
+ | Name | Hex | Use |
68
+ |------|-----|-----|
69
+ | Blue | \`#4a9eed\` | Primary actions, links, data series 1 |
70
+ | Amber | \`#f59e0b\` | Warnings, highlights, data series 2 |
71
+ | Green | \`#22c55e\` | Success, positive, data series 3 |
72
+ | Red | \`#ef4444\` | Errors, negative, data series 4 |
73
+ | Purple | \`#8b5cf6\` | Accents, special items, data series 5 |
74
+ | Pink | \`#ec4899\` | Decorative, data series 6 |
75
+ | Cyan | \`#06b6d4\` | Info, secondary, data series 7 |
76
+ | Lime | \`#84cc16\` | Extra, data series 8 |
77
+
78
+ ### Excalidraw Fills (pastel, for shape backgrounds)
79
+ | Color | Hex | Good For |
80
+ |-------|-----|----------|
81
+ | Light Blue | \`#a5d8ff\` | Input, sources, primary nodes |
82
+ | Light Green | \`#b2f2bb\` | Success, output, completed |
83
+ | Light Orange | \`#ffd8a8\` | Warning, pending, external |
84
+ | Light Purple | \`#d0bfff\` | Processing, middleware, special |
85
+ | Light Red | \`#ffc9c9\` | Error, critical, alerts |
86
+ | Light Yellow | \`#fff3bf\` | Notes, decisions, planning |
87
+ | Light Teal | \`#c3fae8\` | Storage, data, memory |
88
+ | Light Pink | \`#eebefa\` | Analytics, metrics |
89
+
90
+ ### Background Zones (use with opacity: 30 for layered diagrams)
91
+ | Color | Hex | Good For |
92
+ |-------|-----|----------|
93
+ | Blue zone | \`#dbe4ff\` | UI / frontend layer |
94
+ | Purple zone | \`#e5dbff\` | Logic / agent layer |
95
+ | Green zone | \`#d3f9d8\` | Data / tool layer |
96
+
97
+ ---
98
+
99
+ ## Excalidraw Elements
100
+
101
+ ### Required Fields (all elements)
102
+ \`type\`, \`id\` (unique string), \`x\`, \`y\`, \`width\`, \`height\`
103
+
104
+ ### Defaults (skip these)
105
+ strokeColor="#1e1e1e", backgroundColor="transparent", fillStyle="solid", strokeWidth=2, roughness=1, opacity=100
106
+ Canvas background is white.
107
+
108
+ ### Element Types
109
+
110
+ **Rectangle**: \`{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 100 }\`
111
+ - \`roundness: { type: 3 }\` for rounded corners
112
+ - \`backgroundColor: "#a5d8ff"\`, \`fillStyle: "solid"\` for filled
113
+
114
+ **Ellipse**: \`{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }\`
115
+
116
+ **Diamond**: \`{ "type": "diamond", "id": "d1", "x": 100, "y": 100, "width": 150, "height": 150 }\`
117
+
118
+ **Labeled shape (PREFERRED)**: Add \`label\` to any shape for auto-centered text. No separate text element needed.
119
+ \`{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 80, "label": { "text": "Hello", "fontSize": 20 } }\`
120
+ - Works on rectangle, ellipse, diamond
121
+ - Text auto-centers and container auto-resizes to fit
122
+ - Saves tokens vs separate text elements
123
+
124
+ **Labeled arrow**: \`"label": { "text": "connects" }\` on an arrow element.
125
+
126
+ **Standalone text** (titles, annotations only):
127
+ \`{ "type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello", "fontSize": 20 }\`
128
+ - x is the LEFT edge of the text. To center text at position cx: set x = cx - estimatedWidth/2
129
+ - estimatedWidth \u2248 text.length \xD7 fontSize \xD7 0.5
130
+ - Do NOT rely on textAlign or width for positioning \u2014 they only affect multi-line wrapping
131
+
132
+ **Arrow**: \`{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0, "points": [[0,0],[200,0]], "endArrowhead": "arrow" }\`
133
+ - points: [dx, dy] offsets from element x,y
134
+ - endArrowhead: null | "arrow" | "bar" | "dot" | "triangle"
135
+
136
+ ### Arrow Bindings
137
+ Arrow: \`"startBinding": { "elementId": "r1", "fixedPoint": [1, 0.5] }\`
138
+ fixedPoint: top=[0.5,0], bottom=[0.5,1], left=[0,0.5], right=[1,0.5]
139
+
140
+ **cameraUpdate** (pseudo-element \u2014 controls the viewport, not drawn):
141
+ \`{ "type": "cameraUpdate", "width": 800, "height": 600, "x": 0, "y": 0 }\`
142
+ - x, y: top-left corner of the visible area (scene coordinates)
143
+ - width, height: size of the visible area \u2014 MUST be 4:3 ratio (400\xD7300, 600\xD7450, 800\xD7600, 1200\xD7900, 1600\xD71200)
144
+ - Animates smoothly between positions \u2014 use multiple cameraUpdates to guide attention as you draw
145
+ - No \`id\` needed \u2014 this is not a drawn element
146
+
147
+ **delete** (pseudo-element \u2014 removes elements by id):
148
+ \`{ "type": "delete", "ids": "b2,a1,t3" }\`
149
+ - Comma-separated list of element ids to remove
150
+ - Also removes bound text elements (matching \`containerId\`)
151
+ - Place AFTER the elements you want to remove
152
+ - Never reuse a deleted id \u2014 always assign new ids to replacements
153
+
154
+ ### Drawing Order (CRITICAL for streaming)
155
+ - Array order = z-order (first = back, last = front)
156
+ - **Emit progressively**: background \u2192 shape \u2192 its label \u2192 its arrows \u2192 next shape
157
+ - BAD: all rectangles \u2192 all texts \u2192 all arrows
158
+ - GOOD: bg_shape \u2192 shape1 \u2192 text1 \u2192 arrow1 \u2192 shape2 \u2192 text2 \u2192 ...
159
+
160
+ ### Example: Two connected labeled boxes
161
+ \`\`\`json
162
+ [
163
+ { "type": "cameraUpdate", "width": 800, "height": 600, "x": 50, "y": 50 },
164
+ { "type": "rectangle", "id": "b1", "x": 100, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid", "label": { "text": "Start", "fontSize": 20 } },
165
+ { "type": "rectangle", "id": "b2", "x": 450, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid", "label": { "text": "End", "fontSize": 20 } },
166
+ { "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 150, "height": 0, "points": [[0,0],[150,0]], "endArrowhead": "arrow", "startBinding": { "elementId": "b1", "fixedPoint": [1, 0.5] }, "endBinding": { "elementId": "b2", "fixedPoint": [0, 0.5] } }
167
+ ]
168
+ \`\`\`
169
+
170
+ ### Camera & Sizing (CRITICAL for readability)
171
+
172
+ The diagram displays inline at ~700px width. Design for this constraint.
173
+
174
+ **Recommended camera sizes (4:3 aspect ratio ONLY):**
175
+ - Camera **S**: width 400, height 300 \u2014 close-up on a small group (2-3 elements)
176
+ - Camera **M**: width 600, height 450 \u2014 medium view, a section of a diagram
177
+ - Camera **L**: width 800, height 600 \u2014 standard full diagram (DEFAULT)
178
+ - Camera **XL**: width 1200, height 900 \u2014 large diagram overview. WARNING: font size smaller than 18 is unreadable
179
+ - Camera **XXL**: width 1600, height 1200 \u2014 panorama / final overview of complex diagrams. WARNING: minimum readable font size is 21
180
+
181
+ ALWAYS use one of these exact sizes. Non-4:3 viewports cause distortion.
182
+
183
+ **Font size rules:**
184
+ - Minimum fontSize: **16** for body text, labels, descriptions
185
+ - Minimum fontSize: **20** for titles and headings
186
+ - Minimum fontSize: **14** for secondary annotations only (sparingly)
187
+ - NEVER use fontSize below 14 \u2014 it becomes unreadable at display scale
188
+
189
+ **Element sizing rules:**
190
+ - Minimum shape size: 120\xD760 for labeled rectangles/ellipses
191
+ - Leave 20-30px gaps between elements minimum
192
+ - Prefer fewer, larger elements over many tiny ones
193
+
194
+ ALWAYS start with a \`cameraUpdate\` as the FIRST element. For example:
195
+ \`{ "type": "cameraUpdate", "width": 800, "height": 600, "x": 0, "y": 0 }\`
196
+
197
+ - x, y: top-left corner of visible area (scene coordinates)
198
+ - ALWAYS emit the cameraUpdate BEFORE drawing the elements it frames \u2014 camera moves first, then content appears
199
+ - The camera animates smoothly between positions
200
+ - Leave padding: don't match camera size to content size exactly (e.g., 500px content in 800x600 camera)
201
+
202
+ Examples:
203
+ \`{ "type": "cameraUpdate", "width": 800, "height": 600, "x": 0, "y": 0 }\` \u2014 standard view
204
+ \`{ "type": "cameraUpdate", "width": 400, "height": 300, "x": 200, "y": 100 }\` \u2014 zoom into a detail
205
+ \`{ "type": "cameraUpdate", "width": 1600, "height": 1200, "x": -50, "y": -50 }\` \u2014 panorama overview
206
+
207
+ Tip: For large diagrams, emit a cameraUpdate to focus on each section as you draw it.
208
+
209
+ ## Diagram Example
210
+
211
+ Example prompt: "Explain how photosynthesis works"
212
+
213
+ Uses 2 camera positions: start zoomed in (M) for title, then zoom out (L) to reveal the full diagram. Sun art drawn last as a finishing touch.
214
+
215
+ - **Camera 1** (400x300): Draw the title "Photosynthesis" and formula subtitle zoomed in
216
+ - **Camera 2** (800x600): Zoom out \u2014 draw the leaf zone, process flow (Light Reactions \u2192 Calvin Cycle), inputs (Sunlight, Water, CO2), outputs (O2, Glucose), and finally a cute 8-ray sun
217
+
218
+ \`\`\`json
219
+ [
220
+ {"type":"cameraUpdate","width":400,"height":300,"x":200,"y":-20},
221
+ {"type":"text","id":"ti","x":280,"y":10,"text":"Photosynthesis","fontSize":28,"strokeColor":"#1e1e1e"},
222
+ {"type":"text","id":"fo","x":245,"y":48,"text":"6CO2 + 6H2O --> C6H12O6 + 6O2","fontSize":16,"strokeColor":"#757575"},
223
+ {"type":"cameraUpdate","width":800,"height":600,"x":0,"y":-20},
224
+ {"type":"rectangle","id":"lf","x":150,"y":90,"width":520,"height":380,"backgroundColor":"#d3f9d8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","strokeWidth":1,"opacity":35},
225
+ {"type":"text","id":"lfl","x":170,"y":96,"text":"Inside the Leaf","fontSize":16,"strokeColor":"#15803d"},
226
+ {"type":"rectangle","id":"lr","x":190,"y":190,"width":160,"height":70,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","label":{"text":"Light Reactions","fontSize":16}},
227
+ {"type":"arrow","id":"a1","x":350,"y":225,"width":120,"height":0,"points":[[0,0],[120,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","label":{"text":"ATP","fontSize":14}},
228
+ {"type":"rectangle","id":"cc","x":470,"y":190,"width":160,"height":70,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#8b5cf6","label":{"text":"Calvin Cycle","fontSize":16}},
229
+ {"type":"rectangle","id":"sl","x":10,"y":200,"width":120,"height":50,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","label":{"text":"Sunlight","fontSize":16}},
230
+ {"type":"arrow","id":"a2","x":130,"y":225,"width":60,"height":0,"points":[[0,0],[60,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow"},
231
+ {"type":"rectangle","id":"wa","x":200,"y":360,"width":140,"height":50,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","label":{"text":"Water (H2O)","fontSize":16}},
232
+ {"type":"arrow","id":"a3","x":270,"y":360,"width":0,"height":-100,"points":[[0,0],[0,-100]],"strokeColor":"#4a9eed","strokeWidth":2,"endArrowhead":"arrow"},
233
+ {"type":"rectangle","id":"co","x":480,"y":360,"width":130,"height":50,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","label":{"text":"CO2","fontSize":16}},
234
+ {"type":"arrow","id":"a4","x":545,"y":360,"width":0,"height":-100,"points":[[0,0],[0,-100]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow"},
235
+ {"type":"rectangle","id":"ox","x":540,"y":100,"width":100,"height":40,"backgroundColor":"#ffc9c9","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#ef4444","label":{"text":"O2","fontSize":16}},
236
+ {"type":"arrow","id":"a5","x":310,"y":190,"width":230,"height":-50,"points":[[0,0],[230,-50]],"strokeColor":"#ef4444","strokeWidth":2,"endArrowhead":"arrow"},
237
+ {"type":"rectangle","id":"gl","x":690,"y":195,"width":120,"height":60,"backgroundColor":"#c3fae8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","label":{"text":"Glucose","fontSize":18}},
238
+ {"type":"arrow","id":"a6","x":630,"y":225,"width":60,"height":0,"points":[[0,0],[60,0]],"strokeColor":"#22c55e","strokeWidth":2,"endArrowhead":"arrow"},
239
+ {"type":"ellipse","id":"sun","x":30,"y":110,"width":50,"height":50,"backgroundColor":"#fff3bf","fillStyle":"solid","strokeColor":"#f59e0b","strokeWidth":2},
240
+ {"type":"arrow","id":"r1","x":55,"y":108,"width":0,"height":-14,"points":[[0,0],[0,-14]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
241
+ {"type":"arrow","id":"r2","x":55,"y":162,"width":0,"height":14,"points":[[0,0],[0,14]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
242
+ {"type":"arrow","id":"r3","x":28,"y":135,"width":-14,"height":0,"points":[[0,0],[-14,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
243
+ {"type":"arrow","id":"r4","x":82,"y":135,"width":14,"height":0,"points":[[0,0],[14,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
244
+ {"type":"arrow","id":"r5","x":73,"y":117,"width":10,"height":-10,"points":[[0,0],[10,-10]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
245
+ {"type":"arrow","id":"r6","x":37,"y":117,"width":-10,"height":-10,"points":[[0,0],[-10,-10]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
246
+ {"type":"arrow","id":"r7","x":73,"y":153,"width":10,"height":10,"points":[[0,0],[10,10]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null},
247
+ {"type":"arrow","id":"r8","x":37,"y":153,"width":-10,"height":10,"points":[[0,0],[-10,10]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":null,"startArrowhead":null}
248
+ ]
249
+ \`\`\`
250
+
251
+ Common mistakes to avoid:
252
+ - **Camera size must match content with padding** \u2014 if your content is 500px tall, use 800x600 camera, not 500px. No padding = truncated edges
253
+ - **Center titles relative to the diagram below** \u2014 estimate the diagram's total width and center the title text over it, not over the canvas
254
+ - **Arrow labels need space** \u2014 long labels like "ATP + NADPH" overflow short arrows. Keep labels short or make arrows wider
255
+ - **Elements overlap when y-coordinates are close** \u2014 always check that text, boxes, and labels don't stack on top of each other (e.g., an output box overlapping a zone label)
256
+ - **Draw art/illustrations LAST** \u2014 cute decorations (sun, stars, icons) should appear as the final drawing step so they don't distract from the main content being built
257
+
258
+ ## Sequence flow Diagram Example
259
+
260
+ Example prompt: "show a sequence diagram explaining MCP Apps"
261
+
262
+ This demonstrates a UML-style sequence diagram with 4 actors (User, Agent, App iframe, MCP Server), dashed lifelines, and labeled arrows showing the full MCP Apps request/response flow. Camera pans progressively across the diagram:
263
+
264
+ - **Camera 1** (600x450): Title "MCP Apps \u2014 Sequence Flow"
265
+ - **Cameras 2\u20135** (400x300 each): Zoom into each actor column right-to-left \u2014 draw header box + dashed lifeline for Server, App, Agent, User. Right-to-left so the camera snakes smoothly: pan left across actors, then pan right following the first message arrows
266
+ - **Camera 6** (400x300): Zoom into User \u2014 draw stick figure (head + body)
267
+ - **Camera 7** (600x450): Zoom out \u2014 draw first message arrows: user prompt \u2192 agent, agent tools/call \u2192 server, tool result back, result forwarded to app iframe
268
+ - **Camera 8** (600x450): Pan down \u2014 draw user interaction with app, app requesting tools/call back to agent
269
+ - **Camera 9** (600x450): Pan further down \u2014 agent forwards to server, fresh data flows back through the chain, context update from app to agent
270
+ - **Camera 10** (800x600): Final zoom-out showing the complete sequence
271
+
272
+ \`\`\`json
273
+ [
274
+ {"type":"cameraUpdate","width":600,"height":450,"x":80,"y":-10},
275
+ {"type":"text","id":"title","x":200,"y":15,"text":"MCP Apps \u2014 Sequence Flow","fontSize":24,"strokeColor":"#1e1e1e"},
276
+
277
+ {"type":"cameraUpdate","width":400,"height":300,"x":450,"y":-5},
278
+ {"type":"rectangle","id":"sHead","x":600,"y":60,"width":130,"height":40,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":2,"label":{"text":"MCP Server","fontSize":16}},
279
+ {"type":"arrow","id":"sLine","x":665,"y":100,"width":0,"height":490,"points":[[0,0],[0,490]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
280
+
281
+ {"type":"cameraUpdate","width":400,"height":300,"x":250,"y":-5},
282
+ {"type":"rectangle","id":"appHead","x":400,"y":60,"width":130,"height":40,"backgroundColor":"#b2f2bb","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","strokeWidth":2,"label":{"text":"App iframe","fontSize":16}},
283
+ {"type":"arrow","id":"appLine","x":465,"y":100,"width":0,"height":490,"points":[[0,0],[0,490]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
284
+
285
+ {"type":"cameraUpdate","width":400,"height":300,"x":80,"y":-5},
286
+ {"type":"rectangle","id":"aHead","x":230,"y":60,"width":100,"height":40,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#8b5cf6","strokeWidth":2,"label":{"text":"Agent","fontSize":16}},
287
+ {"type":"arrow","id":"aLine","x":280,"y":100,"width":0,"height":490,"points":[[0,0],[0,490]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
288
+
289
+ {"type":"cameraUpdate","width":400,"height":300,"x":-10,"y":-5},
290
+ {"type":"rectangle","id":"uHead","x":60,"y":60,"width":100,"height":40,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2,"label":{"text":"User","fontSize":16}},
291
+ {"type":"arrow","id":"uLine","x":110,"y":100,"width":0,"height":490,"points":[[0,0],[0,490]],"strokeColor":"#b0b0b0","strokeWidth":1,"strokeStyle":"dashed","endArrowhead":null},
292
+
293
+ {"type":"cameraUpdate","width":400,"height":300,"x":-40,"y":50},
294
+ {"type":"ellipse","id":"uh","x":58,"y":110,"width":20,"height":20,"backgroundColor":"#a5d8ff","fillStyle":"solid","strokeColor":"#4a9eed","strokeWidth":2},
295
+ {"type":"rectangle","id":"ub","x":57,"y":132,"width":22,"height":26,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#4a9eed","strokeWidth":2},
296
+
297
+ {"type":"cameraUpdate","width":600,"height":450,"x":-20,"y":-30},
298
+ {"type":"arrow","id":"m1","x":110,"y":135,"width":170,"height":0,"points":[[0,0],[170,0]],"strokeColor":"#1e1e1e","strokeWidth":2,"endArrowhead":"arrow","label":{"text":"display a chart","fontSize":14}},
299
+ {"type":"rectangle","id":"note1","x":130,"y":162,"width":310,"height":26,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#f59e0b","strokeWidth":1,"opacity":50,"label":{"text":"Interactive app rendered in chat","fontSize":14}},
300
+
301
+ {"type":"cameraUpdate","width":600,"height":450,"x":170,"y":25},
302
+ {"type":"arrow","id":"m2","x":280,"y":210,"width":385,"height":0,"points":[[0,0],[385,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","label":{"text":"tools/call","fontSize":16}},
303
+ {"type":"arrow","id":"m3","x":665,"y":250,"width":-385,"height":0,"points":[[0,0],[-385,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","label":{"text":"tool input/result","fontSize":16}},
304
+ {"type":"arrow","id":"m4","x":280,"y":290,"width":185,"height":0,"points":[[0,0],[185,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","label":{"text":"result \u2192 app","fontSize":16}},
305
+
306
+ {"type":"cameraUpdate","width":600,"height":450,"x":-10,"y":135},
307
+ {"type":"arrow","id":"m5","x":110,"y":340,"width":355,"height":0,"points":[[0,0],[355,0]],"strokeColor":"#4a9eed","strokeWidth":2,"endArrowhead":"arrow","label":{"text":"user interacts","fontSize":16}},
308
+ {"type":"arrow","id":"m6","x":465,"y":380,"width":-185,"height":0,"points":[[0,0],[-185,0]],"strokeColor":"#22c55e","strokeWidth":2,"endArrowhead":"arrow","label":{"text":"tools/call request","fontSize":16}},
309
+
310
+ {"type":"cameraUpdate","width":600,"height":450,"x":170,"y":235},
311
+ {"type":"arrow","id":"m7","x":280,"y":420,"width":385,"height":0,"points":[[0,0],[385,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","label":{"text":"tools/call (forwarded)","fontSize":16}},
312
+ {"type":"arrow","id":"m8","x":665,"y":460,"width":-385,"height":0,"points":[[0,0],[-385,0]],"strokeColor":"#f59e0b","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","label":{"text":"fresh data","fontSize":16}},
313
+ {"type":"arrow","id":"m9","x":280,"y":500,"width":185,"height":0,"points":[[0,0],[185,0]],"strokeColor":"#8b5cf6","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","label":{"text":"fresh data","fontSize":16}},
314
+
315
+ {"type":"cameraUpdate","width":600,"height":450,"x":50,"y":327},
316
+ {"type":"rectangle","id":"note2","x":130,"y":522,"width":310,"height":26,"backgroundColor":"#d3f9d8","fillStyle":"solid","roundness":{"type":3},"strokeColor":"#22c55e","strokeWidth":1,"opacity":50,"label":{"text":"App updates with new data","fontSize":14}},
317
+ {"type":"arrow","id":"m10","x":465,"y":570,"width":-185,"height":0,"points":[[0,0],[-185,0]],"strokeColor":"#22c55e","strokeWidth":2,"endArrowhead":"arrow","strokeStyle":"dashed","label":{"text":"context update","fontSize":16}},
318
+
319
+ {"type":"cameraUpdate","width":800,"height":600,"x":-5,"y":2}
320
+ ]
321
+ \`\`\`
322
+
323
+ ## Checkpoints (restoring previous state)
324
+
325
+ Every create_view call returns a \`checkpointId\` in its response. To continue from a previous diagram state, start your elements array with a restoreCheckpoint element:
326
+
327
+ \`[{"type":"restoreCheckpoint","id":"<checkpointId>"}, ...additional new elements...]\`
328
+
329
+ The saved state (including any user edits made in fullscreen) is loaded from the client, and your new elements are appended on top. This saves tokens \u2014 you don't need to re-send the entire diagram.
330
+
331
+ ## Deleting Elements
332
+
333
+ Remove elements by id using the \`delete\` pseudo-element:
334
+
335
+ \`{"type":"delete","ids":"b2,a1,t3"}\`
336
+
337
+ Works in two modes:
338
+ - **With restoreCheckpoint**: restore a saved state, then surgically remove specific elements before adding new ones
339
+ - **Inline (animation mode)**: draw elements, then delete and replace them later in the same array to create transformation effects
340
+
341
+ Place delete entries AFTER the elements you want to remove. The final render filters them out.
342
+
343
+ **IMPORTANT**: Every element id must be unique. Never reuse an id after deleting it \u2014 always assign a new id to replacement elements.
344
+
345
+ ## Animation Mode \u2014 Transform in Place
346
+
347
+ Instead of building left-to-right and panning away, you can animate by DELETING elements and replacing them at the same position. Combined with slight camera moves, this creates smooth visual transformations during streaming.
348
+
349
+ Pattern:
350
+ 1. Draw initial elements
351
+ 2. cameraUpdate (shift/zoom slightly)
352
+ 3. \`{"type":"delete","ids":"old1,old2"}\`
353
+ 4. Draw replacements at same coordinates (different color/content)
354
+ 5. Repeat
355
+
356
+ Example prompt: "Pixel snake eats apple"
357
+
358
+ Snake moves right by adding a head segment and deleting the tail. On eating the apple, tail is NOT deleted (snake grows). Camera nudges between frames add subtle motion.
359
+
360
+ \`\`\`json
361
+ [
362
+ {"type":"cameraUpdate","width":400,"height":300,"x":0,"y":0},
363
+ {"type":"ellipse","id":"ap","x":260,"y":78,"width":20,"height":20,"backgroundColor":"#ef4444","fillStyle":"solid","strokeColor":"#ef4444"},
364
+ {"type":"rectangle","id":"s0","x":60,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
365
+ {"type":"rectangle","id":"s1","x":88,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
366
+ {"type":"rectangle","id":"s2","x":116,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
367
+ {"type":"rectangle","id":"s3","x":144,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
368
+ {"type":"cameraUpdate","width":400,"height":300,"x":1,"y":0},
369
+ {"type":"rectangle","id":"s4","x":172,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
370
+ {"type":"delete","ids":"s0"},
371
+ {"type":"cameraUpdate","width":400,"height":300,"x":0,"y":1},
372
+ {"type":"rectangle","id":"s5","x":200,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
373
+ {"type":"delete","ids":"s1"},
374
+ {"type":"cameraUpdate","width":400,"height":300,"x":1,"y":0},
375
+ {"type":"rectangle","id":"s6","x":228,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
376
+ {"type":"delete","ids":"s2"},
377
+ {"type":"cameraUpdate","width":400,"height":300,"x":0,"y":0},
378
+ {"type":"rectangle","id":"s7","x":256,"y":130,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
379
+ {"type":"delete","ids":"s3"},
380
+ {"type":"cameraUpdate","width":400,"height":300,"x":1,"y":1},
381
+ {"type":"rectangle","id":"s8","x":256,"y":102,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
382
+ {"type":"delete","ids":"s4"},
383
+ {"type":"cameraUpdate","width":400,"height":300,"x":0,"y":0},
384
+ {"type":"rectangle","id":"s9","x":256,"y":74,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
385
+ {"type":"delete","ids":"ap"},
386
+ {"type":"cameraUpdate","width":400,"height":300,"x":1,"y":0},
387
+ {"type":"rectangle","id":"s10","x":256,"y":46,"width":28,"height":28,"backgroundColor":"#22c55e","fillStyle":"solid","strokeColor":"#15803d","strokeWidth":1},
388
+ {"type":"delete","ids":"s5"}
389
+ ]
390
+ \`\`\`
391
+
392
+ Key techniques:
393
+ - Add head + delete tail each frame = snake movement illusion
394
+ - On eat: delete apple instead of tail = snake grows by one
395
+ - Post-eat frame resumes normal add-head/delete-tail, proving the snake is now longer
396
+ - Camera nudges (0,0 \u2192 1,0 \u2192 0,1 \u2192 ...) add subtle motion between frames
397
+ - Always use NEW ids for added segments (s0\u2192s4\u2192s5\u2192...); never reuse deleted ids
398
+
399
+ ## Dark Mode
400
+
401
+ If the user asks for a dark theme/mode diagram, use a massive dark background rectangle as the FIRST element (before cameraUpdate). Make it 10x the camera size so it covers the entire viewport even when panning:
402
+
403
+ \`{"type":"rectangle","id":"darkbg","x":-4000,"y":-3000,"width":10000,"height":7500,"backgroundColor":"#1e1e2e","fillStyle":"solid","strokeColor":"transparent","strokeWidth":0}\`
404
+
405
+ Then use these colors on the dark background:
406
+
407
+ **Text colors (on dark):**
408
+ | Color | Hex | Use |
409
+ |-------|-----|-----|
410
+ | White | \`#e5e5e5\` | Primary text, titles |
411
+ | Muted | \`#a0a0a0\` | Secondary text, annotations |
412
+ | NEVER | \`#555\` or darker | Invisible on dark bg! |
413
+
414
+ **Shape fills (on dark):**
415
+ | Color | Hex | Good For |
416
+ |-------|-----|----------|
417
+ | Dark Blue | \`#1e3a5f\` | Primary nodes |
418
+ | Dark Green | \`#1a4d2e\` | Success, output |
419
+ | Dark Purple | \`#2d1b69\` | Processing, special |
420
+ | Dark Orange | \`#5c3d1a\` | Warning, pending |
421
+ | Dark Red | \`#5c1a1a\` | Error, critical |
422
+ | Dark Teal | \`#1a4d4d\` | Storage, data |
423
+
424
+ **Stroke/arrow colors (on dark):**
425
+ Use the Primary Colors from above \u2014 they're bright enough on dark backgrounds. For shape borders, use slightly lighter variants or \`#555555\` for subtle outlines.
426
+
427
+ ## Tips
428
+ - Do NOT call read_me again \u2014 you already have everything you need
429
+ - Use the color palette consistently
430
+ - **Text contrast is CRITICAL** \u2014 never use light gray (#b0b0b0, #999) on white backgrounds. Minimum text color on white: #757575. For colored text on light fills, use dark variants (#15803d not #22c55e, #2563eb not #4a9eed). White text needs dark backgrounds (#9a5030 not #c4795b)
431
+ - Do NOT use emoji in text \u2014 they don't render in Excalidraw's font
432
+ - cameraUpdate is MAGICAL and users love it! please use it a lot to guide the user's attention as you draw. It makes a huge difference in readability and engagement.
433
+ `;
434
+ async function resolveElements(parsed, store) {
435
+ const restoreEl = parsed.find((el) => el.type === "restoreCheckpoint");
436
+ let resolvedElements;
437
+ if (restoreEl?.id) {
438
+ const base = await store.load(restoreEl.id);
439
+ if (!base) {
440
+ return {
441
+ ok: false,
442
+ error: `Checkpoint "${restoreEl.id}" not found \u2014 it may have expired or never existed. Please recreate the diagram from scratch.`
443
+ };
444
+ }
445
+ const deleteIds = /* @__PURE__ */ new Set();
446
+ for (const el of parsed) {
447
+ if (el.type === "delete") {
448
+ for (const id of String(el.ids ?? el.id).split(",")) deleteIds.add(id.trim());
449
+ }
450
+ }
451
+ const baseFiltered = base.elements.filter(
452
+ (el) => !deleteIds.has(el.id) && !deleteIds.has(el.containerId)
453
+ );
454
+ const newEls = parsed.filter(
455
+ (el) => el.type !== "restoreCheckpoint" && el.type !== "delete"
456
+ );
457
+ resolvedElements = [...baseFiltered, ...newEls];
458
+ } else {
459
+ resolvedElements = parsed.filter((el) => el.type !== "delete");
460
+ }
461
+ const cameras = parsed.filter((el) => el.type === "cameraUpdate");
462
+ const badRatio = cameras.find((c) => {
463
+ if (!c.width || !c.height) return false;
464
+ const ratio = c.width / c.height;
465
+ return Math.abs(ratio - 4 / 3) > 0.15;
466
+ });
467
+ const ratioHint = badRatio ? `
468
+ Tip: your cameraUpdate used ${badRatio.width}x${badRatio.height} \u2014 try to stick with 4:3 aspect ratio (e.g. 400x300, 800x600) in future.` : "";
469
+ return { ok: true, resolvedElements, ratioHint };
470
+ }
471
+ function generateCheckpointId() {
472
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 18);
473
+ }
474
+
475
+ // src/server.ts
476
+ var __filename = fileURLToPath(import.meta.url);
477
+ var __dirname = path.dirname(__filename);
478
+ var DIST_DIR = __filename.endsWith(".ts") ? path.join(__dirname, "..", "dist") : __dirname;
479
+ function registerTools(server, distDir, store) {
480
+ const resourceUri = "ui://excalidraw/mcp-app.html";
481
+ server.registerTool(
482
+ "read_me",
483
+ {
484
+ description: "Returns the Excalidraw element format reference with color palettes, examples, and tips. Call this BEFORE using create_view for the first time.",
485
+ annotations: { readOnlyHint: true }
486
+ },
487
+ async () => {
488
+ return { content: [{ type: "text", text: RECALL_CHEAT_SHEET }] };
489
+ }
490
+ );
491
+ registerAppTool(
492
+ server,
493
+ "create_view",
494
+ {
495
+ title: "Draw Diagram",
496
+ description: `Renders a hand-drawn diagram using Excalidraw elements.
497
+ Elements stream in one by one with draw-on animations.
498
+ Call read_me first to learn the element format.`,
499
+ inputSchema: z.object({
500
+ elements: z.string().describe(
501
+ "JSON array string of Excalidraw elements. Must be valid JSON \u2014 no comments, no trailing commas. Keep compact. Call read_me first for format reference."
502
+ )
503
+ }),
504
+ annotations: { readOnlyHint: true },
505
+ _meta: { ui: { resourceUri } }
506
+ },
507
+ async ({ elements }) => {
508
+ if (elements.length > MAX_INPUT_BYTES) {
509
+ return {
510
+ content: [{ type: "text", text: `Elements input exceeds ${MAX_INPUT_BYTES} byte limit. Reduce the number of elements or use checkpoints to build incrementally.` }],
511
+ isError: true
512
+ };
513
+ }
514
+ let parsed;
515
+ try {
516
+ parsed = JSON.parse(elements);
517
+ } catch (e) {
518
+ return {
519
+ content: [{ type: "text", text: `Invalid JSON in elements: ${e.message}. Ensure no comments, no trailing commas, and proper quoting.` }],
520
+ isError: true
521
+ };
522
+ }
523
+ const result = await resolveElements(parsed, store);
524
+ if (!result.ok) {
525
+ return { content: [{ type: "text", text: result.error }], isError: true };
526
+ }
527
+ const { resolvedElements, ratioHint } = result;
528
+ const checkpointId = generateCheckpointId();
529
+ await store.save(checkpointId, { elements: resolvedElements });
530
+ return {
531
+ content: [{ type: "text", text: `Diagram displayed! Checkpoint id: "${checkpointId}".
532
+ If user asks to create a new diagram - simply create a new one from scratch.
533
+ However, if the user wants to edit something on this diagram "${checkpointId}", take these steps:
534
+ 1) read widget context (using read_widget_context tool) to check if user made any manual edits first
535
+ 2) decide whether you want to make new diagram from scratch OR - use this one as starting checkpoint:
536
+ simply start from the first element [{"type":"restoreCheckpoint","id":"${checkpointId}"}, ...your new elements...]
537
+ this will use same diagram state as the user currently sees, including any manual edits they made in fullscreen, allowing you to add elements on top.
538
+ To remove elements, use: {"type":"delete","ids":"<id1>,<id2>"}${ratioHint}` }],
539
+ structuredContent: { checkpointId }
540
+ };
541
+ }
542
+ );
543
+ registerAppTool(
544
+ server,
545
+ "export_to_excalidraw",
546
+ {
547
+ description: "Upload diagram to excalidraw.com and return shareable URL.",
548
+ inputSchema: { json: z.string().describe("Serialized Excalidraw JSON") },
549
+ _meta: { ui: { visibility: ["app"] } }
550
+ },
551
+ async ({ json }) => {
552
+ if (json.length > MAX_INPUT_BYTES) {
553
+ return {
554
+ content: [{ type: "text", text: `Export data exceeds ${MAX_INPUT_BYTES} byte limit.` }],
555
+ isError: true
556
+ };
557
+ }
558
+ try {
559
+ const remappedJson = json;
560
+ const concatBuffers = (...bufs) => {
561
+ let total = 4;
562
+ for (const b of bufs) total += 4 + b.length;
563
+ const out = new Uint8Array(total);
564
+ const dv = new DataView(out.buffer);
565
+ dv.setUint32(0, 1);
566
+ let off = 4;
567
+ for (const b of bufs) {
568
+ dv.setUint32(off, b.length);
569
+ off += 4;
570
+ out.set(b, off);
571
+ off += b.length;
572
+ }
573
+ return out;
574
+ };
575
+ const te = new TextEncoder();
576
+ const fileMetadata = te.encode(JSON.stringify({}));
577
+ const dataBytes = te.encode(remappedJson);
578
+ const innerPayload = concatBuffers(fileMetadata, dataBytes);
579
+ const compressed = deflateSync(Buffer.from(innerPayload));
580
+ const cryptoKey = await globalThis.crypto.subtle.generateKey(
581
+ { name: "AES-GCM", length: 128 },
582
+ true,
583
+ ["encrypt"]
584
+ );
585
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
586
+ const encrypted = await globalThis.crypto.subtle.encrypt(
587
+ { name: "AES-GCM", iv },
588
+ cryptoKey,
589
+ compressed
590
+ );
591
+ const encodingMeta = te.encode(JSON.stringify({
592
+ version: 2,
593
+ compression: "pako@1",
594
+ encryption: "AES-GCM"
595
+ }));
596
+ const payload = Buffer.from(concatBuffers(encodingMeta, iv, new Uint8Array(encrypted)));
597
+ const res = await fetch("https://json.excalidraw.com/api/v2/post/", {
598
+ method: "POST",
599
+ body: payload
600
+ });
601
+ if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
602
+ const { id } = await res.json();
603
+ const jwk = await globalThis.crypto.subtle.exportKey("jwk", cryptoKey);
604
+ const url = `https://excalidraw.com/#json=${id},${jwk.k}`;
605
+ return { content: [{ type: "text", text: url }] };
606
+ } catch (err) {
607
+ return {
608
+ content: [{ type: "text", text: `Export failed: ${err.message}` }],
609
+ isError: true
610
+ };
611
+ }
612
+ }
613
+ );
614
+ registerAppTool(
615
+ server,
616
+ "save_checkpoint",
617
+ {
618
+ description: "Update checkpoint with user-edited state.",
619
+ inputSchema: { id: z.string(), data: z.string() },
620
+ _meta: { ui: { visibility: ["app"] } }
621
+ },
622
+ async ({ id, data }) => {
623
+ if (data.length > MAX_INPUT_BYTES) {
624
+ return {
625
+ content: [{ type: "text", text: `Checkpoint data exceeds ${MAX_INPUT_BYTES} byte limit.` }],
626
+ isError: true
627
+ };
628
+ }
629
+ try {
630
+ await store.save(id, JSON.parse(data));
631
+ return { content: [{ type: "text", text: "ok" }] };
632
+ } catch (err) {
633
+ return { content: [{ type: "text", text: `save failed: ${err.message}` }], isError: true };
634
+ }
635
+ }
636
+ );
637
+ registerAppTool(
638
+ server,
639
+ "read_checkpoint",
640
+ {
641
+ description: "Read checkpoint state for restore.",
642
+ inputSchema: { id: z.string() },
643
+ _meta: { ui: { visibility: ["app"] } }
644
+ },
645
+ async ({ id }) => {
646
+ try {
647
+ const data = await store.load(id);
648
+ if (!data) return { content: [{ type: "text", text: "" }] };
649
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
650
+ } catch (err) {
651
+ return { content: [{ type: "text", text: `read failed: ${err.message}` }], isError: true };
652
+ }
653
+ }
654
+ );
655
+ const cspMeta = {
656
+ ui: {
657
+ csp: {
658
+ resourceDomains: ["https://esm.sh"],
659
+ connectDomains: ["https://esm.sh"]
660
+ }
661
+ }
662
+ };
663
+ registerAppResource(
664
+ server,
665
+ resourceUri,
666
+ resourceUri,
667
+ { mimeType: RESOURCE_MIME_TYPE },
668
+ async () => {
669
+ const html = await fs.readFile(path.join(distDir, "mcp-app.html"), "utf-8");
670
+ return {
671
+ contents: [{
672
+ uri: resourceUri,
673
+ mimeType: RESOURCE_MIME_TYPE,
674
+ text: html,
675
+ _meta: {
676
+ ui: {
677
+ ...cspMeta.ui,
678
+ prefersBorder: true,
679
+ permissions: { clipboardWrite: {} }
680
+ }
681
+ }
682
+ }]
683
+ };
684
+ }
685
+ );
686
+ }
687
+ function createServer(store) {
688
+ const server = new McpServer({
689
+ name: "Excalidraw",
690
+ version: "1.0.0"
691
+ });
692
+ registerTools(server, DIST_DIR, store);
693
+ return server;
694
+ }
695
+ export {
696
+ createServer,
697
+ registerTools
698
+ };