mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Do not resend a request after a transport failure. Report the uncertain outcome and reconnect on the next explicit request. Validation: all six cargo xtask ci gates passed on the merge candidate; 1,177 native tests passed and all 15 WASM packages checked. Lost-reply, invalid UTF-8 and timeout probes reproduced duplicate edits before the fix and one edit afterward. Task: #514
123 lines
7.2 KiB
Markdown
123 lines
7.2 KiB
Markdown
# MCP server
|
|
|
|
`designcraft-cli mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server that lets Claude and
|
|
other agents drive DesignCraft: create documents, lay out frames and text, apply styles, place images, and look at
|
|
the result as PNG images. It speaks newline-delimited JSON-RPC 2.0 on stdin/stdout (protocol `2025-06-18`, also
|
|
`2025-03-26` and `2024-11-05`); logs go to stderr. The implementation is the `designcraft-mcp` crate.
|
|
|
|
## Register it
|
|
|
|
Build the CLI (`cargo build --release -p designcraft-cli`), then:
|
|
|
|
```sh
|
|
# Headless: an in-process engine, no window
|
|
claude mcp add designcraft -- /path/to/target/release/designcraft-cli mcp
|
|
|
|
# Drive the running desktop app (start it first with a control port)
|
|
designcraft --control 7979
|
|
claude mcp add designcraft-app -- /path/to/target/release/designcraft-cli mcp --connect 7979
|
|
```
|
|
|
|
Other MCP clients use the same command line, e.g. in a JSON config:
|
|
|
|
```json
|
|
{"mcpServers": {"designcraft": {"command": "/path/to/designcraft-cli", "args": ["mcp"]}}}
|
|
```
|
|
|
|
### From an installed release
|
|
|
|
The release packages ship `designcraft-cli` alongside the desktop app, so no build is needed:
|
|
|
|
| Install | CLI |
|
|
|---|---|
|
|
| Windows (MSI) | `C:\Program Files\DesignCraft\designcraft-cli.exe` by default (wherever you installed it otherwise), not on `PATH` |
|
|
| Linux (deb, rpm) | `/usr/bin/designcraft-cli` |
|
|
| macOS | the separate `designcraft-cli-<version>-macos-<arch>.zip` release asset (the `.app` holds only the desktop app) |
|
|
|
|
```sh
|
|
# Windows, default install folder
|
|
claude mcp add designcraft -- "C:\Program Files\DesignCraft\designcraft-cli.exe" mcp
|
|
# Linux, or macOS with the CLI unzipped onto PATH
|
|
claude mcp add designcraft -- designcraft-cli mcp
|
|
```
|
|
|
|
## Modes
|
|
|
|
| | Headless (`mcp`) | Connected (`mcp --connect PORT` or `HOST:PORT`) |
|
|
|---|---|---|
|
|
| Engine | In-process `designcraft_engine::Session`, starts with an empty Letter document (`--sample` opens the sample magazine) | The app's session, through its loopback control channel ([control-protocol.md](control-protocol.md)) |
|
|
| Rendering | `designcraft-render` (CPU) | The app's renderer |
|
|
| Window tools (`screenshot`, `click`, `drag`, `menu_list`, `ui_inspect`, `ui_set`, `dialog_*`) | Return an error explaining how to connect | Work |
|
|
| UI-only commands via `execute` (`view.*`, `window.*`, `help.*`, `edit.dynamicSpelling`, and `app.*` except `app.links`) | Error | Work |
|
|
|
|
In connected mode every tool maps to a control-channel method (`engine.execute`, `ui.render`, `ui.pointer`, …), so
|
|
what the agent does shows up live in the window. A failed request is not resent: the operation may have completed,
|
|
so inspect the document before retrying. The next request re-establishes the connection.
|
|
|
|
## Coordinates
|
|
|
|
Points (1/72 in), y down, in **spread** space: on a single-page spread the page's top-left is (0, 0); on a facing
|
|
spread the right page starts at x = page width. `inspect_document` lists page bounds per spread. Page indices in
|
|
`render_page` / `export_png` are 0-based. `pointer` events in connected mode may use `"space": "screen"` (egui points,
|
|
see `ui_inspect` → `canvasRect`).
|
|
|
|
## Text offsets
|
|
|
|
Positions in a story's text are **UTF-8 byte offsets**, not character counts: `anchor` / `focus` of `text.select`,
|
|
`start` / `end` of `story.replaceRange` and of `find.find` matches, the `pos` that `text.placeCaret`, `text.insert`
|
|
and `text.move` return, a story's `length` in `get_story` and `inspect_document`, and its `overset` position in
|
|
`get_story`. A character outside ASCII takes 2 to 4 bytes (é 2, — and は 3), so the story `はただ商店` has length 15
|
|
and its second character starts at 3. An offset inside a character snaps back to the start of that character.
|
|
|
|
## Tools
|
|
|
|
| Tool | What it does |
|
|
|---|---|
|
|
| `list_commands` | Every command (id, label, menu path, shortcut, params, enabled), with `filter` / `enabledOnly` |
|
|
| `execute` | Run any command: `{command, params}` |
|
|
| `batch` | Run `commands: [{command, params}]` (or `script` text, one `command.id {json}` per line) in order, stopping at the first error (reports `failedIndex`); `"$N.path"` parameters use earlier results ([agents.md](agents.md)) |
|
|
| `inspect_document` | Pages, spreads, items (ids, bounds, fill, story), stories (overset), layers, styles, swatches, selection |
|
|
| `get_story` | Text, frames, paragraphs, lines, overset of a story (`story` or text `frame`) |
|
|
| `set_story_text` | Replace a story's text (`\n` = new paragraph) |
|
|
| `new_document` | `file.new` (preset, width, height, pages, facingPages, columns, gutter, margins, bleed, title) or `sample: true` |
|
|
| `open_document` / `save_document` | Native `.designcraft` files |
|
|
| `place_image` | Image from `path` or `base64` into `frame`, the selected frame, or a new frame at x, y, width |
|
|
| `render_page` | Render a page → MCP image content (PNG, base64); optional `path` to also save it |
|
|
| `export_png` | Write a page as PNG (or JPEG by extension) |
|
|
| `screenshot` | Whole app window → image content (connected only; captured to a temp file and read back) |
|
|
| `select_tool` | Activate a tool (`selection`, `type`, `rectangleFrame`, `line`, …) |
|
|
| `pointer` | Mouse gesture events (`down`/`drag`/`up`/`move`/`doubleclick`) for the active tool, optional `tool` |
|
|
| `key` | Key press with modifiers (headless: active tool → command shortcuts → tool shortcuts) |
|
|
| `type_text` | Type at the text insertion point (or into the focused dialog field in the app) |
|
|
| `click` / `drag` | Real pointer input at screen points (connected only) |
|
|
| `menu_list`, `ui_inspect`, `ui_set` | Menu tree, UI state, change UI state (connected only) |
|
|
| `dialog_set` / `dialog_confirm` / `dialog_cancel` | Fill in and close the open dialog (connected only) |
|
|
|
|
Tool failures come back as results with `isError: true` and a message the model can act on; protocol errors use
|
|
JSON-RPC error codes. Resources: `designcraft://document` (document.inspect) and `designcraft://commands`.
|
|
|
|
## Example
|
|
|
|
```text
|
|
new_document {"preset": "A4", "margins": 36}
|
|
execute {"command": "frame.create", "params": {"rect": [36, 36, 559, 200], "content": "text", "text": "Hello"}}
|
|
→ {"id": 8, "story": 9}
|
|
set_story_text {"story": 9, "text": "Summer Fair\nSaturday in the park"}
|
|
execute {"command": "type.char", "params": {"size": 28}}
|
|
place_image {"path": "/tmp/photo.jpg", "x": 36, "y": 220, "width": 523}
|
|
render_page {"page": 0} → PNG image
|
|
```
|
|
|
|
Or with the Type tool, the way a user would:
|
|
|
|
```text
|
|
pointer {"tool": "type", "events": [{"kind": "down", "x": 72, "y": 72}, {"kind": "drag", "x": 300, "y": 200}, {"kind": "up", "x": 300, "y": 200}]}
|
|
type_text {"text": "Hello"}
|
|
key {"key": "Enter"}
|
|
key {"key": "Escape"}
|
|
```
|
|
|
|
## Community links
|
|
|
|
`app.links` returns the ArtCraft Discord (https://discord.gg/artcraft), the website (https://getartcraft.com), DesignCraft's app page (https://getartcraft.com/apps/designcraft), its GitHub repository (https://github.com/storytold/designcraft) and the issue tracker. In the running app, the UI commands `help.discord`, `help.appPage`, `help.github`, `help.issues`, `help.website`, `help.app {app}` open them in the browser and `help.about {tab?}` shows the About window (tabs `about`, `contributors`, `models`). The server's `serverInfo.websiteUrl` is the app page.
|