# Install Sketchdex is a stdio MCP server published on npm as `sketchdex`. Every client below runs the same command: `npx -y sketchdex`. Nothing is installed globally and there is no account. Needs Node 20 or newer. Rendering (`render_scene`) needs a Chromium-based browser on the machine: Chrome, Chromium, Edge or Brave. Everything else works without one. ## Claude Code ```bash claude mcp add --scope user sketchdex -- npx -y sketchdex ``` `--scope user` registers it for every project. Check with `claude mcp list`, or `/mcp` inside a session. A session that was already open needs restarting before the tools show up. ## Codex ```bash codex mcp add sketchdex -- npx -y sketchdex ``` Or in `~/.codex/config.toml`: ```toml [mcp_servers.sketchdex] command = "npx" args = ["-y", "sketchdex"] ``` ## Cursor Global: `~/.cursor/mcp.json`. Per project: `.cursor/mcp.json` in the repo. ```json { "mcpServers": { "sketchdex": { "command": "npx", "args": ["-y", "sketchdex"] } } } ``` Cursor also opens a one-click install from this link: `cursor://anysphere.cursor-deeplink/mcp/install?name=sketchdex&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNrZXRjaGRleCJdfQ==` ## VS Code (Copilot agent mode) ```bash code --add-mcp '{"name":"sketchdex","command":"npx","args":["-y","sketchdex"]}' ``` Or in `.vscode/mcp.json` (note the key is `servers`, not `mcpServers`): ```json { "servers": { "sketchdex": { "type": "stdio", "command": "npx", "args": ["-y", "sketchdex"] } } } ``` ## Claude Desktop Edit `claude_desktop_config.json` and restart the app. - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "sketchdex": { "command": "npx", "args": ["-y", "sketchdex"] } } } ``` ## Windsurf `~/.codeium/windsurf/mcp_config.json`, same `mcpServers` block as Cursor. ## Gemini CLI ```bash gemini mcp add sketchdex npx -y sketchdex ``` ## Anything else Any client that can launch a stdio MCP server: command `npx`, arguments `-y sketchdex`. If the client takes a single command string, use `npx -y sketchdex`. ## Options Add these after the package name in `args`, so they work in every client: - `--vault /path/to/folder` stores scenes somewhere other than `~/Documents/sketchdex`. An iCloud or Dropbox folder is fine. Or set the environment variables `SKETCHDEX_VAULT` and `SKETCHDEX_BROWSER` in the server's `env` block. See the `vault` and `rendering` topics. ## Check it works In a session, ask: *draw me how the event loop works using sketchdex*. The agent should call `create_scene`, then `render_scene`, and show a PNG. To see and edit the result, run `npx sketchdex app`: it opens the library in the browser, and the new scene is the first card. The `app` topic covers the interface. --- # The vault Scenes are plain `.excalidraw` files in one folder, the vault. The default is `~/Documents/sketchdex`. `npx sketchdex vault` prints the folder the server is actually using. ``` ~/Documents/sketchdex/ ├── index.json names, timestamps, pins (a cache, not the truth) ├── scenes/.excalidraw the scene itself, real Excalidraw format ├── thumbs/.svg library previews ├── history// snapshots taken before agent writes, gzipped └── trash/-.excalidraw ``` The files are the truth. `index.json` is rebuilt from them if it is lost, so you can copy, sync or version the folder freely. Any scene opens on excalidraw.com or in any Excalidraw editor as it is. ## Moving it Pick one: - `--vault /path/to/folder` after the package name in the client config, e.g. `"args": ["-y", "sketchdex", "--vault", "/Users/me/Notes/sketches"]`. - `SKETCHDEX_VAULT=/path/to/folder` in the server's environment. `~` is expanded by the flag, since a JSON config never does that itself. The folder is created on first use. Existing scenes do not move on their own; copy the `scenes/` folder across and the index rebuilds. The app and the MCP server must point at the same folder to see the same scenes. `npx sketchdex app --vault /path` matches the flag above. ## History and trash Every write through a tool takes a snapshot of the previous state first, labelled with the note the agent gave it. Snapshots are gzipped, capped at 20 per scene, and never expire on their own. `list_history` shows them, `restore_snapshot` brings one back, and the app's History sidebar does the same with a preview. Deleting a scene moves the file to `trash/` with a timestamp. Nothing in `trash/` is cleaned up automatically. --- # The app The app is where the person sees what the agent drew: a library of every scene in the vault, and the real Excalidraw editor for each one. It is the same package as the MCP server, so nothing extra is installed. ## Opening it ```bash npx sketchdex app ``` Starts a local server, prints its URL (`http://localhost:5173` unless that port is busy) and opens it in the default browser. Leave the terminal running; `Ctrl C` stops it. Flags: - `--port `: default 5173. A busy port moves to the next free one. - `--no-open`: do not open a browser tab, just print the URL. - `--vault `: the scene folder, if the MCP server was given one too. A scene has a stable address, `http://localhost:5173/#/s/`, where the id is the one `list_scenes` and `create_scene` return. After drawing something, tell the person to run `npx sketchdex app` if it is not already open, or give them that link if it is. The page notices agent writes as they happen, so it can stay open while the agent draws and the canvas updates on its own. ## The interface **Library** (the front page): a grid of scene cards with hand-drawn previews, newest edit first. At the top: a search box, sort by Edited, Created or Name, Jump (`Cmd K` or `Alt K`, a switcher that opens any scene by typing part of its name), Import (an `.excalidraw` file from anywhere, excalidraw.com exports included), and New scene. Each card has rename, duplicate, pin and delete; deleted scenes go to the vault's `trash/`. **Editor**: the official Excalidraw canvas, full screen, with every tool and shortcut from excalidraw.com, so anything the person already knows carries over. Above it a small bar: All scenes (back to the library), the scene name (click to rename) with its saved state, previous and next scene, History with a count of pending agent writes, and the `Cmd K` switcher. Changes save on their own; there is no save button. `Cmd K` shadows Excalidraw's own add-link shortcut, so links go through the context menu. **History** (the sidebar in the editor): every write the agent made, newest first, each with the label the agent gave it and a preview of the scene as it was before. For each one: restore that state, keep the current state and clear the list, or discard the snapshot. Writes made outside sketchdex, in another editor for example, are snapshotted too and marked as such. The agent can do the same through `list_history` and `restore_snapshot`. Editing while the agent draws is fine. Both sides write deltas against a revision token and conflicting saves are merged, so a box the person moved stays moved when the agent adds the next one. ## What it serves Everything is bound to localhost and refuses requests from other hosts and cross-site writes. - `/`: the library and editor. - `POST /mcp`: the same tools over HTTP, for a client that can only be given a URL. - `POST /api/agent/`: the same tools as plain REST, one JSON body per call, for an agent that can run curl but has no MCP client. The `drawing` topic lists the operations. The MCP server and the app are independent. Neither needs the other running, but they must share a vault to see the same scenes. --- # Rendering `render_scene` draws the scene exactly as Excalidraw exports it and returns a PNG, so an agent can look at its own work. It needs a Chromium-based browser on the machine and finds one on its own: Chrome, Chromium, Edge or Brave, in the usual install locations on macOS, Windows and Linux. Nothing is downloaded. In a git checkout with `electron` installed, Electron is the fallback. `SKETCHDEX_BROWSER=/path/to/binary` in the server's environment overrides the search. Use it when the browser is somewhere unusual, or to pick one. ## What to expect - The first render after the server starts takes a few seconds while the browser comes up. After that about a second and a half. - The PNG is always written to a file in the system temp folder (`sketchdex-renders/`) and its path is in the result, so it can be read or attached even when it was too large to show inline. - Above roughly 150 KB the image travels as a downscaled preview plus the path to the full file. Clients truncate over-long results silently, which is why the ceiling is conservative. `SKETCHDEX_INLINE_LIMIT` (characters of base64) changes it. - A page much taller or wider than it is high comes back as a few tiles, each labelled with the scene box it shows. Pass `tiles: 1` for a single image. - `region` renders one box in scene coordinates, the way to look closely at one part. `ids` renders only those elements. `transparent: true` leaves the paper out, for dropping a sketch onto a slide or a web page. - `scale` 1 to 3 sets the pixel density; `maxWidth` caps the long edge. `check_scene` finds layout problems by geometry (hidden text, overflowing labels, dangling arrows, strays) without a browser, and is far cheaper. Use it after a batch of edits; render when the question is how it looks. --- # Tools 19 tools over stdio. Every tool that changes a scene requires `label`, a short note the user reads in the history sidebar. `scene` accepts an id, an exact name, or a unique part of a name. - `list_scenes`: Every scene in the vault with its id, name, element count and last-edited time. - `describe_scene`: What is on a scene, as structure rather than pixels: counts by type, overall bounds, and a rounded summary of each element with its id, position, size, text and bindings. - `read_elements`: Full, unabridged elements, filtered down to the ones you need. - `read_scene`: The whole scene exactly as stored, plus the path to the .excalidraw file on disk. - `create_scene`: Make a new scene, optionally with its starting elements. - `add_elements`: Append elements to a scene, leaving everything already there untouched. - `update_elements`: Change named fields on existing elements. - `delete_elements`: Remove elements by id. - `move_elements`: Shift a set of elements by dx, dy in one write, keeping everything else about them. - `replace_scene`: Replace a scene's entire contents. - `set_background`: Set the paper: the scene's real background colour, shown exactly as given (never inverted). - `render_scene`: A PNG of the scene as it actually renders, so you can look at what you drew. - `import_svg`: Draw by writing SVG. - `check_scene`: Find layout problems by geometry instead of by looking: text hidden behind a filled shape drawn after it, captions running past the panel they sit in, labels too tall for their container, zero-size elements, broken arrow bindings, strays parked far off, exact duplicates. - `sketchdex_info`: Version of this server, where it is running from, which vault it is using, and how long this process has been alive. - `sketchdex_docs`: The manual, for answering the user's questions and fixing problems without guessing: install on any client, where scenes are stored and how to move the vault, the library app, what rendering needs, every tool, the drawing contract, and troubleshooting for the errors that come up. - `snapshot_scene`: Stash the current state without changing anything, so it can be restored from the history sidebar. - `list_history`: Snapshots held for a scene, newest first. - `restore_snapshot`: Put a snapshot back on the canvas. --- # Sketchdex: a brief for an agent Self-contained. You need nothing else to start drawing. Sketchdex is a local Excalidraw library on this machine. Drawings are called **scenes** and live as real `.excalidraw` files in a **vault** (by default `~/Documents/sketchdex`; `sketchdex vault` prints the real path). You can read them, draw on them, and look at what you drew. Everything is local: no network, no account, no upload. There is a safety contract, described under **History**. Read it before writing. --- ## Three ways in. Pick whichever you have. ### 1. MCP If you have tools named `list_scenes`, `describe_scene`, `add_elements`, `render_scene` and so on, the server is registered with your client: use them and skip to **The element format**. Argument names match the JSON fields below. For anything about setup (installing on a client, the vault, the app, rendering, an error), `sketchdex_docs` has the manual by topic. (The same tools are also served over HTTP at `POST localhost:5173/mcp` while the app is running, for a client you can only hand a URL.) ### 2. Shell (any agent that can run bash) The app exposes the identical surface at `POST /api/agent/:op` on **port 5173**. One endpoint, one JSON body, `scene` names the target. ```bash curl -s localhost:5173/api/agent/describe \ -H 'Content-Type: application/json' \ -d '{"scene":"Project Centis"}' ``` If nothing answers on 5173, start it: `npx sketchdex app` (or `npm run dev` in a checkout). ### 3. Node (no server needed) ```js import { createAgentApi } from "sketchdex"; const api = createAgentApi(); await api.describeScene("Project Centis"); ``` Same implementation as the other two. Works with the app closed. --- ## Operations `POST localhost:5173/api/agent/`, body is JSON. `scene` accepts a scene id, an exact name, or a unique part of a name. | op | body | does | |---|---|---| | `list` | `{}` | every scene: id, name, element count, updated | | `describe` | `{scene, type?, limit?}` | structural summary - **start here** | | `elements` | `{scene, ids?, type?, contains?, region?, limit?}` | full elements, filtered | | `read` | `{scene, includeFiles?}` | the whole scene + its file path | | `path` | `{scene}` | just the file path | | `create` | `{name, elements?}` | new scene | | `add` | `{scene, elements, label}` | append elements | | `update` | `{scene, elements, label}` or `{scene, ids, patch, label}` | change fields on existing elements | | `delete` | `{scene, ids, label}` | remove elements | | `move` | `{scene, ids?, groupIds?, region?, dx, dy, label}` | shift a set of elements as one | | `replace` | `{scene, elements, label}` | replace everything (destructive) | | `background` | `{scene, color, label}` | canvas colour | | `snapshot` | `{scene, label}` | stash current state, change nothing | | `history` | `{scene}` | snapshots held | | `restore` | `{scene, snapshot}` | put a snapshot back | Rendering (`render_scene`) is MCP-only - it returns an image, which REST can't usefully hand back. Over shell, open the scene in the app instead. **`label` is required on every mutating op** and enforced server-side, not just declared in the schema. A call without one is rejected. **Rendering.** Every render is written to a file and the `path` always comes back. When the full image is too big to travel in one result you get a downscaled JPEG preview inline plus the path to the full-resolution PNG, so there is always something to look at. `inlined` says which you got. A tall or wide page (long edge more than about twice the short one) comes back as a few **tiles** cut along its length, each tagged with the scene box it shows; `tiles: 1` forces one image. To look closely at one part, render a `region`, taking the box from `describe`'s `bounds` or a tile's `region`: `{"scene":"Brain Dump","region":{"x":0,"y":0,"width":900,"height":700},"scale":2}`. `ids` renders just those elements. Draw, render, look, fix: anything visual handed over without having looked at it is a guess. First render after a code change takes ~30s (it builds the page); after that ~1.5s. Oversized **text** results (a `read` of a big scene) are written to a file the same way rather than truncated, with the path returned. ### Read cheaply, write as a delta Rewriting the file is free: it happens on local disk, inside the server, not in your context. What costs you is **reading**. Scenes here run past 700 elements and 4MB. So: `describe` first (rounded, defaults stripped, ~10 fields per element), then `elements` for the few you actually need in full. `read` is a last resort. And never read a scene, mutate the array, and `replace` it back. Send only what changes: ```bash curl -s localhost:5173/api/agent/update -H 'Content-Type: application/json' -d '{ "scene": "Project Centis", "label": "shift the auth box right", "elements": [{ "id": "kdy29EsP9-Y3MDfCVRBon", "x": 420 }] }' ``` That element's other 25 fields are untouched, and any edit the user made in the meantime survives. `{"ids": [...], "patch": {...}}` applies one change to many. --- ## The element format Elements are **real Excalidraw elements**. Any field you set is kept verbatim - paste a full element out of a `.excalidraw` file and it round-trips unchanged. Fields you omit are filled in. `id`, `index`, `seed`, `version`, `versionNonce` and `updated` are always generated for you. Types: `rectangle` `ellipse` `diamond` `arrow` `line` `text` `freedraw` `frame`. Others (`image`, `embeddable`, …) pass through with universal defaults only. ``` x, y, width, height y axis points DOWN, origin top-left angle radians strokeColor hex, default "#1e1e1e" backgroundColor hex or "transparent" <- this is the FILL fillStyle "hachure" | "cross-hatch" | "solid" strokeWidth 1 | 2 | 4 strokeStyle "solid" | "dashed" | "dotted" roughness 0 architect | 1 artist | 2 cartoonist opacity 0-100 roundness {"type": 3} rounded, null sharp fontSize, fontFamily 5 hand-drawn (default) | 2 normal | 3 code textAlign "left" | "center" | "right" groupIds shared string = elements move together points [[0,0],[dx,dy],...] for line/arrow/freedraw, relative ``` ### Paper and ink: colours are exactly what you write Nothing is ever inverted for display. The canvas shows every hex as it is, including the background, so `#ffc21a` is gold and `#ffffff` is white on any scene. There is no dark "mode": a dark scene is one whose data is dark. - **New scenes are dark paper**: `viewBackgroundColor` `#121212`. - **Default ink follows the paper.** Anything you draw without a `strokeColor` gets `#e9ecef` on dark paper and `#1e1e1e` on light paper. `describe_scene` reports both as `background` and `ink`. Check them before choosing colours - a `#1e1e1e` outline on `#121212` paper is invisible. - `set_background` changes the paper; the default pen follows it. Existing elements keep their colours, so recolour ink you already drew if you flip a scene from light to dark. - Fills should be chosen for the paper. On dark paper, mid-tone fills read best (`#245b8f` `#2b6b3a` `#8a2b2b` `#8a6a12`), with light ink strokes and white `#ffffff` for highlights. On light paper Excalidraw's soft set works: stroke `#1e1e1e #e03131 #2f9e44 #1971c2 #f08c00`, fill `#ffc9c9 #b2f2bb #a5d8ff #ffec99`. Character colours are character colours: Iron Man is `#e0101a` and `#ffc21a` on either paper. ### The look is hand-drawn. Keep it that way. This is Excalidraw. The point of every drawing here is that it reads as sketched by a person with a pen, not exported from a vector tool. Everything generated for you (SVG imports, the shape primitives) already comes out that way; what you write by hand has to match. The rules, in order of how much they matter: - **roughness 1** on everything (it is the default). 2 for a loose doodle. Never 0 unless the thing must be precise: a logo, a wiring diagram. - **Native shapes first.** A `rectangle`, `ellipse`, `diamond` or `arrow` drawn as its own type gets Excalidraw's double-stroke wobble. The same box drawn as a 4-point `line` does not. Heads, eyes, wheels, faces: `ellipse`. - **Few points, confident strokes.** A curve someone drew by hand has anchors 15-20px apart and each segment gets one bow. A dense polyline (a point every 2-3px) at roughness 1 looks furry, not sketchy - it is the single reason an import reads as clip-art. If you write `points` yourself keep them sparse and set `roundness: {"type": 2}` so they draw as a curve. - **Flat colour, no shading.** One solid fill per part; the wobbly outline does the rest. A darker part is a darker hex, not a hatched overlay or a shadow shape, and there are no gradients faked with stacked shapes. `hachure` / `cross-hatch` are for things that *are* hatched (a cutaway, a glass pane), not for texture. Overworked shading is the fastest way for a sketch to stop looking like one. - **Detail is a few extra strokes, not more polygons.** A leg is one tapered silhouette with a line at the knee - not a thigh polygon, a shin polygon and a foot polygon. When a figure looks like a toy, the fix is the silhouette, never more parts. More parts is how a sketch turns into a vector diagram. - **Text** in the hand-drawn font (`fontFamily` 5, the default). Stroke width 1 or 2, 4 for a bold outline. A white or paper canvas. Any hex works, but a black background with neon panels is a poster, not a sketch. ### Text wraps to the width you give it - A **bound `label`** wraps to its container's width, and the container grows taller if the label needs the room. - **Standalone text with a `width`** wraps to it. - **Standalone text with no width** stays on one line. - `wrap: ` forces a width, `wrap: false` opts out. As in Excalidraw, `text` holds the wrapped string and `originalText` holds what you passed. Editing text later re-wraps to the element's current width. ### To draw something that has to *look* like something, write SVG `import_svg` is THE way to draw a character, creature, object, vehicle, logo or illustration. Stacking shapes with `add_elements` never works for this: every part comes out crisp and the whole comes out as a pile (fat, seamed, toy-like), because nothing holds the proportions. One SVG comes out as one drawing. The import is hand-drawn, not pasted: every path becomes a real editable Excalidraw element (grouped as one piece), curves are re-spaced to hand-sized strokes and drawn with the sketchy pen, ``/``/`` become native shapes. Write the SVG for **shape, proportion and colour** and let the canvas supply the pen. Do not fake wobble or hatching in the SVG. A recipe that works for a figure: - `viewBox` about 300 x 600 for a standing figure; import at `width` 350-450. - **One closed path per body region** (torso, each leg, each arm), tapered like a real limb: forearm ~60% of the upper arm, calf into a narrow ankle. Plus head, hands, feet. Let them overlap; later paths draw on top. Then detail as separate strokes *over* the silhouette: plates, seams, webbing, a knee line. A few, not many. - Every shape: `fill` a vibrant, character-accurate hex; `stroke` the scene ink (from `describe`) or a deep shade of the fill; `stroke-width` 2 to 2.5. Filled shapes with no stroke read as flat. - Proportion from a rule: 7-8 heads tall for heroic, 5-6 for stylised. Head first, derive the rest. - Then **render and look**; fix what is off. The first pass is usually mis-proportioned, the second is right. Supported: `path` (all commands), `rect`, `circle`, `ellipse`, `line`, `polyline`, `polygon`, `g`, `transform`, `