Skip to content

Instantly share code, notes, and snippets.

@ucalyptus
Last active June 11, 2026 21:36
Show Gist options
  • Select an option

  • Save ucalyptus/4fdbb778674216702397b2c23468b415 to your computer and use it in GitHub Desktop.

Select an option

Save ucalyptus/4fdbb778674216702397b2c23468b415 to your computer and use it in GitHub Desktop.
Excalidraw drawing skill for coding agents

Excalidraw Drawing Skill

Use this skill whenever the user asks you to draw, diagram, visualise, sketch, or chart something in Excalidraw — whether they name the URL explicitly or just say "draw me a flowchart of X".


1. Trigger recognition

Fire this skill when the user's message matches any of these patterns:

Signal Example
Explicit URL "draw X in https://excalidraw.ucalyptus.me/"
Named tool "use excalidraw to diagram…"
Draw/sketch verb "draw a flowchart of…", "sketch the architecture for…"
Diagram noun "make a sequence diagram", "create a system diagram"
Visualise intent "show me how X works visually", "map out the relationships between…"

Do NOT fire for: ASCII art requests, Mermaid/PlantUML requests (unless the user specifically wants Excalidraw), chart data requests (use a charting tool instead).


2. MCP server

Endpoint: https://excalidraw.ucalyptus.me/mcp
Transport: Streamable HTTP (JSON-RPC 2.0, POST)
Auth: Authorization: Bearer <token>

The token is a static secret — it does not expire. It lives in Cloudflare Worker secrets (source of truth) and must be copied to each device once. To rotate it: echo "new-secret" | npx wrangler secret put MCP_SECRET (no redeploy needed).

New device setup (one-time)

Retrieve the token from your password manager, then run:

mkdir -p ~/.claude
cat > ~/.claude/mcp.json << 'EOF'
{
  "mcpServers": {
    "excalidraw": {
      "type": "http",
      "url": "https://excalidraw.ucalyptus.me/mcp",
      "headers": {
        "Authorization": "Bearer <paste-token-here>"
      }
    }
  }
}
EOF

After that, restart Claude Code. The tools will be available as mcp__excalidraw__* in every session on that device. You never need to touch this file again unless you rotate the token.

Verify the connection

curl -s -X POST https://excalidraw.ucalyptus.me/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <your-token>" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 -m json.tool

A 200 response listing the four tools confirms you're connected.

The server is registered in ~/.claude/mcp.json. In a Claude Code session the tools are available directly as mcp__excalidraw__*. In any other context call the endpoint via HTTP.

Available tools

Tool Purpose
read_me Returns the element format cheat sheet. Call once per session before drawing.
create_drawing Creates a blank drawing. Returns {id, url}.
get_drawing Fetches current elements for an existing drawing by id.
update_drawing Full-replaces the scene. Args: id, elements[], optional appState.

3. Exact workflow

Follow these steps in order every time. Do not skip steps.

Step 1  Detect intent → this skill applies
Step 2  Call read_me (skip if already called this session)
Step 3  Does the user give you an existing drawing URL/id?
          YES → call get_drawing to load current elements, then plan additions/changes
          NO  → call create_drawing to get a fresh id
Step 4  Plan the layout on paper (mentally) before writing JSON
Step 5  Call update_drawing with the full elements array
Step 6  Return the drawing URL to the user

Always return the URL. The user needs it to open the drawing.


4. Element format reference

Every element is a JSON object in the elements array passed to update_drawing.

4.1 Fields shared by all elements

id          string   Unique within the drawing. Use short descriptive ids: "user-svc", "db", "a1".
x, y        number   Top-left corner position in canvas units.
strokeColor string   Hex colour for the border/line. Default: "#1e1e1e"
opacity     number   0–100. Default: 100

4.2 Rectangle / Ellipse / Diamond

{
  "type": "rectangle",
  "id": "db",
  "x": 400, "y": 200,
  "width": 160, "height": 60,
  "backgroundColor": "#a5d8ff",
  "fillStyle": "solid",
  "strokeColor": "#1e1e1e",
  "strokeWidth": 2,
  "roughness": 1,
  "roundness": {"type": 3},
  "label": {"text": "Postgres", "fontSize": 16}
}
Field Values Notes
type "rectangle" | "ellipse" | "diamond"
width, height number See sizing guide §5.1
backgroundColor hex Omit for transparent
fillStyle "solid" | "hachure" | "cross-hatch" "solid" for clean look
strokeWidth 1 | 2 | 4 2 is default
roughness 0 | 1 | 2 0 = clean, 1 = sketchy, 2 = very rough
roundness {"type": 3} Rounded corners; omit for sharp
label.text string Text rendered inside the shape
label.fontSize number 14–20 for node labels

4.3 Text (standalone)

{"type": "text", "id": "t1", "x": 100, "y": 50, "text": "My heading", "fontSize": 24, "strokeColor": "#1e1e1e"}

Use standalone text for titles, section labels, and annotations. For node labels use the label field on shapes instead.

4.4 Arrow / Line

{
  "type": "arrow",
  "id": "a-user-api",
  "x": 220, "y": 190,
  "width": 80, "height": 0,
  "points": [[0, 0], [80, 0]],
  "strokeColor": "#1e1e1e",
  "strokeWidth": 2,
  "endArrowhead": "arrow",
  "startBinding": {"elementId": "user", "fixedPoint": [1, 0.5]},
  "endBinding":   {"elementId": "api",  "fixedPoint": [0, 0.5]}
}

Critical rules for arrows:

  1. x, y = the START point of the arrow in canvas coordinates.
  2. points = relative offsets from x, y. First point is always [0, 0]. Last point is [width, height] — so a rightward arrow of length 80 has points: [[0,0],[80,0]].
  3. width and height = the bounding-box size (can be negative for left/up arrows).
  4. endArrowhead: "arrow" (filled triangle) | "bar" | "dot" | null (no head, gives a line).
  5. startArrowhead: same options; omit for single-headed arrows.

Binding arrows to shapes (makes them snap to the node):

"startBinding": {"elementId": "<shape-id>", "fixedPoint": [<rx>, <ry>]}
"endBinding":   {"elementId": "<shape-id>", "fixedPoint": [<rx>, <ry>]}

fixedPoint is a relative position on the shape's bounding box:

  • [0, 0.5] = left midpoint (use as endBinding when arrow enters from the left)
  • [1, 0.5] = right midpoint (use as startBinding when arrow leaves to the right)
  • [0.5, 0] = top midpoint
  • [0.5, 1] = bottom midpoint

The arrow's x, y must be set to the actual canvas coordinates of the binding point, not the center of the source shape. Calculate: shape.x + shape.width * fixedPoint[0] for x, etc.

Diagonal arrows: set both width and height to non-zero and adjust points accordingly.


5. Layout guide

5.1 Standard sizes

Element Width Height
Service / component node 160 60
Database / storage node 140 60
User / actor (ellipse) 80 80
Decision diamond 120 80
Section container (rectangle, no fill) varies varies
Title text fontSize 24
Label text fontSize 14–16

5.2 Spacing rules

  • Horizontal gap between nodes: 80–120px (arrow length = gap)
  • Vertical gap between rows: 80–100px
  • Start x: 60–100 (leave margin from canvas edge)
  • Start y: 60–100 (leave room for a title at y=20)
  • Use multiples of 20 for all positions — stays on Excalidraw's grid

5.3 Diagram-specific layouts

Left-to-right flow (pipelines, request flows)

Row at y=160: [Node A] --arrow--> [Node B] --arrow--> [Node C]
              x=60               x=300               x=540

Top-to-bottom flow (class hierarchy, org charts)

[Root]          y=80,  x=300 (centred)
   |
[Child A]       y=220, x=100
[Child B]       y=220, x=400

Hub-and-spoke (one node connects to many)

[Hub] at centre (x=340, y=260)
Spokes at: top, bottom, left, right (200px away)

Sequence diagram (manual layout)

Actors as labelled rectangles across the top at y=60, spaced 200px apart
Vertical lifelines as thin lines below each actor
Numbered arrows connecting lifelines

5.4 Colour conventions (use consistently)

Meaning Fill hex Use for
Client / user #a5d8ff (light blue) Browsers, mobile apps, users
Service / API #b2f2bb (light green) Backend services, workers
Storage #fff3bf (light yellow) Databases, caches, queues
External / 3rd party #ffd8a8 (light orange) CDNs, payment providers
Infra / runtime #d0bfff (light purple) K8s, Cloudflare, VMs
Warning / error #ffc9c9 (light red) Error paths, alerts
Neutral container no fill, strokeColor: "#ced4da" Grouping boxes

Stroke colour is always "#1e1e1e" unless you're colour-coding arrows.


6. Complete worked examples

6.1 Simple request/response

[
  {"type":"text","id":"title","x":180,"y":20,"text":"HTTP Request Flow","fontSize":22,"strokeColor":"#1e1e1e"},
  {"type":"rectangle","id":"browser","x":60,"y":140,"width":140,"height":60,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Browser","fontSize":16}},
  {"type":"rectangle","id":"server","x":300,"y":140,"width":140,"height":60,"backgroundColor":"#b2f2bb","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Server","fontSize":16}},
  {"type":"rectangle","id":"db","x":540,"y":140,"width":140,"height":60,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Database","fontSize":16}},
  {"type":"arrow","id":"a1","x":200,"y":163,"width":100,"height":0,"points":[[0,0],[100,0]],"endArrowhead":"arrow","strokeColor":"#1e1e1e","startBinding":{"elementId":"browser","fixedPoint":[1,0.5]},"endBinding":{"elementId":"server","fixedPoint":[0,0.5]}},
  {"type":"arrow","id":"a2","x":440,"y":163,"width":100,"height":0,"points":[[0,0],[100,0]],"endArrowhead":"arrow","strokeColor":"#1e1e1e","startBinding":{"elementId":"server","fixedPoint":[1,0.5]},"endBinding":{"elementId":"db","fixedPoint":[0,0.5]}}
]

6.2 Flowchart with decision

[
  {"type":"text","id":"title","x":220,"y":20,"text":"Login Flow","fontSize":22,"strokeColor":"#1e1e1e"},
  {"type":"rectangle","id":"start","x":260,"y":80,"width":120,"height":50,"backgroundColor":"#b2f2bb","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Start","fontSize":16}},
  {"type":"diamond","id":"check","x":250,"y":200,"width":140,"height":80,"backgroundColor":"#fff3bf","fillStyle":"solid","label":{"text":"Valid?","fontSize":15}},
  {"type":"rectangle","id":"ok","x":440,"y":215,"width":140,"height":50,"backgroundColor":"#b2f2bb","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Dashboard","fontSize":16}},
  {"type":"rectangle","id":"fail","x":60,"y":215,"width":140,"height":50,"backgroundColor":"#ffc9c9","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Show Error","fontSize":16}},
  {"type":"arrow","id":"a1","x":320,"y":130,"width":0,"height":70,"points":[[0,0],[0,70]],"endArrowhead":"arrow","startBinding":{"elementId":"start","fixedPoint":[0.5,1]},"endBinding":{"elementId":"check","fixedPoint":[0.5,0]}},
  {"type":"arrow","id":"a2","x":390,"y":240,"width":50,"height":0,"points":[[0,0],[50,0]],"endArrowhead":"arrow","startBinding":{"elementId":"check","fixedPoint":[1,0.5]},"endBinding":{"elementId":"ok","fixedPoint":[0,0.5]}},
  {"type":"text","id":"yes","x":400,"y":224,"text":"yes","fontSize":13,"strokeColor":"#22c55e"},
  {"type":"arrow","id":"a3","x":250,"y":240,"width":-50,"height":0,"points":[[0,0],[-50,0]],"endArrowhead":"arrow","startBinding":{"elementId":"check","fixedPoint":[0,0.5]},"endBinding":{"elementId":"fail","fixedPoint":[1,0.5]}},
  {"type":"text","id":"no","x":192,"y":224,"text":"no","fontSize":13,"strokeColor":"#ef4444"}
]

6.3 Architecture with grouping container

[
  {"type":"text","id":"title","x":160,"y":20,"text":"Cloudflare Worker Stack","fontSize":22,"strokeColor":"#1e1e1e"},
  {"type":"rectangle","id":"cf-box","x":240,"y":80,"width":420,"height":280,"backgroundColor":"transparent","fillStyle":"solid","strokeColor":"#ced4da","strokeWidth":1,"roundness":{"type":3}},
  {"type":"text","id":"cf-label","x":260,"y":90,"text":"Cloudflare Edge","fontSize":14,"strokeColor":"#868e96"},
  {"type":"rectangle","id":"worker","x":280,"y":120,"width":140,"height":60,"backgroundColor":"#d0bfff","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Worker","fontSize":16}},
  {"type":"rectangle","id":"d1","x":480,"y":120,"width":140,"height":60,"backgroundColor":"#fff3bf","fillStyle":"solid","roundness":{"type":3},"label":{"text":"D1","fontSize":16}},
  {"type":"rectangle","id":"r2","x":480,"y":220,"width":140,"height":60,"backgroundColor":"#ffd8a8","fillStyle":"solid","roundness":{"type":3},"label":{"text":"R2","fontSize":16}},
  {"type":"rectangle","id":"client","x":60,"y":230,"width":140,"height":60,"backgroundColor":"#a5d8ff","fillStyle":"solid","roundness":{"type":3},"label":{"text":"Browser","fontSize":16}},
  {"type":"arrow","id":"a1","x":200,"y":260,"width":80,"height":-110,"points":[[0,0],[80,-110]],"endArrowhead":"arrow","startBinding":{"elementId":"client","fixedPoint":[1,0.5]},"endBinding":{"elementId":"worker","fixedPoint":[0,0.5]}},
  {"type":"arrow","id":"a2","x":420,"y":150,"width":60,"height":0,"points":[[0,0],[60,0]],"endArrowhead":"arrow","startBinding":{"elementId":"worker","fixedPoint":[1,0.5]},"endBinding":{"elementId":"d1","fixedPoint":[0,0.5]}},
  {"type":"arrow","id":"a3","x":420,"y":160,"width":60,"height":90,"points":[[0,0],[60,90]],"endArrowhead":"arrow","startBinding":{"elementId":"worker","fixedPoint":[1,0.7]},"endBinding":{"elementId":"r2","fixedPoint":[0,0.5]}}
]

7. Updating an existing drawing

When the user gives you an existing URL like https://excalidraw.ucalyptus.me/d/abc123:

  1. Extract the id: everything after /d/abc123
  2. Call get_drawing with that id
  3. Parse the returned elements array
  4. Modify or extend the array as needed (add new elements, change properties, remove by omitting)
  5. Call update_drawing with the full new array — it is a full replace, not a patch

Preserve existing element ids when modifying so arrows keep their bindings.


8. Arrow coordinate calculation cheat sheet

Given two shapes A and B, to draw a rightward arrow from A to B:

arrowX     = A.x + A.width          (right edge of A)
arrowY     = A.y + A.height / 2     (vertical midpoint of A)
arrowWidth = B.x - arrowX           (distance to left edge of B)
arrowHeight = 0                      (horizontal arrow)
points     = [[0,0],[arrowWidth,0]]

startBinding.elementId = A.id
startBinding.fixedPoint = [1, 0.5]   (right midpoint of A)
endBinding.elementId   = B.id
endBinding.fixedPoint  = [0, 0.5]    (left midpoint of B)

For a downward arrow from A to B:

arrowX      = A.x + A.width / 2
arrowY      = A.y + A.height
arrowWidth  = 0
arrowHeight = B.y - arrowY
points      = [[0,0],[0,arrowHeight]]

startBinding.fixedPoint = [0.5, 1]   (bottom midpoint of A)
endBinding.fixedPoint   = [0.5, 0]   (top midpoint of B)

9. Common mistakes to avoid

Mistake Fix
Arrow points don't match width/height Last point must equal [width, height]
fixedPoint backwards [1,0.5] = right exit, [0,0.5] = left entry — don't swap
Shapes overlap because of wrong x/y Plan layout on paper first, check spacing with §5.2
Empty label text crashes render Either omit label or ensure label.text is non-empty
Ids with spaces or special chars Use only a-z, 0-9, -, _ in ids
Forgetting to return the URL Always end with the drawing URL
Calling read_me every turn Call it once per session; do not repeat

10. Response format

After a successful draw, your reply should be:

Here's the diagram: https://excalidraw.ucalyptus.me/d/<id>

[Optional: 1–2 sentences describing what you drew and any notable layout decisions.]

If the user asked for changes to an existing drawing:

Updated: https://excalidraw.ucalyptus.me/d/<id>

[What changed.]

Keep it short — the diagram speaks for itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment