Files
wordcraft/docs/control-protocol.md
Valentin KleinandClaude Opus 5.5 4a75c57f58 Comment balloons edit in place, and the ruler drags page margins (#167) (#289)
Clicking a comment balloon in the markup area selects it like Word: its text
becomes editable in place, with Reply, Resolve/Reopen and Delete, and replies
are shown (and editable) under the comment they answer. Each edit is written
back through a new review.editComment command, one undo step per editing
session; the Comments pane uses the same path (it used to write the text
without an undo step) and gains a Reply button.

The margin edges on the horizontal ruler (left/right) and the vertical ruler
(top/bottom) can be dragged to set the section's margins through
layout.margins, one undo step per drag, snapped to 1/16 inch and clamped so
at least an inch of text remains. Indent markers keep priority where they
overlap an edge. Setting margins by number no longer switches off mirrored
margins; only a preset decides that.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:38:06 -04:00

4.6 KiB

Control protocol

Start the app with a control port: wordcraft --control 7981 (or WORDCRAFT_CONTROL_PORT=7981). It listens on 127.0.0.1 only. Send one JSON object per line, with the window's key (see Keys); each gets one reply line:

{"id": 1, "key": "…", "method": "engine.execute", "params": {"command": "text.insert", "params": {"text": "Hello"}}}
{"id": 1, "ok": true, "result": {"anchor": {...}, "focus": {...}}}

Errors come back as {"id": …, "ok": false, "error": "message"}. A failed command leaves the document unchanged. A line that isn't valid JSON gets one error reply, then the server closes the connection (so an HTTP request sent to the port can't run a command); reconnect to continue.

Keys

Any local program (and possibly a web page) can reach a loopback port, so every request needs the window's key. Once the port is bound, the app writes a random key (256 bits, as hex) to <settings>/control-key.<instance> and removes the file when the window closes; a new start writes a new key. <settings> is the folder that holds the preferences: on Linux $XDG_CONFIG_HOME/wordcraft (by default ~/.config/wordcraft), on macOS ~/Library/Application Support/WordCraft, on Windows %APPDATA%\WordCraft. <instance> is the Flatpak instance id inside Flatpak, and local-<port> otherwise. On Unix the file has mode 0600.

cat ~/.config/wordcraft/control-key.local-7981   # Linux, outside Flatpak, port 7981
  • Send the key as top-level "key" with every request, or once per connection with {"method": "auth", "key": "…"} (reply {"ok": true, "result": {}}); later requests on that connection may leave key out. Only auth reads params.key; for other methods params.key is an ordinary parameter (ui.key {"key": "Enter"} presses Enter).
  • The key is checked on every request. A missing or wrong key gets {"ok": false, "error": "unauthorized"}, and the app closes the connection.
  • A line over 1 MiB gets {"ok": false, "error": "line too long"}, and the app closes the connection.
  • A connection that sends no authorised request within 30 s of being accepted is closed. That is one limit: blank lines and bad JSON do not extend it. After an authorised request the connection may stay idle.

wordcraft-cli mcp --connect takes the key from WORDCRAFT_CONTROL_KEY, or else from the key file for the port it connects to, and reads it again on every new connection (the app may have restarted). It sends the key only to loopback addresses (127.0.0.0/8, ::1, localhost). For any other host it sends no key, says so, and the app refuses its requests: to drive an app on another machine, forward its port to 127.0.0.1 (for example ssh -L 7981:127.0.0.1:7981 host) and set WORDCRAFT_CONTROL_KEY to that app's key.

Methods

Method Params Result
engine.execute command, params the command's result (any id from engine.commands, including UI commands ui.tab, ui.dialog, ui.backstage, ui.zotero.* (see docs/zotero.md), ui.language (`{"value": "auto"
engine.commands — every command: id, label, location, shortcut, params, enabled
document.inspect text? blocks (text, style, runs, lists, tables), parts, sections, selection, pages
ui.inspect — UI state, view, dialog, window size, page rects on screen, caret, comment balloons (balloons: id + screen rect; balloon: the selected one), perf
ui.click x, y, button?, count?, shift?, cmd?, alt? real pointer input at window coordinates
ui.clickText page (0-based), x, y (points from the page's top-left), count? click inside a page
ui.move / ui.drag x,y / x,y,toX,toY,steps? pointer move / drag
ui.press / ui.release x,y, modifiers primary button down / up there (with ui.move between: a drag you can screenshot halfway)
ui.key key (B, Enter, Left…), shift?, alt?, cmd? key press through egui (shortcuts apply)
ui.text text typed text through egui
ui.screenshot path? PNG of the window
ui.render path, page? (1-based), scale? PNG of a page, rendered by the engine
ui.parity — feature-catalog parity
ui.resize / ui.focus / app.quit window control

Any other method name is treated as a command id: {"method": "format.bold"} works.

Positions are {"story": "body" | {"part": n}, "path": [block, row, cell, block, …], "off": byteOffset}.