mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-29 12:57:09 -06:00
* Engine: check automation read and write roots on every path a command reads or writes (#769) * MCP: confine the headless session to the automation roots (#769) * CLI: add --automation-read-root and --automation-write-root to mcp (#769) * Docs: document --automation-read-root and --automation-write-root for mcp (#769) * Review fixes for #798: confine file.printPdf; guard all of engine/src - print.rs::print_pdf_to_file (file.printPdf, added on main) wrote with std::fs::write directly, so a confined agent could print a PDF anywhere. It now goes through io::write_file, which checks the automation write root. - The guard test now walks every file under crates/engine/src (not only cmd/), except io.rs, file_access.rs and the tests*.rs modules, and also flags OpenOptions. - Tests: an engine test that file.printPdf is refused outside the write root, works inside and is unchanged without roots; the MCP integration test covers it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review fixes for #798: list file.printPdf among the confined writes in docs/mcp.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Font files added by path go through io, inside the automation read root (#769) --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
5.8 KiB
Markdown
100 lines
5.8 KiB
Markdown
# MCP server
|
|
|
|
WordCraft speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio, so
|
|
Claude and other agents can write and edit documents.
|
|
|
|
```sh
|
|
wordcraft-cli mcp # headless session (no window)
|
|
wordcraft --control 7981 & # or: drive the running app…
|
|
wordcraft-cli mcp --connect 127.0.0.1:7981 # …including clicks, keys and screenshots
|
|
```
|
|
|
|
Claude Code: `claude mcp add wordcraft -- wordcraft-cli mcp`.
|
|
|
|
With `--connect`, the bridge sends the app's control key, which it reads from
|
|
`WORDCRAFT_CONTROL_KEY` or from the key file the app writes at start. It sends the key only to
|
|
loopback addresses; see [Keys](control-protocol.md#keys).
|
|
|
|
## Tracked changes by an agent
|
|
|
|
Tracked changes and comments are recorded under the session's user name ("WordCraft User" by
|
|
default). Name the agent so reviewers can tell its edits from people's: start the headless server
|
|
with `wordcraft-cli mcp --author "Claude (copyedit)"`, or run
|
|
`file.setAuthor {"name": "Claude (copyedit)"}`. Turn tracking on explicitly with
|
|
`review.trackChanges {"value": true}`; without `value` it toggles, which turns tracking *off* in
|
|
a document that was saved with it on. `wordcraft-cli run` takes `--author` too.
|
|
|
|
## Confining file access
|
|
|
|
```sh
|
|
wordcraft-cli mcp --automation-read-root /work/project --automation-write-root /work/project/out
|
|
```
|
|
|
|
```json
|
|
{"mcpServers": {"wordcraft": {"command": "/abs/path/target/release/wordcraft-cli",
|
|
"args": ["mcp", "--automation-read-root", "/work/project", "--automation-write-root", "/work/project/out"]}}}
|
|
```
|
|
|
|
The two flags (VectorCraft, PhotoCraft and the other Craft apps take the same ones, so one client
|
|
config works across the suite) keep the agent to the folders you give it:
|
|
|
|
- Every file a command reads must lie inside the **read root**: `open_document` and `file.open`,
|
|
`file.newFromTemplate`, `insert.textFromFile` and `insert.object`, `insert.picture` and
|
|
`picture.change` with a `path`, `mailings.recipients` with a `path` (csv, tsv, txt, xlsx, xlsm,
|
|
ods), `review.compare` (`path` and `original`), the templates and documents `tools.updateStyles`,
|
|
`styles.organizer` and `styles.copyFrom` read. The pictures an opened HTML file names are read
|
|
from beside that file, so they are inside the read root too (a link that leads out is not followed).
|
|
- Every file a command writes must lie inside the **write root**: `save_document`, `file.save`,
|
|
`file.saveAs`, `file.exportPdf`, `file.exportPng`, `file.printPdf`, `file.saveTemplate`, and `mailings.finish` with
|
|
a `path`.
|
|
- Read and write access are separate. Give the same folder twice for both; a root left out grants
|
|
none of its access (with only `--automation-read-root`, nothing can be written to disk). Without
|
|
either flag nothing changes.
|
|
- A path is made absolute against the server's working directory, then its links are followed: a
|
|
file (or folder) link that leads out of a root is outside it. For a file that doesn't exist yet,
|
|
its nearest existing folder's links are followed and the rest must be plain names, so `..` below a
|
|
folder that doesn't exist is refused, as is a link that leads nowhere (writing through it would
|
|
create its target). The result is compared with the root by whole folder names: `/work2/x` is not
|
|
inside `/work`. On Windows case is ignored, UNC paths (`\\server\share\...`) are resolved like
|
|
drive-letter ones, and device names (`CON`, `NUL`, `COM1`...) are refused.
|
|
- A refused path is an ordinary tool error the agent can read and correct, for example
|
|
`automation path rejected: outside the write root /work/project/out: /etc/x.docx` or
|
|
`automation filesystem access is not granted: write authority is absent: /work/project/out/x.docx`.
|
|
- The roots must be existing folders; a missing one stops the server with an error. Each flag is
|
|
given once (`--flag <dir>` or `--flag=<dir>`).
|
|
|
|
The flags confine the headless server; `--connect` with them is an error, since the server would
|
|
otherwise talk to an app that isn't confined. WordCraft's desktop app has no equivalent flags yet,
|
|
so a running app (`wordcraft --control`) is not confined.
|
|
|
|
What the roots do not cover: commands that take no path (`text.insert`, `execute` of anything that
|
|
works on the open document) can still read and change that document, and `get_text` and
|
|
`inspect_document` return it. The Zotero commands talk to a local Zotero over a socket, not to files.
|
|
Anything that reads or writes a path goes through one place in the engine (`io::open_path_with`,
|
|
`io::save_path_with`, `io::read_file`, `io::write_file`); a test keeps command modules from
|
|
touching the file system on their own.
|
|
|
|
The checks hold for every thread of the server process. A path is checked, then opened: another
|
|
program that swaps a link in between can still win that race, so the roots keep an agent to the
|
|
files it was given rather than guarding against other software on the computer.
|
|
|
|
## Tools
|
|
|
|
| Tool | What it does |
|
|
|---|---|
|
|
| `list_commands` | every command (filter with `query`) — over 270 of them |
|
|
| `execute` | run one command: `{command: "insert.table", params: {rows: 3, cols: 4}}` |
|
|
| `batch` | run several commands in order |
|
|
| `new_document` | blank, sample, letter, resume, report |
|
|
| `open_document` / `save_document` | docx, doc (open only), odt, rtf, html, md, tex, txt, json; save also pdf, png |
|
|
| `type_text` | type at the caret (`paragraphs: true` splits lines into paragraphs) |
|
|
| `select_text` | select the n-th occurrence of some text |
|
|
| `get_text` / `inspect_document` | read the document back (verify without screenshots) |
|
|
| `render_page` | a page as PNG |
|
|
| `parity` | feature coverage |
|
|
| `screenshot`, `click`, `key`, `ui_inspect` | app only (`--connect`) |
|
|
|
|
Resources: `wordcraft://document` (inspect) and `wordcraft://commands`.
|
|
|
|
The acceptance test `crates/mcp/src/tests.rs` writes a formatted document using MCP only.
|