Skip to content

Architecture

drawmode exposes three MCP tools: draw (generate/edit diagrams), draw_describe (convert .excalidraw to TypeScript), and draw_info (capabilities reference). The LLM writes TypeScript code against the Diagram SDK:

  1. LLM receives the draw tool with SDK type definitions (~100 lines)
  2. LLM writes code using the Diagram class
  3. executor.ts runs code via new Function("Diagram", code) with a ConfiguredDiagram subclass
  4. SDK runtime validation catches errors at call time — invalid IDs, wrong element types, missing dependencies throw actionable errors the LLM can self-correct on
  5. Zig WASM (Graphviz C statically linked) handles layout positioning and arrow routing via the dot engine
  6. WASM structural validation checks the rendered output (bound text, duplicate IDs, overlapping shapes)
  7. Output: .excalidraw file, excalidraw.com URL, .png file, or .svg file
drawmode/
├── src/ # TypeScript (MCP server + SDK)
│ ├── index.ts # MCP server entry point (stdio + HTTP)
│ ├── sdk.ts # Diagram SDK (addBox, connect, render, etc.)
│ ├── executor.ts # Local executor (new Function + Diagram subclass)
│ ├── layout.ts # Layout bridge (loads Zig WASM with statically-linked Graphviz)
│ ├── upload.ts # Excalidraw.com upload (encrypt + POST)
│ ├── png.ts # Image export (PNG/SVG via puppeteer + Excalidraw CDN)
│ ├── types.ts # Shared types (ColorPreset, ShapeOpts, ConnectOpts, etc.)
│ ├── sdk-types.ts # SDK type definitions string (embedded in tool description)
│ └── widget.html # MCP Apps HTML widget for Claude Desktop/Cowork
├── wasm/ # Zig WASM module
│ ├── src/
│ │ ├── main.zig # WASM exports (alloc, resetHeap, layoutGraph, routeArrows, validate)
│ │ ├── layout.zig # Graphviz layout (C lib statically linked)
│ │ ├── arrows.zig # Arrow routing
│ │ ├── validate.zig # Structural validation
│ │ ├── compress.zig # Zlib compression (RFC 1950/1951)
│ │ ├── font.zig # Font metrics
│ │ └── util.zig # Shared utilities
│ └── build.zig
├── worker/ # Cloudflare Worker entry point (remote MCP)
│ ├── index.ts
│ └── wrangler.toml
├── package.json
└── tsconfig.json

The primary layout engine is Graphviz — the C library is statically linked into the Zig WASM module (drawmode.wasm).

  • dot layout: Layered graph layout (Sugiyama algorithm) with proper crossing minimization
  • splines=ortho: Orthogonal edge routing for vertical layouts (TB/BT); curved splines for horizontal layouts (LR/RL)
  • Cluster subgraphs: Groups rendered as Graphviz clusters for proper containment
  • rank=same: Nodes with same row value share a rank in the layout
  • Coordinate conversion: Graphviz points (Y-up) → Excalidraw pixels (Y-down)

A single Zig WASM binary with Graphviz C statically linked. Handles layout, edge routing, and validation:

WASM exports:

ExportDescription
alloc / deallocBump allocator memory management
resetHeapReset bump allocator between calls
layoutGraphPrimary layout engine (Graphviz Sugiyama with orthogonal edge routing)
routeArrowsArrow endpoint routing
validateStructural validation (bound text, duplicate IDs, overlapping elements)
zlibCompressZlib compression for excalidraw.com upload

Input/output is JSON bytes — the TypeScript layer serializes to JSON, passes to WASM, and reads back results.

Layer 1: SDK runtime validation (at call time)

Every SDK method validates its inputs before modifying state. This catches errors immediately with actionable messages:

  • connect(from, to) — both IDs must be nodes (not groups or frames)
  • addGroup(label, children) / addFrame(name, children) — all children must exist; frames cannot be nested
  • message(from, to) — both IDs must be actors
  • updateNode(id) / updateEdge(from, to) — target must exist
  • removeGroup(id) / removeFrame(id) — target must exist

Layer 2: WASM structural validation (after render)

Post-render checks on the generated Excalidraw JSON:

  • Bound text elements have matching containers
  • No duplicate element IDs
  • Arrow endpoints reference valid elements
  • No overlapping elements

The SDK handles these Excalidraw quirks so the LLM doesn’t have to:

  • Labels need two elements: shape with boundElements + text with containerId
  • The label property does NOT work in raw JSON
  • Elbow arrows need: elbowed: true, roundness: null, roughness: 0
  • Arrow x,y must be on source shape edge, not center
  • Multiple arrows from same edge must be staggered (20%, 35%, 50%, 65%, 80%)

PNG and SVG export uses headless Chrome (puppeteer) to load the Excalidraw library from CDN and call ExcalidrawLib.exportToSvg() — the same rendering pipeline as the Excalidraw web app.

  • PNG: SVG → canvas at 2x resolution → toDataURL("image/png")
  • SVG: Direct XMLSerializer.serializeToString() of the SVG element
  • CDN: Pinned to @excalidraw/excalidraw@0.17.6
  • Optional: puppeteer is in optionalDependencies — PNG/SVG export gracefully fails if not installed

Architecture diagram

PackagePurpose
@modelcontextprotocol/sdkMCP protocol (McpServer, transports)
zodSchema validation for MCP tool parameters
puppeteer (optional)Headless Chrome for PNG/SVG export

No external dependencies. Standard library only.

Terminal window
pnpm install # Install dependencies
pnpm build # Build TS + WASM (Zig failure non-fatal)
pnpm build:wasm # Build WASM module only
pnpm dev # Dev server (HTTP mode)
pnpm test # Run tests
pnpm typecheck # TypeScript type checking
cd wasm && zig build # Build WASM module only
cd wasm && zig build test # Run Zig tests