กลับ Figjum

Figjum for agents

หน้านี้เขียนให้ โมเดลอ่าน ไม่ใช่คนอ่าน — เป็นคู่มือ API กับกติกาการวาดสำหรับ AI ที่ต่อบอร์ดผ่าน MCP (คู่มือปุ่มสำหรับคนอยู่ที่ /figjum/docs)

raw markdown (/figjum/llm.txt)

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:


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/<boardId>

Plain HTTP. One endpoint, draw only, no snapshot and no clear:

POST https://kit.madebytle.com/api/figjum/boards/<boardId>/draw

Both authenticate with the same header:

Authorization: Bearer fjm_<token>

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

toolwhat it does
get_board_snapshotread shapes, boundingBox, frames, and an SVG preview
draw_on_boardadd shapes
clear_boardremove ALL shapes — irreversible, no undo
get_capabilities_docthis document, over the wire
export_board_as_pngrender the board to an inline base64 PNG (scale 0.5–4)
measure_textpre-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/<id>, not /board/<id>. 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.

kindrequirednotes
penpoints: [[x,y], ...] (≥2)smooth freehand curve
linex1, y1, x2, y2straight segment
rectanglex, y, w, hoptional text label inside
ellipsex, y, w, hoptional text label inside
arrowx1, y1, x2, y2optional text label
textx, y, textstandalone label, no container
notex, y, w, h, textsticky card: filled colour, no border, top-left text, honours any w/h
imagesrc, x, y, w, hsrc = 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

fieldvaluesdefault
colorblack, grey, red, light-red, orange, yellow, green, light-green, blue, light-blue, violet, light-violet, whiteblack
sizes, m, l, xlm
fillnone, semi, solid, patternnone (note: solid)
fontsans, serif, mono, drawsans
dashsolid, dashed, dotted, drawsolid
alignstart, middle, endstart (note text)
auto_fittrue, falsetrue

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:

curl -sS https://kit.madebytle.com/api/figjum/mcp/<boardId> \
  -H "Authorization: Bearer fjm_<token>" \
  -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:

curl -X POST https://kit.madebytle.com/api/figjum/boards/<boardId>/draw \
  -H "Authorization: Bearer fjm_<token>" \
  -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:

{
  "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.0auto_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.0measure_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.
v1.5.5