openpencil/packages/docs/programmable/index.md
Danila Poyarkov b52d7e2651
feat: control documents, history, settings, and tools from the CLI and MCP (#871)
* fix(app): record MCP and CLI structural edits as undo steps

The automation bridge ran non-atomic tools, render, and eval without an
undo entry, so Edit > Undo could not revert layers an MCP client or the
CLI created, deleted, or rearranged. Snapshot the page around these
edits as the AI chat does, and skip the entry when nothing changed so
read-only scripts leave the history alone.

* feat(app): activate documents, undo, redo, and change settings over automation

Add activate_document, undo, redo, get_settings, and update_settings to
the app's automation bridge. Settings cover appearance, snapping, canvas
rendering, recovery, and chat preferences, validated with Valibot and
applied through their owning stores; credentials, models, MCP
connections, storage, and tool access stay out of reach.

* feat(mcp): expose document activation, history, and settings tools

* feat(cli): manage documents, history, settings, and tools in the running app

Turn documents into a command group (list, open, new, save, close,
activate), add undo, redo, and settings get/set, and add tool
list/describe/call so every MCP tool runs from the shell, against the
running app or headlessly on a file.

* docs: document app control from the CLI and MCP

* fix: never prompt in the app from automation closes and saves

close_file opened the app's Save changes dialog, which an agent cannot
answer: the call timed out and the dialog stayed open. It now fails on
unsaved changes unless the caller passes unsaved "save" or "discard"
(CLI --save or --discard). save_file and new_document no longer open a
Save dialog for a document that was never saved, report a failed save
as an error, and leave the document untouched when the path is refused.

* docs: describe non-interactive close and save

* fix: address review findings in app automation

Keep a document's source when a save to a new path fails, report
vector-edit undo and redo no-ops as unapplied, echo only the applied
patch from update_settings so writing cannot read settings, reject
tool call --write/--output without a file, and stop settings get from
following inherited keys.

* fix(app): record render undo on the page that receives the layers

A render into a parent on another page was snapshotted against the
target page, so undo left the new layers in place. Snapshot the page
that contains the parent instead, and document that eval edits made
after switching pages stay outside the undo step.

* feat(app): limit automation undo to its own steps and expose design check settings

The undo history is shared with the person in the editor, so an agent's
undo could revert the user's last edit. Automation undo and redo now act
only on steps made through the bridge, and only while they are newest;
otherwise they fail and leave the history alone. Vector edit mode's
session history is off limits entirely. Settings automation also covers
the design check preferences that landed on master.
2026-10-04 16:02:36 +00:00

7.9 KiB
Raw Blame History

layout title description
doc Automation AI chat, CLI, JSX renderer, MCP server, and other automation surfaces built on the OpenPencil editor engine.

Automation

OpenPencil treats design files as data. Every operation available in the editor — creating shapes, setting fills, managing auto-layout, exporting assets — is also available from the terminal, from AI agents, and from code. No plugins to install, no API keys, no waiting list.

The editor UI and the automation interfaces use the same engine. If you can do it by clicking, you can do it by scripting.

The bigger idea

OpenPencil is not just meant to be a design app.

It is also meant to be a toolkit: something you can embed into other products, wrap with your own UI, and use to build editing workflows that fit your own domain.

That is why the automation surface matters. The app, the CLI, the AI tools, the JSX renderer, the MCP server, and the SDK all build on the same underlying editor engine.

AI Chat

The built-in assistant has access to 90+ tools that cover the full surface of the editor. Describe what you want in natural language — "add a 16px drop shadow to all buttons", "create a card component with dark mode variant", "export every frame on this page at 2×".

AI Chat →

Collaboration

Real-time multiplayer editing over peer-to-peer WebRTC. No server, no account. Share a room link and edit together with live cursors and follow mode. Document state syncs via CRDT, so edits merge automatically even on flaky connections.

Collaboration →

Vue SDK

Build OpenPencil-powered editors with the same Vue SDK the app uses internally. The SDK exposes editor context, canvas wiring, selection state, command models, property-panel composables, and headless primitives.

Vue SDK →

JSX Renderer

Describe UI as JSX — the same syntax LLMs already know from React. A single call can create an entire component tree with frames, text, auto-layout, fills, and strokes. Compact, declarative, and diffable.

Going the other direction, export any selection back to JSX with Tailwind classes — useful for handing off to development or feeding designs back into an LLM.

JSX Renderer →

CLI

Inspect, lint, export, and analyze design documents without opening the editor. List pages, search nodes, extract design tokens, catch layout or accessibility issues, and render to PNG — all from the terminal with machine-readable JSON output.

The CLI also connects to the running desktop app via RPC, so you can script the editor while you're using it: open, save, and switch documents, undo, change settings, and call any MCP tool.

Inspecting Files · Exporting · Analyzing Designs · Scripting · Controlling the App

MCP Server

Connect Claude Code, Cursor, Windsurf, or any MCP-compatible client to OpenPencil. The server exposes 90 tools for reading, creating, and modifying designs — the same tools the built-in AI chat uses. Runs over stdio or HTTP with session support.

MCP Server →

URL scheme

The desktop app registers openpencil://, so a published page — a Storybook story, a design review, a README — can link straight to a layer:

openpencil://open?file=web/design/hikyo.pen&node=Button/Large/Default

file is a repository-relative path ending in .pen or .fig; absolute paths and . or .. segments are refused. node is optional. Both values are URL-encoded — path separators may stay literal, but a literal + must be sent as %2B — and a repeated key takes its last value.

The app matches file against the paths of the open tabs as a whole trailing segment sequence, and focuses that tab without re-reading the document, so a file that moved or turned unreadable since it opened still gets its layer selected. The first open tab whose path ends with the requested path wins, which matters when two checkouts have the same file open. Segments are compared the way the platform's filesystem does: ASCII-case-insensitively on macOS and Windows, exactly on Linux, so Web/Design/hikyo.pen and web/design/hikyo.pen are the same file on a Mac and two different ones on Linux. If no open tab matches, a file picker asks for the file once; the picked file must end with the same relative path, otherwise the link is cancelled. No path is joined onto a root and no filesystem access is granted beyond what the picker returns. A file the link actually opens — the picked one — joins the recent-files list like any other file you open; focusing a tab that was already open does not touch the list, because nothing was opened.

With a node name, the app selects every layer carrying that exact name on the current page and zooms the view to the whole selection. When the current page has none, it switches to the first page that does, loading pages as needed. An unknown name shows a notice and leaves the document open. Opening a file and selecting layers is all the scheme can do.

The web app takes the same link from its own address bar:

https://app.openpencil.dev/?file=https://raw.githubusercontent.com/open-pencil/open-pencil/master/tests/fixtures/pencil_button.pen&node=Button/Large/Default

Here file is an absolute https: URL ending in .pen or .fig — the web app has no filesystem, so a relative path, an http: URL or any other extension is refused with a console warning and nothing else. The extension is read off the URL's path, so a query string on the linked file changes nothing. A fragment is dropped before the fetch: it never reaches the server, so …/hikyo.pen#a and …/hikyo.pen#b open one tab, not two. node behaves exactly as above: the same exact-name selection and zoom, the same notice when no layer carries the name. Both values are URL-encoded, a literal + must be sent as %2B, and a repeated key takes its last value, as on the desktop. The link is handled on any route, so /share/<room>?file=… and /demo?file=… work like /?file=….

The browser fetches the file cross-origin, so the host must allow it: raw.githubusercontent.com sends Access-Control-Allow-Origin: * and works. The request carries no credentials and refuses to follow redirects, which keeps an https: link from being bounced to a plaintext one — a https://github.com/<owner>/<repo>/raw/... URL redirects to raw.githubusercontent.com and is therefore refused, so link to the raw host directly. file and node are stripped from the address bar through the router as soon as they are read — before the fetch, and also when the link was refused — so a reload does not re-open the document, and neither a copied URL nor a later in-app navigation carries the link payload. A linked document is capped at 64 MiB: the body is counted as it streams in, not trusted from Content-Length, and the request is aborted the moment it goes over, with the link reporting that the file exceeds 64 MiB. A deployment that serves the app under a Content-Security-Policy must allow the linked host in connect-src, otherwise the fetch is blocked and the link reports that it could not open the file.

On macOS the scheme belongs to the installed app bundle, so links reach an installed build and not a tauri dev process. On Windows and Linux the link arrives through the deep-link plugin, including when the app is not running yet: the link is queued at startup and handled once the editor is ready; on Linux the bundled desktop entry passes the link through %U.

Why Open?

Figma is a closed platform. Their MCP server is read-only. CDP browser access was killed in version 126. Design files live in a proprietary format on someone else's servers. Plugin development requires a custom runtime with limited APIs.

OpenPencil is the alternative: open source, MIT licensed, every operation scriptable, data stored locally. Your design files are yours — inspect them, transform them, pipe them into CI, feed them to an LLM. No permission needed.