mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
The Appearance panel names the style a linked object uses and its menu offers Redefine Graphic Style "name", which updates every object still linked (edited objects keep their look and are unlinked). The Graphic Styles panel breaks links (button and menu), selects all unused styles, sorts by name, deletes several styles, and opens Graphic Style Options (rename; links follow) on double-click; New Graphic Style from the menu or with Alt asks for a name. Select > Same > Graphic Style and Appearance Attribute work, and Document Info lists the style names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
216 lines
15 KiB
Markdown
216 lines
15 KiB
Markdown
# VectorCraft MCP server
|
||
|
||
`vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio
|
||
(newline-delimited JSON-RPC 2.0, protocol `2025-06-18`; `2025-03-26` and `2024-11-05` also accepted). Agents use it to
|
||
draw, inspect and look at VectorCraft documents.
|
||
|
||
It has two backends:
|
||
|
||
| Mode | How | What works |
|
||
|---|---|---|
|
||
| **Remote** | `vectorcraft-cli mcp --connect 127.0.0.1:7979` (the app must run with `vectorcraft --control 7979`) | Everything. Tool calls are forwarded over the [control protocol](control-protocol.md), so you watch the app change live. |
|
||
| **Headless** | `vectorcraft-cli mcp --headless` | An in-process engine session with a CPU renderer. Everything except the UI-only tools (`inspect_ui`, `type_text`, `open_panel`, `screenshot {window:true}`). |
|
||
|
||
With no flag, the server tries `127.0.0.1:7979` and falls back to headless. Logs go to stderr; stdout carries
|
||
only protocol messages.
|
||
|
||
## Build and register
|
||
|
||
```sh
|
||
cargo build --release -p vectorcraft-cli
|
||
claude mcp add vectorcraft -- "$PWD/target/release/vectorcraft-cli" mcp
|
||
# or pin a mode:
|
||
claude mcp add vectorcraft-headless -- "$PWD/target/release/vectorcraft-cli" mcp --headless
|
||
claude mcp add vectorcraft-app -- "$PWD/target/release/vectorcraft-cli" mcp --connect 127.0.0.1:7979
|
||
```
|
||
|
||
Other clients use the same command in their JSON config:
|
||
|
||
```json
|
||
{"mcpServers": {"vectorcraft": {"command": "/abs/path/target/release/vectorcraft-cli", "args": ["mcp"]}}}
|
||
```
|
||
|
||
For a live session, start the app first: `cargo run --release -p vectorcraft -- --control 7979`.
|
||
|
||
## Tools
|
||
|
||
Coordinates are points in document space: y points down, the origin is the first artboard's top-left, and a new
|
||
document is 612 × 792 (US Letter). New objects become the selection. Most commands act on the selection or on
|
||
explicit `ids`.
|
||
|
||
Paint values (`fill`, `stroke`) accept `"#rrggbb"`, `"none"`, `[r,g,b]` (0..1), `{"c","m","y","k"}`, `{"gray"}`,
|
||
or a full `paint.setFill` params object (`{"gradient": …}`, `{"swatch": "name"}`).
|
||
The Fill/Stroke proxy commands run through `run_command`: `paint.invert` and `paint.complement` recolour the active
|
||
proxy keeping each colour's model, `paint.lastColor` / `paint.lastGradient` re-apply the last solid colour or gradient,
|
||
and `paint.recent` returns the recent colours that every paint command (and the eyedropper) feeds.
|
||
`paint.proxies` returns what the proxies show: the fill and stroke, which one is active, and whether the selected
|
||
objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" proxy).
|
||
|
||
| Tool | Arguments | Notes |
|
||
|---|---|---|
|
||
| `list_commands` | `{filter?, enabledOnly?}` | The command catalogue: id, label, menu, shortcut, params doc, enablement. |
|
||
| `run_command` | `{command, params?}` | Runs any command. Use it for everything without a dedicated tool. |
|
||
| `inspect_document` | `{}` | Artboards, layer tree (ids, kinds, bounds, paint), selection, history, tool. |
|
||
| `inspect_ui` | `{}` | UI state. Remote mode only. |
|
||
| `select_tool` | `{tool}` | `selection`, `directSelection`, `pen`, `rectangle`, `ellipse`, `polygon`, `star`, `lineSegment`, … |
|
||
| `pointer_gesture` | `{events:[{kind,x,y,mods?}], tool?, mods?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. Events go through the same path as the mouse. |
|
||
| `draw_path` | `{points \| d, closed?, fill?, stroke?, strokeWidth?}` | `points` is `[[x,y],…]` or `[{x,y,in?,out?,smooth?},…]`. `d` is SVG path data. |
|
||
| `draw_shape` | `{shape, …geometry, fill?, stroke?, strokeWidth?}` | `rectangle`/`ellipse`: `x,y,width,height` (plus `radius` for corners). `polygon`: `cx,cy,radius,sides`. `star`: `cx,cy,radius1,radius2,points`. `line`: `x1,y1,x2,y2`. |
|
||
| `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. |
|
||
| `press_key` | `{key, mods?}` | Remote: a real key event. Headless: runs the command or tool bound to that shortcut, or sends the key to the busy tool. |
|
||
| `type_text` | `{text}` | Remote only. |
|
||
| `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. |
|
||
| `open_panel` | `{panel}` | Remote only. |
|
||
| `screenshot` | `{path?, scale?, artboard?, window?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard; `window:true` captures the app window (remote only). |
|
||
| `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, PNG, JPEG, GIF, WebP, TIFF, BMP (an image opens as a document of its pixel size). Templates open as a new untitled document. `run_command document.formats` lists the formats. |
|
||
| `save_file` | `{path?}` | Saves in the native `.vectorcraft` format. |
|
||
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `pdf`, `png`, `jpg`, `webp` or `vectorcraft` (the list comes from `document.formats`). When `format` is omitted, it comes from the path's extension. PDF writes one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based); the other formats write one artboard. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds; `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
|
||
| `add_text` | `{text, x?, y?, width?, height?, path?, mode?, pathEffect?, size?, font?, color?}` | Point type at (x, y); area type with `width`/`height`; or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`). |
|
||
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
|
||
| `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group. |
|
||
| `transform` | `{ids?, dx?, dy?, rotate?, scale?, scaleX?, scaleY?, reflect?, shear?, origin?, copy?}` | Runs move, rotate, scale, reflect, shear in that order. With `copy`, the first step duplicates. |
|
||
| `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | Nine graph types: column, stacked column, bar, stacked bar, line, area, scatter, pie and radar. Edit later with `graph.setData` / `graph.setType` via `run_command`. |
|
||
| `text_wrap` | `{ids?, offset?, invert?, release?}` | Area type below the objects (same layer) flows around them. |
|
||
| `undo` / `redo` | `{}` | |
|
||
|
||
Appearance stacks: an object can carry several fills and strokes (`appearance.addFill`, `appearance.addStroke`), indexed
|
||
in paint order (0 is painted first, the bottom row of the Appearance panel). `paint.setFill`, `paint.setStroke`,
|
||
`stroke.set`, `stroke.setAdvanced`, `paint.editGradient`, `paint.setGradientGeom` and `transparency.set` take
|
||
`item` to edit one of them; `run_command appearance.setActiveItem {"index": n}` makes that row the target of later
|
||
calls that omit `item` (as clicking the row in the Appearance panel does) until the selection changes; the proxy of
|
||
its kind (`paint.proxies`) and the Gradient tool's annotator then show that row.
|
||
`inspect_document` reports it as `paint.appearanceItem`. Live effects take the same `item` to apply to one fill or
|
||
stroke instead of the whole object: `run_command effect.apply {"effect": "path.offsetPath", "item": 0}`, and
|
||
`effect.remove`, `effect.setParams` (`visible` toggles one) and `effect.duplicate` address that item's effects;
|
||
`apply_effect` uses the active item. `effect.list` reports each object's item effects under `applied[].items`.
|
||
|
||
Errors (unknown tool, bad arguments, a disabled or failing command) come back as a normal result with
|
||
`isError: true` and a message. The model can read the message and retry.
|
||
|
||
Swatches go through `run_command`: `swatch.list` lists every swatch with its colour group, kind and colour;
|
||
`swatch.edit {name, newName?, color?, mode?, global?, spot?}` edits one (fills, strokes and text linked to a
|
||
global or spot swatch follow its colour and name, as one undo step); `swatch.delete {names, unlink?}` deletes
|
||
swatches and colour groups in one undo step (art using a deleted global swatch keeps its colour, unlinked).
|
||
`swatch.new {color?, mode?, spot?, global?, group?}` saves a colour (into a colour group with `group`), gradient
|
||
or pattern; `swatch.newGroup {fromArtwork: true, toGlobal?, includeTints?}` makes a colour group of the selected
|
||
art's colours (by default as global swatches the art links to).
|
||
|
||
## Resources
|
||
|
||
| URI | Content |
|
||
|---|---|
|
||
| `vectorcraft://document` | `document.inspect` summary (JSON) |
|
||
| `vectorcraft://document/json` | The complete document model (JSON) |
|
||
|
||
## Examples
|
||
|
||
Draw, look, export:
|
||
|
||
```json
|
||
{"name":"draw_shape","arguments":{"shape":"rectangle","x":72,"y":72,"width":200,"height":120,"radius":12,"fill":"#1e88e5","stroke":"none"}}
|
||
{"name":"draw_shape","arguments":{"shape":"star","cx":400,"cy":300,"radius1":90,"radius2":40,"points":5,"fill":"#ffc107","stroke":"#000000","strokeWidth":2}}
|
||
{"name":"draw_path","arguments":{"d":"M72 400 C 150 300 250 500 330 400","stroke":"#e53935","strokeWidth":4,"fill":"none"}}
|
||
{"name":"screenshot","arguments":{"scale":0.5}}
|
||
{"name":"export","arguments":{"path":"/tmp/art.svg"}}
|
||
```
|
||
|
||
Long-tail commands:
|
||
|
||
```json
|
||
{"name":"list_commands","arguments":{"filter":"align"}}
|
||
{"name":"run_command","arguments":{"command":"select.all"}}
|
||
{"name":"run_command","arguments":{"command":"object.align","params":{"align":"left"}}}
|
||
{"name":"run_command","arguments":{"command":"object.group"}}
|
||
```
|
||
|
||
Drive a tool like a mouse:
|
||
|
||
```json
|
||
{"name":"pointer_gesture","arguments":{"tool":"ellipse","events":[
|
||
{"kind":"down","x":100,"y":100},{"kind":"drag","x":150,"y":140},{"kind":"up","x":200,"y":180}]}}
|
||
```
|
||
|
||
Edit a gradient with the Gradient tool's annotator (the bar runs along the vector; stop chips sit 10 px under
|
||
it, midpoint diamonds 7 px above): a click on the bar adds a stop, dragging a chip moves it (drag it off the bar to
|
||
delete it, hold Alt to copy it), dragging the end handle changes the vector. The selected stop
|
||
(`gradient.selectStop`) takes Delete and ←/→:
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"paint.setFill","params":{"gradient":{"start":[100,150],"end":[200,150]}}}}
|
||
{"name":"pointer_gesture","arguments":{"tool":"gradient","events":[{"kind":"down","x":130,"y":150},{"kind":"up","x":130,"y":150}]}}
|
||
{"name":"pointer_gesture","arguments":{"events":[{"kind":"down","x":130,"y":160},{"kind":"drag","x":160,"y":160},{"kind":"up","x":160,"y":160}]}}
|
||
{"name":"press_key","arguments":{"key":"Delete"}}
|
||
```
|
||
|
||
Raw protocol (for debugging):
|
||
|
||
```sh
|
||
printf '%s\n' \
|
||
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"sh","version":"0"}}}' \
|
||
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
||
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"draw_shape","arguments":{"shape":"ellipse","x":10,"y":10,"width":100,"height":80,"fill":"#ff0000"}}}' \
|
||
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"inspect_document","arguments":{}}}' \
|
||
| target/release/vectorcraft-cli mcp --headless
|
||
```
|
||
|
||
## Headless batch CLI
|
||
|
||
```sh
|
||
vectorcraft-cli commands # command catalogue (JSON)
|
||
vectorcraft-cli run --in in.svg \
|
||
--cmd select.all --cmd paint.setFill --params '{"color":"#ff0000"}' \
|
||
--export out.svg --export out.png --scale 2
|
||
vectorcraft-cli run --cmd file.new --params '{"width":800,"height":600}' \
|
||
--cmd shape.star --params '{"cx":400,"cy":300,"radius1":200,"radius2":90}' \
|
||
--export star.vectorcraft
|
||
```
|
||
|
||
`run` prints one JSON line per step (`open`, `cmd`, `export`) and exits non-zero on the first failure. `--params`
|
||
applies to the `--cmd` just before it. `run` also accepts the host commands `file.open`, `file.save`, `file.export`,
|
||
`file.exportForScreens` and `tool.select`. `run --in`, `convert` and `info` read every format `document.open` reads
|
||
(`vectorcraft-cli --help` lists them).
|
||
|
||
## Transparency and opacity masks
|
||
|
||
Transparency and opacity-mask commands take `ids` (or `id`), so they need no selection. Opacity is a percentage
|
||
(0..100) in every command (`transparency.set`, `object.setProps`, `appearance.setItem`). `transparency.info` returns
|
||
the Transparency panel's values, with `null` where the objects differ. Making a mask from one object gives it an
|
||
empty mask and enters mask editing: art drawn then becomes the mask, and the mask commands act on the masked object
|
||
until `transparency.stopEditingOpacityMask`. Saves and exports never include the editing layer.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"transparency.set","params":{"ids":[12],"opacity":40,"blend":"Multiply"}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.makeOpacityMask","params":{"ids":[12,15],"invert":true}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.setOpacityMask","params":{"id":12,"clip":false}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.info","params":{"ids":[12,20]}}}
|
||
```
|
||
|
||
## Clipping masks
|
||
|
||
`object.clippingMask.make` clips the selected objects by the topmost one, which may be a path, a compound path or a
|
||
text object (it loses its paint). Compound holes, even-odd fills, glyph outlines and the union of a group's members
|
||
clip alike on screen, in raster export and in SVG and PDF. `object.clippingMask.release` turns the clip group into a
|
||
plain group and keeps the clipping path, unpainted.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"select.set","params":{"ids":[12,15]}}}
|
||
{"name":"run_command","arguments":{"command":"object.clippingMask.make","params":{}}}
|
||
```
|
||
|
||
## Graphic styles
|
||
|
||
A graphic style holds an appearance (fills, strokes, effects) plus opacity, blend mode, isolate and knockout.
|
||
`graphicStyle.new {name?, id?}` captures an object (a group without its own fills or strokes lends its topmost
|
||
object's, type its characters'). `graphicStyle.apply {name, ids?}` gives the objects the style and links them;
|
||
`add: true` adds the style on top of the existing appearance instead. Linked objects stay linked while they keep the
|
||
style's look: editing their appearance or transparency breaks the link, and `graphicStyle.redefine {name?, id?}`
|
||
updates only the objects still linked. `graphicStyle.list` returns each style with the ids linked to it and the
|
||
style of the first selected object; `select.same.graphicStyle` selects an object's fellow users.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.new","params":{"id":12,"name":"Glow"}}}
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.apply","params":{"name":"Glow","ids":[15,20]}}}
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.redefine","params":{"id":12}}}
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.list","params":{}}}
|
||
```
|