Files
designcraft/docs/mcp.md
T
aiongg 18ae8e4761 Merge upstream main a184db6f8 with our 15 PRs (batch-0) into next
Conflicts resolved per file (both sides kept unless noted):
crates/engine/src/atomic.rs | 1 | main's: `write_file` removed (main routes command writes through `crate::io::write_file`); its one caller in file.rs now uses `crate::io::write_file`
crates/engine/src/cmd/datamerge/fill.rs | 1 | next's `Graphic` literal (has `clip` and `wrap`, which next's `Graphic` carries)
crates/engine/src/cmd/datamerge/mod.rs | 1 | main's `read_path_capped` (crate::io metadata/read_capped, roots checked); next's capped read is the same behaviour
crates/engine/src/cmd/datamerge/parse.rs | 1 | main's (`crate::io::metadata`, `crate::io::Metadata`)
crates/engine/src/cmd/edit.rs | 1 | next's `spread_param(p, "edit.paste")` (the second argument is the command id on both sides)
crates/engine/src/cmd/export.rs | 3 | main's: EPUB params doc, `toc_title` once (main's position; next's duplicate dropped)
crates/engine/src/cmd/file.rs | 7 | place doc: next's (lists page/rect/autoflow params #803 checks); next's recent-file functions + main's `foreign_document`; next's open (load_bytes, warnings, non-DesignCraft refusal) with main's checked `crate::io::read_file` for the bytes; main's `check_write` be
crates/engine/src/cmd/find.rs | 7 | main's Find scope (FindContext, scope/selection/story, continuation, UTF-8 repair) on next's `designcraft_compose::grep::Grep` (dialect GREP): find uses `find_all` capped at MAX_MATCHES, change uses `replacements`/`find_all` (literal change text not expanded), fin
crates/engine/src/cmd/fonts.rs | 1 | next's (`has_style`, resolved/replacement face); `has_style` already compares styles ignoring spacing and case, which is main's `same_style` fix; main's test kept
crates/engine/src/cmd/interchange.rs | 1 | next's `import_idml_report_with_previews` (previews for unreadable links)
crates/engine/src/cmd/layout.rs | 8 | pages.insert: main's overflow-safe `after` + next's checked `parent_ref`; main's duplicateSpread doc; layout.section: next's `SectionEdit` (already keeps unnamed settings, #717) with main's `d.repaginate()` moved into `SectionEdit::apply` (remove and set paths),
crates/engine/src/cmd/links.rs | 5 | next's links.list (no doc clone); main's `text_status` via crate::io; `use std::io::Read` dropped (unused after main's read_capped); test literals main's. `link_missing` (next) uses `crate::io::exists` for the file-guard scan
crates/engine/src/cmd/object.rs | 4 | object.fill/stroke tint-alone: main's (23e1680e7; out-of-range tint is an error, which main's test asserts); object.fit doc: next's (lists `axis`)
crates/engine/src/cmd/paths.rs | 1 | next's (shatai transform on outlines)
crates/engine/src/cmd/place_text.rs | 1 | next's `add_flow_frame` (used by autoflow)
crates/engine/src/cmd/preflight.rs | 2 | next's overset/unplacedStory issue (its `overset_location` already applies the parent transform main's #738 fix adds); next's two test modules kept with main's
crates/engine/src/cmd/scripts.rs | 1 | combined: next's `in_undo_step = false` (#406) + main's full rollback of a failed script (#709)
crates/engine/src/cmd/style.rs | 14 | param docs next's (gradient swatch.create, swatch stops, model colours); create_para next's; rename_style main's structure (rename entry, then `redirect_style_refs`), redirect has next's full reference set (cells, defaults, notes, TOC) + main's never-based-on-it
crates/engine/src/cmd/text.rs | 10 | main's selectSentence/Paragraph/Story + select_around; next's selectionAttrs (mixed) + main's openTypeFeatures/type.feature; main's comments; tracked typing: next's `reserve_command_text` + main's `insert_format_at`; empty-paragraph mark formatting main's (with `
crates/engine/src/cmd/thread.rs | 1 | next's `spread_param(p, "text.thread")`
crates/engine/src/dtd.rs | 1 | next's linear, depth-bounded `expand_parameter_entities` with main's budget (input + 1 MiB, `MAX_EXPANDED_BYTES`) and message ("expand the DTD beyond"); 16 MiB constant removed; both sides' tests pass on message
crates/engine/src/guard.rs | 2 | main's whole-transaction snapshot (docs, active, journal; #742) + next's `in_undo_step` restore; tests both
crates/engine/src/lib.rs | 5 | LoadedGraphic doc next's; next's `new_document_preview`; execute: main's `discard_stale_find_scope()` before next's nested-undo run; modules both (tests_autosize, tests_file_guard, tests_params, tests_roots)
crates/engine/src/recovery.rs | 1 | next's test (superset of main's assertion)
crates/engine/src/tests.rs | 4 | moved tests deduplicated (identical on both sides); next's preflight and direct-selection/kerning tests + main's style-delete, locked-frame and baseline-fit tests
crates/engine/src/tests_idml.rs | 3 | main's order (drop cap and margins tests identical on both sides, once each)
crates/ui-egui/src/canvas.rs | 3 | next's screen_options/DisplayKey block and tests kept; two tests that auto-merge had added twice (parent_guides_on_a_page_are_not_picked_there, baseline_grid_lines_are_the_ones_on_the_page) kept once; main's `typing`/`screen_at`/`click` helpers and its tests clicks
crates/ui-egui/src/control.rs | 3 | main's `count` check and pending budget (control queue + synthetic queue); exact-budget test runs both sides' cases (click 255 on 9,492 pending; drag 9492×255)
crates/ui-egui/src/cursors.rs | 1 | main's (same Pen cursor set)
crates/ui-egui/src/dialogs.rs | 14 | dialog body (newDocument … alert match) is next's text, with main's Find/Change scope reset (`set_field`, `reset_find_scope`, `find_change_params`) and main's Create Guides / Margins and Columns arms ported in; `Dialog::new` ruby section (next) + main's `set_fi
crates/ui-egui/src/i18n.rs | 7 | ka and pl modules, languages, statics and lookups both; `category_first` covers uk, ru, pl, ka and `count_label` keeps next's Czech plurals; shared TABLE union; next's duplicate rows for Auto, Number:, Gutter:, Top:, Bottom:, Inside:, Outside:, Left:, Right: dropped
crates/ui-egui/src/i18n/ar.rs | 2 | union of rows, no duplicate keys
crates/ui-egui/src/i18n/it.rs | 1 | union of rows, no duplicate keys
crates/ui-egui/src/i18n/ja.rs | 1 | union of rows, no duplicate keys
crates/ui-egui/src/i18n/pt_br.rs | 1 | union of rows, no duplicate keys
crates/ui-egui/src/i18n/uk.rs | 1 | union of rows, no duplicate keys
crates/ui-egui/src/icons.rs | 2 | both tests (main's typography icon, next's distribute icons)
crates/ui-egui/src/lib.rs | 11 | UiState: next's tabs_panel + main's last_color/last_gradient; `cursors` field once; next's SVG paste and paste-on-spread-in-view; next's extensionless document open; opens_as_document next's comment; tests both (next's control bar / key-name tests, main's rich black
crates/ui-egui/src/menus.rs | 15 | app.language doc next's text with pl and ka; UI_COMMANDS both (next's Tabs, Toggle Presentation, Apply None text; main's panel commands, Apply Color, Apply Gradient); Edit › Interface Language stays out (next moved it to Preferences); Type menu main's order with
crates/ui-egui/src/panels/glyphs.rs | 1 | next's (already has main's scroll-bar fix); the scroll-bar test module auto-merge had added twice kept once
crates/ui-egui/src/panels/layers.rs | 3 | next's drag-and-drop `object_rows`, with main's features ported into it: selection reveal (layer opens, row scrolls into view once), selected rows highlighted (also a group whose member is selected), double-click rename in place (`object.rename` queued like
crates/ui-egui/src/panels/mod.rs | 6 | tabs + typography modules and color_panel_tests; SelInfo has wrap_side/wrap_contour and wrap_offsets (offsets from the graphic's own wrap when it is selected); next's swatch_picker returning the chip response; next's content_selection_tests
crates/ui-egui/src/panels/pages.rs | 1 | next's (parents dragged by id)
crates/ui-egui/src/panels/properties.rs | 4 | next's swatch popup (Apply To, Tint); main's four wrap offsets then next's Wrap To / Contour Type; test fixtures main's (`..Default::default()`, no unused builder)
crates/ui-egui/src/panels/swatches.rs | 3 | main's Delete Swatch button + next's New Swatch comment; next's draggable rows; click: main's PICKED + fill/stroke in front, with next's no-apply on double-click
crates/ui-egui/src/toolbar.rs | 2 | main's Apply Color / Gradient / None wells and functions; next's apply_none doc line
crates/compose/src/lib.rs | 5 | next's line loop kept (mark_leading + last_line_mark, wrap segments/carry, grid gyoudori); main's mark-chars base_leading block (auto-merged) stays beside it. `fits`: next's #106 rule (baseline fit for horizontal frames, full descent for paths, vertical text, footnote
crates/compose/src/tests.rs | 5 | next's tests (tombstone set, sliver/one-sided/contour/clipping wrap tests, borders, lists, kenten, grid, anchored wrap, balance, shaped frames) plus main's an_empty_last_paragraph_isnt_overset; main's wrap_object_beside_one_narrow_column_leaves_the_other_alone re-ad
crates/doc/src/assets.rs | 1 | main's doc comment (same meaning)
crates/doc/src/build.rs | 3 | parent_page_for: next's parent_page_for_side (RTL-aware); single_sided_page_shows_first_parent_page test kept once, at main's position
crates/doc/src/item.rs | 1 | next's rotated_group_bounds_follow_the_children_outlines test kept
crates/doc/src/lib.rs | 1 | next's kerning_increment field kept; main's "Bulleted & Numbered" wording
crates/doc/src/page.rs | 1 | next's outer_page_indices + parent_page_for_side (integration of parent-margins with rtl-master-sides); main's parent_page_index dropped, its only caller (build.rs) uses parent_page_for_side
crates/doc/src/slice.rs | 5 | extract: next's `marked()` (get-based, fix_notes/fix_endnotes/fix_marks; main's version indexed slices and could panic); insert_story: main's take() limit on endnote/editorial payload copies with next's id comment, redundant `let ne`/`let nn` dropped; main's excess_note
crates/geom/src/corners.rs | 1 | next's box_outline_rounds_only_closed_corners test kept
crates/render/src/damage.rs | 1 | next's wrapping-anchored-object damage test kept
crates/render/src/fx.rs | 2 | feather: next's choke/corners sigma+join inside main's structure (body and clip at device resolution, with_filters(.., None)); inner glow centre: next's blend `mix` with main's clip argument (bp, xf)
crates/render/src/lib.rs | 3 | next's layer comment; next's front baseline grid draw after items; gradient match on g.kind (main's tuple scrutinee did not match the single-pattern arms)
crates/render/src/text.rs | 5 | paint_glyphs: main's paint_stroke closure structure with next's stroke overprint (overprinting helper, stroke `(TextPaint, bool)`); callers pass next's overprint_fill/overprint_stroke
crates/render/tests/placed_pdf.rs | 1 | main's `near` helper version
crates/tools/src/lib.rs | 1 | main's (PenConvert already declared below, avoids a duplicate variant)
crates/tools/src/pen.rs | 1 | main's module doc
crates/tools/src/select.rs | 1 | next's Drag::Corner variant (live corners) kept
crates/tools/src/tests.rs | 4 | next's tests (content drag, live corners, table border drag, eyedropper matchFrom, fitting handles, pen cursors once); eyedropper_ignores_inactive keeps next's samples_image check (main's exact-JSON compare lacks matchFrom) with main's `..Default::default()` Asset lit
crates/tools/src/text.rs | 2 | next's table border drag (BorderDrag, border_at) kept, then main's comment on locked text frames
crates/tools/src/xform.rs | 2 | main's explicit `Vec<&Item>` for shown children; next's eyedropper matchFrom (integration 796e9d713)
Cargo.toml | 2 | main's craft-pdf workspace dep added (craft-print stays once, in next's craft-libs block); [patch.crates-io] keeps next's winit (rust-windowing #4694 backport) and adds main's krilla patch; main's skylartaylor winit left out (see UNSURE 1)
Cargo.lock | 1 | union: next's craft-atomic and craft-cms entries plus main's craft-pdf entry; needs `cargo` normalisation
README.md | 1 | Open bullet combines INDD import via indd-utils (next) and editable PowerPoint export (main); main's paragraph break kept
contributors/people.toml | 1 | both people kept (switch87 from next, Nmarello from main)
apps/designcraft-cli/src/main.rs | 4 | next's `Export` enum dispatch kept; main's .pptx export added as `Export::Pptx` (run prints slides and warnings, script runs file.exportPptx); main's `--pdf-options` null-as-{} parse; usage lines list .pptx
apps/designcraft/src/main.rs | 3 | `startup.leave()` right after `app.ui` (main #380) and next's macOS `sync_movable` kept after the IME discard; next's Apple Events install kept; main's macOS Dock Quit `with_close_requested_on_terminate` block left out (needs the skylartaylor winit; UNSURE 1)
crates/fonts/src/fontdb.rs | 9 | FONT6 aliases (next) combined with #911 WWS names (main): ScannedFace and both CatalogEntry constructors carry `wws` and `aliases`; `canonical_family` falls back to WWS family names; `exact_style_traits` canonicalises then tries typographic, then WWS names (`named_st
crates/format/src/lib.rs | 1 | tests: both sides kept (next's r7a nullable-default tests; main's oversized ZIP entry/asset/entry-count tests)
crates/idml/src/import.rs | 11 | main's bounded `Package` reader under next's `import_idml_report_with_previews` (metadata read through `pkg.part`); next's `tab_list_repaired` kept; ruler origin: next's `settings.ruler_origin`, and main's guide import reads it (main's private `RulerOrigin` enum and
crates/idml/src/tests.rs | 6 | item-level union: all next tests plus main's new ones (ruler guides, package limits, CJK leading model, liquid page rules module, mojikumi); no test dropped
crates/mcp/src/backend.rs | 1 | test comment merged from both
crates/pdf/src/tests.rs | 1 | tests: both sides kept
crates/pdf/src/text.rs | 4 | next's (skew `tan`, cluster `start`, ruby overprint warning); main had no change here beyond base
crates/textimport/src/docx.rs | 5 | next's (ListCounter integration for list levels, page-break test position); main's side was the older form of the same code
crates/epub/src/lib.rs | 30 | non-test code is next's (StoryWriter holds img_src/images/no_alt/heading_ids; covers main's #787/#791/#782 fixes, plus #314 group transforms, anchored images in images_without_alt, endnote story skipped in the TOC); tests: next's set plus main's extra css_weight_style a
docs/control-protocol.md | 2 | next's rows (ui.paste, count 1–255, documentGrid) and wording
docs/file-format-parity.md | 1 | rows combined (next's ICC notes, main's TIFF indexed colour)
docs/gaps.md | 2 | status change notes unioned; Flex Layout removed from missing (main implemented it), next's Parent Options sentence kept
docs/localization-parity.md | 4 | Polish and Georgian rows; 5 languages beyond the twelve; file list includes cs and ka; both history rows
docs/roadmap.md | 1 | M1 next's (#186 fixed on next), M2 main's IME wording
docs/target-app-parity.md | 6 | next's status line; File formats row next's, UI/UX row main's (IME no longer failing); UI line next's plus main's Swatches detail; one Page tool line (main's fuller one); Load styles and main's fuller Pathfinder line both kept, main's duplicate short Pathfinder line r
docs/ui-parity.md | 1 | IME row main's, Font menu row next's

i18n/fr.rs: five rows dropped that main's shared table now has (Row, Open Path, Flex Layout, Current Page Number, Rulers).
2026-10-16 03:09:22 +00:00

14 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

Confining file access

designcraft-cli mcp --automation-read-root /work/project --automation-write-root /work/project/out
{"mcpServers": {"designcraft": {"command": "designcraft-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, file.open, file.openIdml, place_image and file.place / place.load / media.options with a path, links.relink, links.relinkFolder, color.loadProfile, swatch.load, library.open, book.open, data.source.select, data.merge and file.importXml / xml.loadDtd with a path, and file.recovery.list / file.recovery.open with a dir. A DesignCraft document names more files than the agent does: linked images, data sources, book chapters, the Document Fonts folder beside a document, an IDML package's linked images and the files an image cell of a merge names. Those are read through the same checks, so a path in a document that leads outside the read root reads as a file that isn't there.
  • Every file a command writes must lie inside the write root: save_document, export_png, render_page with a path, file.save, file.saveAs, file.saveACopy, file.exportPdf, file.exportEpub, file.exportFixedEpub, file.exportHtml, file.exportText, file.exportXml, file.exportIdml, file.printBooklet, snippet.export, swatch.save, library.new and the library's saves, book.new, book.add, book.paginate, book.syncStyles, book.exportPdf, file.package, links.copyTo and file.recovery.save / file.recovery.open with a dir (reopening removes the entries it read). file.package and links.copyTo check their folder before they create anything.
  • 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.designcraft or automation filesystem access is not granted: write authority is absent: /work/project/out/x.designcraft.
  • 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 in-process server, so --connect together with them is an error: the server would otherwise talk to an app that isn't confined. DesignCraft's desktop app has no equivalent flags yet, so a running app (designcraft --control) is not confined. --sample is the operator's own choice and is opened before the roots apply.

What the roots do not cover: commands that take no path still read and change the open document, and inspect_document returns it. The system font scan, the installed fonts that file.package copies, and the app's own recovery folder are fixed paths, not agent input, and are not checked. A Document Fonts folder and each font file in it are checked before they are opened, so a link that leads out of the read root is skipped with a note. file.print hands a PDF to the system's print spooler, which is not a path the agent names. Anything that reads or writes a path goes through one place in the engine (the io module). The crates below the engine have no file access of their own: the IDML import and the font folders take a reader or an access check from the engine, and ICC profiles arrive as bytes. A test scans the source of every crate for the file system, however it is imported or spelled, and lists the few files it excludes and why.

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.

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.

Text and image fitting

Hyphenation is on by default for new text (hyphenate: true). To turn it off, select the text or a text frame, then call execute:

execute          {"command": "type.para", "params": {"attrs": {"hyphenate": false}}}

With a text selection, this formats the selected paragraphs; with selected text frames, it formats their whole stories.

Use execute with object.fit to fit a selected graphic frame (or pass ids):

mode What it does
fillProportionally Preserves aspect ratio, covers the frame and clips overflow.
fitProportionally Preserves aspect ratio, shows the whole image and may leave empty space.
fitContentToFrame Stretches the content to the frame's width and height.
centerContent Keeps the current size and positions content at the fitting alignment (center by default).
fitFrameToContent Resizes the frame to the content's bounds.

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)
ime Input method events at the Type tool's caret: preedit (marked text in place, active = converting clause in characters), commit (typed as one step)
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.