# Figjum for agents Figjum is a tldraw-based collaborative whiteboard at kit.madebytle.com. This page is the manual for a model driving a board over MCP or plain HTTP. It is plain text on purpose — fetch it, do not browse it. The human-facing manual (the buttons a person clicks) is a different page: /figjum/docs. Stable URLs: - https://kit.madebytle.com/figjum/llm — this page, HTML - https://kit.madebytle.com/figjum/llm.txt — this page, text/plain --- # Half A — the API ## Two ways in **MCP (recommended).** A streamable-HTTP MCP server scoped to one board: https://kit.madebytle.com/api/figjum/mcp/ **Plain HTTP.** One endpoint, draw only, no snapshot and no clear: POST https://kit.madebytle.com/api/figjum/boards//draw Both authenticate with the same header: Authorization: Bearer fjm_ Get the URL and the token from the board page: open the board, click **Connect**, copy the snippet for your client. A token is scoped to exactly one board. Pointing it at another board's endpoint returns 403 `token_board_mismatch` naming the board the token actually belongs to — that means wrong token, not a deleted board. ## The six tools | tool | what it does | |---|---| | `get_board_snapshot` | read shapes, boundingBox, frames, and an SVG preview | | `draw_on_board` | add shapes | | `clear_board` | remove ALL shapes — irreversible, no undo | | `get_capabilities_doc` | this document, over the wire | | `export_board_as_png` | render the board to an inline base64 PNG (scale 0.5–4) | | `measure_text` | pre-flight text sizing against real tldraw font metrics | ## Limits — read these before you plan a turn **There is no selective shape edit.** Nothing updates, moves, restyles, or deletes an individual shape. Changing one box means `clear_board` then redrawing the entire composition. Practical consequence: keep the ops array that produced the current board in your own context, so a revision is an edit to your JSON rather than an archaeology exercise against the snapshot. **`clear_board` has no undo** on the API side. A human at the board can press Cmd/Ctrl+Z, an agent cannot. **Shapes append, they do not replace.** Two `draw_on_board` calls with the same coordinates stack two sets of shapes on top of each other. To redraw, clear first — or pass `appendBelow: true` and let the server place the new batch below everything that already exists. **`get_board_snapshot` is lossy.** It reports type, position, size, colour and text. It is enough to avoid overlapping existing content and to decide whether to clear. It is not a round-trippable source you can feed back into `draw_on_board`. **Boxes do not grow to fit their label.** A note, rectangle or ellipse stores a fixed w×h. Since v0.7.0 the server measures and shrinks the label for you (see auto_fit below), so this is handled — but it shrinks, it never widens the box. **Board content lives in PartyKit, not in Postgres.** The database row holds metadata only (title, owner, expiry, thumbnail). There is no historical board content to migrate or query. **Board lifetime.** A board row has a 90-day TTL from creation; boards owned by an account are effectively exempt. A daily job removes only boards already past their TTL. Drawing persists immediately, and large boards (including base64 images) are stored in chunks so nothing is silently dropped. If a draw cannot be persisted, the API answers 507 `draw_not_persisted` rather than a false success. **The board page is `/figjum/board/`**, not `/board/`. A signed-out visit is rewritten to 404 — that is an auth redirect, not a deleted board. ## Coordinate system Pixels. x grows right, y grows down. Compose inside 0–1000 on both axes and let `offsetX` / `offsetY` (default 0) place the composition. `boundingBox` in the snapshot tells you the area already in use. ## Ops `draw_on_board` takes `ops`, an ordered array of 1–5000 shape operations. | kind | required | notes | |---|---|---| | `pen` | `points: [[x,y], ...]` (≥2) | smooth freehand curve | | `line` | `x1, y1, x2, y2` | straight segment | | `rectangle` | `x, y, w, h` | optional `text` label inside | | `ellipse` | `x, y, w, h` | optional `text` label inside | | `arrow` | `x1, y1, x2, y2` | optional `text` label | | `text` | `x, y, text` | standalone label, no container | | `note` | `x, y, w, h, text` | sticky card: filled colour, no border, top-left text, honours any w/h | | `image` | `src, x, y, w, h` | `src` = public http(s) URL or `data:image/...;base64,...`; optional `alt` | For `image`, prefer a `data:` URI. Hotlink-protected hosts fail CORS, and an external URL renders as a placeholder in `export_board_as_png`. ## Style, optional on every op | field | values | default | |---|---|---| | `color` | black, grey, red, light-red, orange, yellow, green, light-green, blue, light-blue, violet, light-violet, white | black | | `size` | s, m, l, xl | m | | `fill` | none, semi, solid, pattern | none (note: solid) | | `font` | sans, serif, mono, draw | sans | | `dash` | solid, dashed, dotted, draw | solid | | `align` | start, middle, end | start (note text) | | `auto_fit` | true, false | **true** | `font` applies to text, rectangle, ellipse, arrow labels, and note. `dash` applies to pen, line, rectangle, ellipse, arrow. Do not set `font` or `dash` just to get a clean look — since v0.3.1 the defaults are `sans` and `solid`, which is what you want. Set `font: "draw"` and `dash: "draw"` only when you deliberately want a hand-sketched result. ## auto_fit — on by default since v0.7.0 Labels on note, rectangle and ellipse are fitted by the server automatically. It measures the text with the same tldraw font metrics the browser renders with, shrinks the size step by step (xl → l → m → s) until the label fits w×h, and only truncates with an ellipsis if it still overflows at `s`. The draw response reports every size change and truncation it applied. You do not need to pass anything. You do not need to count characters. Earlier versions made this opt-in, and every agent shipped its own character-counting heuristic; that guesswork is now the server's job. Both APIs behave the same way — MCP `draw_on_board` and `POST /draw`. Pass `auto_fit: false` on an op only when you deliberately want the size you asked for and accept the overflow. Metrics, if you want to compute a layout yourself: sizes s/m/l/xl render at 18/22/26/32 px, line-height 1.35, 16 px padding on every side — so the usable area is (w − 32) × (h − 32). Thai combining marks measure as zero width, emoji as roughly 1.2 em. `measure_text` exposes the same calculation directly. ## Frames Pass `frame: { x, y, w, h, name?, color? }` to wrap a batch in a named tldraw frame. Inside a frame, op coordinates become FRAME-RELATIVE (0..w, 0..h) and anything outside is clipped. Use it for portrait or landscape compositions that would otherwise spill, or to group one catalogue tile (images plus a note card) under a heading. ## Building a board across several turns Either let the server place the batch: { "appendBelow": true, "appendGap": 40, "ops": [ ... ] } The server reads the current board, finds the lowest existing content, and shifts this batch below it. `appendGap` defaults to 40. Or place it yourself: `get_board_snapshot` → read `boundingBox.maxY` → pass `offsetY`. ## The JSON-RPC envelope MCP over streamable HTTP. Initialize, then call tools: ```bash curl -sS https://kit.madebytle.com/api/figjum/mcp/ \ -H "Authorization: Bearer fjm_" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "draw_on_board", "arguments": { "ops": [ { "kind": "text", "x": 320, "y": 40, "text": "Deploy pipeline", "size": "l" }, { "kind": "line", "x1": 100, "y1": 80, "x2": 900, "y2": 80, "color": "grey" } ] } } }' ``` Most clients never see this envelope — `claude mcp add` or an `.mcp.json` entry handles it. Use the raw form for debugging or from a language without an MCP client. ## The plain HTTP endpoint No JSON-RPC, no tool names, draw only: ```bash curl -X POST https://kit.madebytle.com/api/figjum/boards//draw \ -H "Authorization: Bearer fjm_" \ -H "Content-Type: application/json" \ -d '{"ops":[{"kind":"rectangle","x":100,"y":100,"w":220,"h":90,"text":"Build","fill":"semi","color":"blue","size":"s"}]}' ``` Answers `{ ok, inserted, persisted, ids }`, or 507 `draw_not_persisted` if the shapes reached memory but not durable storage. It accepts `ops`, `offsetX`, `offsetY` and `frame`, and applies auto_fit the same way MCP does. It does not support `appendBelow`; read the board over MCP if you need that. --- # Half B — how to draw well These conventions come from Mint, who reverse-engineered them one bad diagram at a time. They are published here so the next agent inherits them instead of rediscovering them. ## Composition - **5 to 60 ops.** Readability beats density. A board a person cannot scan in five seconds has failed regardless of how much is on it. - **Centre the composition around (500, 500)**, working inside 0 to 1000. - **Always open with a header** `text` op at the top, plus a grey `line` divider under it. A diagram without a title reads as debris. - **Never leave bare text floating in space.** Put it in a rectangle. Containers are what make a diagram scannable. A standalone `text` op is for the header, for a label beside a box, and for the closing summary — not for content. - **Comparison** (before/after, option A/B): two columns, left at x=100, right at x=520. Rows that correspond share a y, so the eye can travel sideways. - **Single flow:** rectangles top to bottom, joined by arrows. - **Numeric comparison:** build a grid. A row of boxes reads better than a paragraph, every time. - **Close with a large summary** `text` op if there is a takeaway. ## Colour - red / orange for the "before", the problem, the thing being replaced. - green for the "after", the fix, the recommendation. - violet for AI or processing steps. - grey for dividers and secondary structure. - **Never white.** The canvas is white. ## Text fitting The server now fits labels for you (auto_fit, above). These still apply, because shrinking text is a worse outcome than writing text that fits: - **Width 180 to 220 fits roughly 16 to 22 Thai characters** at the default size. Plan the label to the box, not the box to the label. - **Put `size: "s"` on every rectangle and ellipse** in an architecture or system diagram, unless it is a section header. Confirmed readable by Tle. Reserve m / l / xl for headers and callouts. - If a label still will not fit: shorten it, widen the box past 240, break it with `\n`, or move it to a standalone `text` op beside the box. - Check the draw response. If it reports a size change or a truncation, the label was too long for the layout you chose — fix the layout, do not accept the shrink as a result. ## Punctuation No middle dot (·) and no em dash (—) in any text string. Use a hyphen, a comma, or a line break. Both render badly at small sizes and both are easy to mistake for drawn strokes. ## A worked example A before/after comparison with a header, two columns and a closing summary: ```json { "ops": [ { "kind": "text", "x": 340, "y": 40, "text": "Deploy: before and after", "size": "l" }, { "kind": "line", "x1": 100, "y1": 90, "x2": 900, "y2": 90, "color": "grey" }, { "kind": "text", "x": 100, "y": 120, "text": "Before", "size": "m", "color": "red" }, { "kind": "text", "x": 520, "y": 120, "text": "After", "size": "m", "color": "green" }, { "kind": "rectangle", "x": 100, "y": 160, "w": 300, "h": 80, "text": "Manual build on a laptop", "color": "red", "fill": "semi", "size": "s" }, { "kind": "rectangle", "x": 520, "y": 160, "w": 300, "h": 80, "text": "CI builds on every push", "color": "green", "fill": "semi", "size": "s" }, { "kind": "rectangle", "x": 100, "y": 270, "w": 300, "h": 80, "text": "Deploy takes 40 minutes", "color": "red", "fill": "semi", "size": "s" }, { "kind": "rectangle", "x": 520, "y": 270, "w": 300, "h": 80, "text": "Deploy takes 4 minutes", "color": "green", "fill": "semi", "size": "s" }, { "kind": "arrow", "x1": 420, "y1": 200, "x2": 500, "y2": 200, "color": "grey" }, { "kind": "arrow", "x1": 420, "y1": 310, "x2": 500, "y2": 310, "color": "grey" }, { "kind": "text", "x": 260, "y": 420, "text": "10x faster, and nobody has to be at their desk", "size": "l", "color": "green" } ] } ``` Header, divider, two columns at x=100 and x=520, rows sharing a y, red for before and green for after, `size: "s"` on every box, a summary at the bottom. --- # Changes that affect agents - **v0.7.0** — `auto_fit` is on by default, and the plain HTTP `/draw` endpoint applies it too. Stop passing the flag; stop counting characters. This page published, and its link now travels inside the Connect snippets and the MCP server instructions. - **v0.6.0** — `measure_text` added; `auto_fit` available as an opt-in flag. - **v0.3.1** — defaults changed to `font: "sans"` and `dash: "solid"`. Do not set either unless you want a hand-sketched look.