Files
designcraft/docs/mcp.md
T
Gert Pellin 68596ea696 Refuse command params the description doesn't name
Callers from outside the app now get a parameter error for a key a
command's params description doesn't name, instead of the command
running with its defaults: `text.select {story, start, end}` and
`style.list {bogus: 1}` are refused with "unknown parameter `…`" and the
nearest or the accepted keys.

- engine: `cmd::params` (ported from VectorCraft #1442) reads the keys
  from each params description; `check_params` / `check_keys`.
  `Session::execute` itself stays lenient so old journals replay; in the
  engine's own tests it checks too, as the audit of the descriptions.
- MCP: tools/call answers -32602 for an argument a tool doesn't take and
  for params a command doesn't take (`execute`, every `batch` step before
  the first runs); the headless backend checks as well.
- Control channel (`engine.execute`) and `designcraft-cli run --cmd`.
- Descriptions now name every key their commands read (table target
  cells, anchored objects, gap.move, file.place, data merge, prefs.set,
  …); type.char / type.para stay unchecked (their attributes may be the
  params object itself; the setters refuse unknown attributes).
- Tests that passed keys the commands never read are fixed, and the pen
  tool no longer sends the unread `notifyPen`.

Fixes #776
2026-10-13 17:25:27 +02:00

7.6 KiB

MCP server

designcraft-cli mcp is a Model Context Protocol 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:

# 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:

{"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)
# 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)
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. Commands that create items take spread: a body spread index, or {"kind": "parent", "index": 0} for a parent spread (its index in parents of inspect_document); any other value is an error. 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}. A params key the command doesn't take is a JSON-RPC -32602 (invalid params) error naming it, as is an argument a tool doesn't take; batch checks every step before running the first
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)
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

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:

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"}

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.