Eight tools. The one decision that matters is which of the three writing tools to use, because two of them are destructive in ways that are not obvious.
| Tool | Needs unlock | |
|---|---|---|
list_workspaces |
for names | ids work locked; names are ciphertext |
list_sheets |
for names | same |
read_sheet |
yes | returns an outline, not raw Excalidraw JSON |
validate_spec |
no | no side effects; check before writing |
create_diagram |
no | new sheet from a spec |
update_diagram |
no | replaces a sheet wholesale, regenerating layout |
edit_sheet_text |
yes | rewrites named text, preserving everything else |
import_sheet |
no | writes a local .excalidraw file in verbatim |
Reads fail softly. list_workspaces and list_sheets still return ids with
ciphertext names; read_sheet returns an instruction to run login.
The instruction is written for the model as much as the user, and says two things explicitly: that no restart is needed — retry the same tool after unlocking — and that it must never ask for the passcode itself. A model that asks you to type your passcode into the conversation has moved your account's master secret into a transcript, which is the failure the out-of-band unlock exists to prevent.
Was the sheet generated from a spec? update_diagram is correct.
Regenerating the layout is the point.
Did a person draw it? update_diagram will destroy it. Layout is derived
from the spec, and hand-placed text, region containers, and unbound annotation
arrows have no representation in a spec at all — the sheet comes back as an
auto-laid-out graph. Right content, different diagram.
edit_sheet_text.import_sheet.edit_sheet_textReads the scene, rewrites only the strings you name, writes every other element back untouched.
edit_sheet_text(workspace_id, sheet_id, edits: [{ find, replace }])
find matches with whitespace collapsed, so the flattened single-line form
read_sheet prints works even for an element that contains line breaks. Strip
the leading [rectangle] marker first — that is outline formatting, not part of
the text.
This matters because read_sheet renders newlines as spaces to keep the outline
one line per element. Requiring byte-exact find therefore made its own output
unusable for any multi-line text, while the instructions said to copy from
there. Identifying which element to change should not require reproducing line
breaks that are invisible in every rendering of it.
Edits are all-or-nothing: if any find matches nothing, the sheet is left
alone. A half-applied edit is worse than none — the result is a state nobody
expected, and the diff is invisible without re-reading.
Its one geometry change is growing a box whose text no longer fits. Replacement text is re-wrapped to the width the element already occupied — the column the author chose by hand-wrapping the original — so a longer replacement becomes more lines rather than one very wide one, and the box grows in height to match. Excalidraw regrows a container around text bound to it, but hand-drawn diagrams usually have text merely sitting on a shape, so longer text would otherwise spill out. Boxes are only ever grown, never shrunk, and nothing else moves — reflowing neighbours would be the wholesale rewrite this tool exists to avoid. Any box it grows is named in the result.
Longer replacement text is re-wrapped to the width the element already occupied, so it becomes more lines rather than one very wide one, and the box grows in height to match. Before this, a long replacement measured as a single enormous line: barely taller than what it replaced, so nothing grew, while running hundreds of pixels wider than its container.
If a box still looks wrong after an edit, re-applying the same text as a
no-op replacement — identical find and replace — re-measures it and
grows the box, without needing an export.
It cannot add elements.
import_sheetimport_sheet(workspace_id, file_path, name, sheet_id?)
Writes a local .excalidraw file in exactly as it is — every coordinate
preserved. Use it when geometry has to change: repositioning, resizing,
re-anchoring arrows, adding elements. Do the pass in a real editor, or by
editing the file directly, then push it.
This works only because the server runs on your machine. The file's contents go into the encrypted blob and never into the model's context, so a large scene costs nothing to transfer.
validateScene runs on import but only reports. It is your drawing; overlapping
boxes and unbound arrows are your business.
read_sheetReturns shapes and edges, not raw Excalidraw JSON:
shapes (10):
[rectangle] Browser — React + Excalidraw
[ellipse] Postgres 17
edges (9):
encryptMessage() — in the browser -> nginx :80 "ciphertext only"
A real sheet's raw scene runs to tens of thousands of characters of coordinates, seeds, and style. The outline is what the diagram means, in a few dozen lines.
Labels are recovered from bindings where they exist, and otherwise from geometry — text inside a shape becomes that shape's label, text near an arrow becomes the arrow's. Without that fallback, a hand-drawn diagram reads back as a list of unlabelled boxes, since people place labels on shapes rather than typing into them.