{"owner":"excalidraw","repo":"excalidraw-mcp","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# Excalidraw MCP App Server\n\nStandalone MCP server that streams Excalidraw diagrams as SVG with hand-drawn animations.\n\n## Architecture\n\n```\nserver.ts          → 2 tools (read_me, create_view) + resource + cheat sheet\nmain.ts            → HTTP (Streamable) + stdio transports\nsrc/mcp-app.tsx    → ExcalidrawAppCore (widget logic) + ExcalidrawApp (useApp wrapper)\nsrc/mcp-entry.tsx  → Production entry point: createRoot + ExcalidrawApp\nsrc/global.css     → Animations (stroke draw-on, fade-in) + auto-resize\nsrc/dev.tsx        → Dev entry point: mock app + sample elements + control panel\nsrc/dev-mock.ts    → Mock MCP App with event simulation (sendToolInput, streamElements, etc.)\nindex-dev.html     → Dev HTML entry (served by vite dev server)\nvite.config.dev.ts → Dev-only vite config (resolves from node_modules, no esm.sh externals)\n```\n\n## Tools\n\n### `read_me` (text tool, no UI)\nReturns a cheat sheet with element format, color palettes, coordinate tips, and examples. The model should call this before `create_view`.\n\n### `create_view` (UI tool)\nTakes `elements` — a JSON string of standard Excalidraw elements. The widget parses partial JSON during streaming and renders via `exportToSvg` + morphdom diffing. No Excalidraw React canvas component — pure SVG rendering.\n\n**Screenshot as model context:** After final render, the SVG is captured as a 512px-max PNG and sent via `app.updateModelContext()` so the model can see the diagram and iterate on user feedback.\n\n## Key Design Decisions\n\n### Standard Excalidraw JSON — no extensions\nThe input is standard Excalidraw element JSON. No `label` on containers, no `start`/`end` on arrows. These are Excalidraw's internal \"skeleton\" API (`convertToExcalidrawElements`) — not the standard format.\n\n**Why:** Standard format means any `.excalidraw` file's elements array works as input.\n\n**Trade-off:** Labels require separate text elements with manually computed centered coordinates. The cheat sheet teaches the formula: `x = shape.x + (shape.width - text.width) / 2`.\n\n### No `convertToExcalidrawElements`\nWe tried Excalidraw's skeleton API. Problems:\n1. Needs font metrics at conversion time (canvas `measureText`)\n2. Non-standard format\n3. Added complexity for marginal benefit\n\n### SVG-only rendering (no Excalidraw React canvas)\nThe widget uses `exportToSvg` for ALL rendering — no `<Excalidraw>` React component.\n\n**Why:**\n- Eliminates blink on final render (no component swap from SVG preview to canvas)\n- Loads Virgil hand-drawn font from the start (no `skipInliningFonts`)\n- morphdom works on SVG DOM — smooth diffing between streaming updates\n\n### Auto-sizing\nThe container has no fixed height. SVG gets `width: 100%` + `height: auto` with the `width` attribute removed. The SVG's `viewBox` preserves aspect ratio, so height scales proportionally to content.\n\n### CSP: `esm.sh` allowed\nExcalidraw loads the Virgil font from `esm.sh` at runtime. The resource's `_meta.ui.csp.resourceDomains` includes `https://esm.sh`.\n\n### `prefersBorder: true`\nSet on the resource content's `_meta.ui` so the host renders a border/background around the widget.\n\n### Fullscreen mode\nSupports `app.requestDisplayMode({ mode: \"fullscreen\" })`. Button appears on hover (top-right), hidden in fullscreen (host provides exit UI). Escape key exits fullscreen.\n\n## Checkpoint System\n\nTwo-tier storage for diagram state persistence:\n\n### Architecture\n1. **Server-side store** (primary): `CheckpointStore` interface with 3 implementations:\n   - `FileCheckpointStore` — local dev, writes JSON to `$TMPDIR/excalidraw-mcp-checkpoints/`\n   - `MemoryCheckpointStore` — Vercel fallback (in-memory Map, lost on cold start)\n   - `RedisCheckpointStore` — Vercel with Upstash KV (persistent, 30-day TTL)\n   - Factory: `createVercelStore()` picks Redis if env vars exist, else Memory\n\n2. **localStorage** (widget-side cache): Fast local cache keyed by `excalidraw:<checkpointId>` for persisting user edits across page reloads within the same session.\n\n### Flow\n- `create_view` resolves `restoreCheckpoint` references server-side, saves fully resolved state, returns `checkpointId`\n- Widget reads checkpoints via `read_checkpoint` server tool (private, app-only visibility)\n- User edits in fullscreen sync back to server via `save_checkpoint` server tool (debounced)\n- `cameraUpdate` elements are stored as part of checkpoint data (not a separate viewport field)\n\n### Key Design Decisions\n- Server resolves checkpoints so the model never needs to re-send full element arrays\n- `containerId` filtering ensures bound text elements are deleted with their containers\n- Camera aspect ratio check nudges model toward 4:3 ratios\n- `checkpointId` uses `crypto.randomUUID()` truncated to 18 chars (collision-resistant, URL-safe)\n\n## Build\n\n```bash\nnpm install\nnpm run build\n```\n\nBuild pipeline: `tsc --noEmit` → `vite build` (singlefile HTML) → `tsc -p tsconfig.server.json` → `bun build` (server + index).\n\n## Running\n\n```bash\n# HTTP (Streamable) — default, stateless per-request\nnpm run serve          # or: bun --watch main.ts\n# Starts on http://localhost:3001/mcp\n\n# stdio — for Claude Desktop\nnode dist/index.js --stdio\n\n# Dev mode (watch + serve) — full MCP flow\nnpm run dev\n\n# Dev mode (standalone UI) — no MCP server needed\nnpm run dev:ui\n# Opens http://localhost:5173/index-dev.html with mock app + sample diagram\n```\n\n## Claude Desktop config\n\n```json\n{\n  \"excalidraw\": {\n    \"command\": \"node\",\n    \"args\": [\"<path>/dist/index.js\", \"--stdio\"]\n  }\n}\n```\n\n## Rendering Pipeline\n\n### Streaming (`ontoolinputpartial`)\n1. `parsePartialElements` tries `JSON.parse`, falls back to closing array after last `}`\n2. `excludeIncompleteLastItem` drops the last element (may be incomplete)\n3. Only re-renders when element **count** changes (not on every partial update)\n4. Seeds are **randomized** per render — hand-drawn style animates naturally\n5. `exportToSvg` generates SVG → **morphdom** diffs against existing DOM\n6. morphdom preserves existing elements (no re-animation), only new elements trigger CSS animations\n\n### Final render (`ontoolinput`)\n1. Parses complete JSON, renders with **original seeds** (stable final look)\n2. Same `exportToSvg` + morphdom path — seamless transition, no blink\n3. Sends PNG screenshot to model context (debounced 1.5s)\n\n### CSS Animations (3 layers)\n- **Shapes** (`g, rect, circle, ellipse, text, image`): opacity fade-in 0.5s\n- **Lines** (`path, line, polyline, polygon`): stroke-dashoffset draw-on effect 0.6s\n- **Existing elements**: smooth `transition` on fill/stroke/opacity changes\n\n### Key Libraries\n- **morphdom**: DOM diffing for SVG — preserves existing nodes, only new nodes get animations\n- **exportToSvg**: Excalidraw's SVG export (with fonts inlined by default)\n\n## Cheat Sheet: Progressive Element Ordering\n\nThe `server.ts` cheat sheet instructs the model to emit elements progressively:\n- BAD: all rectangles → all texts → all arrows (blank boxes stream, then labels appear late)\n- GOOD: background shapes first, then per node: shape → label → arrows → next node\n- This way each node appears complete with its label during streaming\n\n## Debugging\n\n### Dev workflow\n1. Edit source files\n2. `npm run build` (or `npm run dev` for watch mode)\n3. Restart the server process (module cache means hot reload doesn't pick up `server.ts` changes for tool definitions)\n4. In Claude Desktop: restart the MCP server connection\n\n### Widget logging — NEVER use console.log\n\nUse the SDK logger — it routes through the host to the log file:\n\n```typescript\napp.sendLog({ level: \"info\", logger: \"Excalidraw\", data: \"my message\" });\n```\n\n**Log file**: `~/Library/Logs/Claude/claude.ai-web.log`\n\n```bash\n# Fullscreen transition logs (logger: \"FS\")\ngrep \"FS\" ~/Library/Logs/Claude/claude.ai-web.log | tail -40\n\n# General widget logs (logger: \"Excalidraw\")\ngrep \"Excalidraw\" ~/Library/Logs/Claude/claude.ai-web.log | tail -20\n\n# Clear logs before repro for clean output\n> ~/Library/Logs/Claude/claude.ai-web.log\n```\n\n### Widget debugging\n- The widget runs in an iframe\n- Check that `exportToSvg` isn't throwing (catches are silent)\n- morphdom issues: compare old vs new SVG structure in Elements panel\n\n### Common issues\n- **No diagram appears:** Check that `ontoolinputpartial` is firing — the `elements` field might be nested differently (`params.arguments.elements` vs `params.elements`)\n- **All elements re-animate on each update:** morphdom not working — check that SVG structure is similar enough for diffing (different root SVG attributes can cause full replacement)\n- **Font is default (not hand-drawn):** `skipInliningFonts` was set to `true` — must be removed/false\n- **Elements in wrong positions during animation:** Don't use CSS `transform: scale()` on SVG child elements — conflicts with Excalidraw's own transform attributes. Use opacity-only animations.\n\n## Gotchas\n\n- `ExcalidrawElement` type is at `@excalidraw/excalidraw/element/types`, not re-exported from main\n- `ExcalidrawImperativeAPI` type is at `@excalidraw/excalidraw/types`\n- Excalidraw's `containerId` on text elements does NOT auto-position text — that only works via `convertToExcalidrawElements` skeleton API\n- The `.SVGLayer` div is not used for rendering but takes layout space — safe to `display: none`\n- morphdom is essential — without it, replacing innerHTML re-triggers all animations on every update\n- `ReactDOM.render()` per update remounts the tree and kills animations — use `createRoot()` once + `useState` if adding React components\n"},"files":{"CLAUDE.md":"# Excalidraw MCP App Server\n\nStandalone MCP server that streams Excalidraw diagrams as SVG with hand-drawn animations.\n\n## Architecture\n\n```\nserver.ts          → 2 tools (read_me, create_view) + resource + cheat sheet\nmain.ts            → HTTP (Streamable) + stdio transports\nsrc/mcp-app.tsx    → ExcalidrawAppCore (widget logic) + ExcalidrawApp (useApp wrapper)\nsrc/mcp-entry.tsx  → Production entry point: createRoot + ExcalidrawApp\nsrc/global.css     → Animations (stroke draw-on, fade-in) + auto-resize\nsrc/dev.tsx        → Dev entry point: mock app + sample elements + control panel\nsrc/dev-mock.ts    → Mock MCP App with event simulation (sendToolInput, streamElements, etc.)\nindex-dev.html     → Dev HTML entry (served by vite dev server)\nvite.config.dev.ts → Dev-only vite config (resolves from node_modules, no esm.sh externals)\n```\n\n## Tools\n\n### `read_me` (text tool, no UI)\nReturns a cheat sheet with element format, color palettes, coordinate tips, and examples. The model should call this before `create_view`.\n\n### `create_view` (UI tool)\nTakes `elements` — a JSON string of standard Excalidraw elements. The widget parses partial JSON during streaming and renders via `exportToSvg` + morphdom diffing. No Excalidraw React canvas component — pure SVG rendering.\n\n**Screenshot as model context:** After final render, the SVG is captured as a 512px-max PNG and sent via `app.updateModelContext()` so the model can see the diagram and iterate on user feedback.\n\n## Key Design Decisions\n\n### Standard Excalidraw JSON — no extensions\nThe input is standard Excalidraw element JSON. No `label` on containers, no `start`/`end` on arrows. These are Excalidraw's internal \"skeleton\" API (`convertToExcalidrawElements`) — not the standard format.\n\n**Why:** Standard format means any `.excalidraw` file's elements array works as input.\n\n**Trade-off:** Labels require separate text elements with manually computed centered coordinates. The cheat sheet teaches the formula: `x = shape.x + (shape.width - text.width) / 2`.\n\n### No `convertToExcalidrawElements`\nWe tried Excalidraw's skeleton API. Problems:\n1. Needs font metrics at conversion time (canvas `measureText`)\n2. Non-standard format\n3. Added complexity for marginal benefit\n\n### SVG-only rendering (no Excalidraw React canvas)\nThe widget uses `exportToSvg` for ALL rendering — no `<Excalidraw>` React component.\n\n**Why:**\n- Eliminates blink on final render (no component swap from SVG preview to canvas)\n- Loads Virgil hand-drawn font from the start (no `skipInliningFonts`)\n- morphdom works on SVG DOM — smooth diffing between streaming updates\n\n### Auto-sizing\nThe container has no fixed height. SVG gets `width: 100%` + `height: auto` with the `width` attribute removed. The SVG's `viewBox` preserves aspect ratio, so height scales proportionally to content.\n\n### CSP: `esm.sh` allowed\nExcalidraw loads the Virgil font from `esm.sh` at runtime. The resource's `_meta.ui.csp.resourceDomains` includes `https://esm.sh`.\n\n### `prefersBorder: true`\nSet on the resource content's `_meta.ui` so the host renders a border/background around the widget.\n\n### Fullscreen mode\nSupports `app.requestDisplayMode({ mode: \"fullscreen\" })`. Button appears on hover (top-right), hidden in fullscreen (host provides exit UI). Escape key exits fullscreen.\n\n## Checkpoint System\n\nTwo-tier storage for diagram state persistence:\n\n### Architecture\n1. **Server-side store** (primary): `CheckpointStore` interface with 3 implementations:\n   - `FileCheckpointStore` — local dev, writes JSON to `$TMPDIR/excalidraw-mcp-checkpoints/`\n   - `MemoryCheckpointStore` — Vercel fallback (in-memory Map, lost on cold start)\n   - `RedisCheckpointStore` — Vercel with Upstash KV (persistent, 30-day TTL)\n   - Factory: `createVercelStore()` picks Redis if env vars exist, else Memory\n\n2. **localStorage** (widget-side cache): Fast local cache keyed by `excalidraw:<checkpointId>` for persisting user edits across page reloads within the same session.\n\n### Flow\n- `create_view` resolves `restoreCheckpoint` references server-side, saves fully resolved state, returns `checkpointId`\n- Widget reads checkpoints via `read_checkpoint` server tool (private, app-only visibility)\n- User edits in fullscreen sync back to server via `save_checkpoint` server tool (debounced)\n- `cameraUpdate` elements are stored as part of checkpoint data (not a separate viewport field)\n\n### Key Design Decisions\n- Server resolves checkpoints so the model never needs to re-send full element arrays\n- `containerId` filtering ensures bound text elements are deleted with their containers\n- Camera aspect ratio check nudges model toward 4:3 ratios\n- `checkpointId` uses `crypto.randomUUID()` truncated to 18 chars (collision-resistant, URL-safe)\n\n## Build\n\n```bash\nnpm install\nnpm run build\n```\n\nBuild pipeline: `tsc --noEmit` → `vite build` (singlefile HTML) → `tsc -p tsconfig.server.json` → `bun build` (server + index).\n\n## Running\n\n```bash\n# HTTP (Streamable) — default, stateless per-request\nnpm run serve          # or: bun --watch main.ts\n# Starts on http://localhost:3001/mcp\n\n# stdio — for Claude Desktop\nnode dist/index.js --stdio\n\n# Dev mode (watch + serve) — full MCP flow\nnpm run dev\n\n# Dev mode (standalone UI) — no MCP server needed\nnpm run dev:ui\n# Opens http://localhost:5173/index-dev.html with mock app + sample diagram\n```\n\n## Claude Desktop config\n\n```json\n{\n  \"excalidraw\": {\n    \"command\": \"node\",\n    \"args\": [\"<path>/dist/index.js\", \"--stdio\"]\n  }\n}\n```\n\n## Rendering Pipeline\n\n### Streaming (`ontoolinputpartial`)\n1. `parsePartialElements` tries `JSON.parse`, falls back to closing array after last `}`\n2. `excludeIncompleteLastItem` drops the last element (may be incomplete)\n3. Only re-renders when element **count** changes (not on every partial update)\n4. Seeds are **randomized** per render — hand-drawn style animates naturally\n5. `exportToSvg` generates SVG → **morphdom** diffs against existing DOM\n6. morphdom preserves existing elements (no re-animation), only new elements trigger CSS animations\n\n### Final render (`ontoolinput`)\n1. Parses complete JSON, renders with **original seeds** (stable final look)\n2. Same `exportToSvg` + morphdom path — seamless transition, no blink\n3. Sends PNG screenshot to model context (debounced 1.5s)\n\n### CSS Animations (3 layers)\n- **Shapes** (`g, rect, circle, ellipse, text, image`): opacity fade-in 0.5s\n- **Lines** (`path, line, polyline, polygon`): stroke-dashoffset draw-on effect 0.6s\n- **Existing elements**: smooth `transition` on fill/stroke/opacity changes\n\n### Key Libraries\n- **morphdom**: DOM diffing for SVG — preserves existing nodes, only new nodes get animations\n- **exportToSvg**: Excalidraw's SVG export (with fonts inlined by default)\n\n## Cheat Sheet: Progressive Element Ordering\n\nThe `server.ts` cheat sheet instructs the model to emit elements progressively:\n- BAD: all rectangles → all texts → all arrows (blank boxes stream, then labels appear late)\n- GOOD: background shapes first, then per node: shape → label → arrows → next node\n- This way each node appears complete with its label during streaming\n\n## Debugging\n\n### Dev workflow\n1. Edit source files\n2. `npm run build` (or `npm run dev` for watch mode)\n3. Restart the server process (module cache means hot reload doesn't pick up `server.ts` changes for tool definitions)\n4. In Claude Desktop: restart the MCP server connection\n\n### Widget logging — NEVER use console.log\n\nUse the SDK logger — it routes through the host to the log file:\n\n```typescript\napp.sendLog({ level: \"info\", logger: \"Excalidraw\", data: \"my message\" });\n```\n\n**Log file**: `~/Library/Logs/Claude/claude.ai-web.log`\n\n```bash\n# Fullscreen transition logs (logger: \"FS\")\ngrep \"FS\" ~/Library/Logs/Claude/claude.ai-web.log | tail -40\n\n# General widget logs (logger: \"Excalidraw\")\ngrep \"Excalidraw\" ~/Library/Logs/Claude/claude.ai-web.log | tail -20\n\n# Clear logs before repro for clean output\n> ~/Library/Logs/Claude/claude.ai-web.log\n```\n\n### Widget debugging\n- The widget runs in an iframe\n- Check that `exportToSvg` isn't throwing (catches are silent)\n- morphdom issues: compare old vs new SVG structure in Elements panel\n\n### Common issues\n- **No diagram appears:** Check that `ontoolinputpartial` is firing — the `elements` field might be nested differently (`params.arguments.elements` vs `params.elements`)\n- **All elements re-animate on each update:** morphdom not working — check that SVG structure is similar enough for diffing (different root SVG attributes can cause full replacement)\n- **Font is default (not hand-drawn):** `skipInliningFonts` was set to `true` — must be removed/false\n- **Elements in wrong positions during animation:** Don't use CSS `transform: scale()` on SVG child elements — conflicts with Excalidraw's own transform attributes. Use opacity-only animations.\n\n## Gotchas\n\n- `ExcalidrawElement` type is at `@excalidraw/excalidraw/element/types`, not re-exported from main\n- `ExcalidrawImperativeAPI` type is at `@excalidraw/excalidraw/types`\n- Excalidraw's `containerId` on text elements does NOT auto-position text — that only works via `convertToExcalidrawElements` skeleton API\n- The `.SVGLayer` div is not used for rendering but takes layout space — safe to `display: none`\n- morphdom is essential — without it, replacing innerHTML re-triggers all animations on every update\n- `ReactDOM.render()` per update remounts the tree and kills animations — use `createRoot()` once + `useState` if adding React components\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# Excalidraw MCP App Server\n\nStandalone MCP server that streams Excalidraw diagrams as SVG with hand-drawn animations.\n\n## Architecture\n\n```\nserver.ts          → 2 tools (read_me, create_view) + resource + cheat sheet\nmain.ts            → HTTP (Streamable) + stdio transports\nsrc/mcp-app.tsx    → ExcalidrawAppCore (widget logic) + ExcalidrawApp (useApp wrapper)\nsrc/mcp-entry.tsx  → Production entry point: createRoot + ExcalidrawApp\nsrc/global.css     → Animations (stroke draw-on, fade-in) + auto-resize\nsrc/dev.tsx        → Dev entry point: mock app + sample elements + control panel\nsrc/dev-mock.ts    → Mock MCP App with event simulation (sendToolInput, streamElements, etc.)\nindex-dev.html     → Dev HTML entry (served by vite dev server)\nvite.config.dev.ts → Dev-only vite config (resolves from node_modules, no esm.sh externals)\n```\n\n## Tools\n\n### `read_me` (text tool, no UI)\nReturns a cheat sheet with element format, color palettes, coordinate tips, and examples. The model should call this before `create_view`.\n\n### `create_view` (UI tool)\nTakes `elements` — a JSON string of standard Excalidraw elements. The widget parses partial JSON during streaming and renders via `exportToSvg` + morphdom diffing. No Excalidraw React canvas component — pure SVG rendering.\n\n**Screenshot as model context:** After final render, the SVG is captured as a 512px-max PNG and sent via `app.updateModelContext()` so the model can see the diagram and iterate on user feedback.\n\n## Key Design Decisions\n\n### Standard Excalidraw JSON — no extensions\nThe input is standard Excalidraw element JSON. No `label` on containers, no `start`/`end` on arrows. These are Excalidraw's internal \"skeleton\" API (`convertToExcalidrawElements`) — not the standard format.\n\n**Why:** Standard format means any `.excalidraw` file's elements array works as input.\n\n**Trade-off:** Labels require separate text elements with manually computed centered coordinates. The cheat sheet teaches the formula: `x = shape.x + (shape.width - text.width) / 2`.\n\n### No `convertToExcalidrawElements`\nWe tried Excalidraw's skeleton API. Problems:\n1. Needs font metrics at conversion time (canvas `measureText`)\n2. Non-standard format\n3. Added complexity for marginal benefit\n\n### SVG-only rendering (no Excalidraw React canvas)\nThe widget uses `exportToSvg` for ALL rendering — no `<Excalidraw>` React component.\n\n**Why:**\n- Eliminates blink on final render (no component swap from SVG preview to canvas)\n- Loads Virgil hand-drawn font from the start (no `skipInliningFonts`)\n- morphdom works on SVG DOM — smooth diffing between streaming updates\n\n### Auto-sizing\nThe container has no fixed height. SVG gets `width: 100%` + `height: auto` with the `width` attribute removed. The SVG's `viewBox` preserves aspect ratio, so height scales proportionally to content.\n\n### CSP: `esm.sh` allowed\nExcalidraw loads the Virgil font from `esm.sh` at runtime. The resource's `_meta.ui.csp.resourceDomains` includes `https://esm.sh`.\n\n### `prefersBorder: true`\nSet on the resource content's `_meta.ui` so the host renders a border/background around the widget.\n\n### Fullscreen mode\nSupports `app.requestDisplayMode({ mode: \"fullscreen\" })`. Button appears on hover (top-right), hidden in fullscreen (host provides exit UI). Escape key exits fullscreen.\n\n## Checkpoint System\n\nTwo-tier storage for diagram state persistence:\n\n### Architecture\n1. **Server-side store** (primary): `CheckpointStore` interface with 3 implementations:\n   - `FileCheckpointStore` — local dev, writes JSON to `$TMPDIR/excalidraw-mcp-checkpoints/`\n   - `MemoryCheckpointStore` — Vercel fallback (in-memory Map, lost on cold start)\n   - `RedisCheckpointStore` — Vercel with Upstash KV (persistent, 30-day TTL)\n   - Factory: `createVercelStore()` picks Redis if env vars exist, else Memory\n\n2. **localStorage** (widget-side cache): Fast local cache keyed by `excalidraw:<checkpointId>` for persisting user edits across page reloads within the same session.\n\n### Flow\n- `create_view` resolves `restoreCheckpoint` references server-side, saves fully resolved state, returns `checkpointId`\n- Widget reads checkpoints via `read_checkpoint` server tool (private, app-only visibility)\n- User edits in fullscreen sync back to server via `save_checkpoint` server tool (debounced)\n- `cameraUpdate` elements are stored as part of checkpoint data (not a separate viewport field)\n\n### Key Design Decisions\n- Server resolves checkpoints so the model never needs to re-send full element arrays\n- `containerId` filtering ensures bound text elements are deleted with their containers\n- Camera aspect ratio check nudges model toward 4:3 ratios\n- `checkpointId` uses `crypto.randomUUID()` truncated to 18 chars (collision-resistant, URL-safe)\n\n## Build\n\n```bash\nnpm install\nnpm run build\n```\n\nBuild pipeline: `tsc --noEmit` → `vite build` (singlefile HTML) → `tsc -p tsconfig.server.json` → `bun build` (server + index).\n\n## Running\n\n```bash\n# HTTP (Streamable) — default, stateless per-request\nnpm run serve          # or: bun --watch main.ts\n# Starts on http://localhost:3001/mcp\n\n# stdio — for Claude Desktop\nnode dist/index.js --stdio\n\n# Dev mode (watch + serve) — full MCP flow\nnpm run dev\n\n# Dev mode (standalone UI) — no MCP server needed\nnpm run dev:ui\n# Opens http://localhost:5173/index-dev.html with mock app + sample diagram\n```\n\n## Claude Desktop config\n\n```json\n{\n  \"excalidraw\": {\n    \"command\": \"node\",\n    \"args\": [\"<path>/dist/index.js\", \"--stdio\"]\n  }\n}\n```\n\n## Rendering Pipeline\n\n### Streaming (`ontoolinputpartial`)\n1. `parsePartialElements` tries `JSON.parse`, falls back to closing array after last `}`\n2. `excludeIncompleteLastItem` drops the last element (may be incomplete)\n3. Only re-renders when element **count** changes (not on every partial update)\n4. Seeds are **randomized** per render — hand-drawn style animates naturally\n5. `exportToSvg` generates SVG → **morphdom** diffs against existing DOM\n6. morphdom preserves existing elements (no re-animation), only new elements trigger CSS animations\n\n### Final render (`ontoolinput`)\n1. Parses complete JSON, renders with **original seeds** (stable final look)\n2. Same `exportToSvg` + morphdom path — seamless transition, no blink\n3. Sends PNG screenshot to model context (debounced 1.5s)\n\n### CSS Animations (3 layers)\n- **Shapes** (`g, rect, circle, ellipse, text, image`): opacity fade-in 0.5s\n- **Lines** (`path, line, polyline, polygon`): stroke-dashoffset draw-on effect 0.6s\n- **Existing elements**: smooth `transition` on fill/stroke/opacity changes\n\n### Key Libraries\n- **morphdom**: DOM diffing for SVG — preserves existing nodes, only new nodes get animations\n- **exportToSvg**: Excalidraw's SVG export (with fonts inlined by default)\n\n## Cheat Sheet: Progressive Element Ordering\n\nThe `server.ts` cheat sheet instructs the model to emit elements progressively:\n- BAD: all rectangles → all texts → all arrows (blank boxes stream, then labels appear late)\n- GOOD: background shapes first, then per node: shape → label → arrows → next node\n- This way each node appears complete with its label during streaming\n\n## Debugging\n\n### Dev workflow\n1. Edit source files\n2. `npm run build` (or `npm run dev` for watch mode)\n3. Restart the server process (module cache means hot reload doesn't pick up `server.ts` changes for tool definitions)\n4. In Claude Desktop: restart the MCP server connection\n\n### Widget logging — NEVER use console.log\n\nUse the SDK logger — it routes through the host to the log file:\n\n```typescript\napp.sendLog({ level: \"info\", logger: \"Excalidraw\", data: \"my message\" });\n```\n\n**Log file**: `~/Library/Logs/Claude/claude.ai-web.log`\n\n```bash\n# Fullscreen transition logs (logger: \"FS\")\ngrep \"FS\" ~/Library/Logs/Claude/claude.ai-web.log | tail -40\n\n# General widget logs (logger: \"Excalidraw\")\ngrep \"Excalidraw\" ~/Library/Logs/Claude/claude.ai-web.log | tail -20\n\n# Clear logs before repro for clean output\n> ~/Library/Logs/Claude/claude.ai-web.log\n```\n\n### Widget debugging\n- The widget runs in an iframe\n- Check that `exportToSvg` isn't throwing (catches are silent)\n- morphdom issues: compare old vs new SVG structure in Elements panel\n\n### Common issues\n- **No diagram appears:** Check that `ontoolinputpartial` is firing — the `elements` field might be nested differently (`params.arguments.elements` vs `params.elements`)\n- **All elements re-animate on each update:** morphdom not working — check that SVG structure is similar enough for diffing (different root SVG attributes can cause full replacement)\n- **Font is default (not hand-drawn):** `skipInliningFonts` was set to `true` — must be removed/false\n- **Elements in wrong positions during animation:** Don't use CSS `transform: scale()` on SVG child elements — conflicts with Excalidraw's own transform attributes. Use opacity-only animations.\n\n## Gotchas\n\n- `ExcalidrawElement` type is at `@excalidraw/excalidraw/element/types`, not re-exported from main\n- `ExcalidrawImperativeAPI` type is at `@excalidraw/excalidraw/types`\n- Excalidraw's `containerId` on text elements does NOT auto-position text — that only works via `convertToExcalidrawElements` skeleton API\n- The `.SVGLayer` div is not used for rendering but takes layout space — safe to `display: none`\n- morphdom is essential — without it, replacing innerHTML re-triggers all animations on every update\n- `ReactDOM.render()` per update remounts the tree and kills animations — use `createRoot()` once + `useState` if adding React components\n","category":"root","tokens":2369}]}