* feat: author behaviours on main components
A main component or component set can behave as a Switch, Checkbox,
Slider, or Tabs, after Reka UI's primitives. The behaviour lives in
OpenPencil plugin data: boolean values bind to variant or boolean
properties with the values meaning on and off, a number keeps its own
range since Figma has no number property, and the control's
subcomponents bind to the component's slots. A Behaviour section in the
properties panel adds, binds, and removes it, each as one undo step,
and flags required bindings that are missing. The canvas-only layout's
pill becomes a component that preview will reuse.
* feat: preview instances with behaviours on the canvas
View > Preview (Cmd+Alt+Enter) puts the canvas in preview: a lone canvas
switches to the canvas-only layout with a Previewing pill, and a split
canvas previews on its own side. Clicking a Switch or Checkbox flips it,
dragging a Slider moves its thumb and range, and clicking a Tabs trigger
shows its panel. Preview keeps its state on copies of the instances it
touched, in a private graph with the document's ids, and the canvas
draws those copies in place of the originals, so the document, undo,
autosave, and collaborators never see it. Escape or the pill leaves
preview, Reset restores every control, and editing shortcuts, labels,
and outlines stay off while previewing.
* feat: translate behaviour and preview strings; cover preview with an e2e flow
* refactor(vue): reuse VariantDefinitionControl for behaviour property options
* refactor: split variant actions and preview interactions by domain
Variant authoring was one 706-line closure; it is now graph queries
(model), undo snapshots (history), property definition edits
(definitions), and the editor facade (index). Preview interactions move
into play/kinds, one module per control, registered by behaviour kind so
a new kind cannot ship without its contract and interaction. Behaviour
contracts are keyed by kind. In the Vue SDK, slot and variant authoring
controls get their own folders beside component-props and behaviour,
and the app's variant section joins slot/ and behaviour/.
* refactor: keep the behaviour model in scene-graph's plugin-data registry
Master now defines every OpenPencil plugin-data key in one typed registry
in scene-graph. The behaviour schema registers there as a field, and the
model and contracts move beside slots, exported from the package root;
the @open-pencil/core/behaviours subpath is gone.
* feat: interaction states and keyboard focus in preview
A behaviour can bind a variant property to the default, hover, pressed,
focus, and disabled states; binding it maps values named like those
states. Preview switches the instance's copy to the matching variant as
the pointer hovers, presses, and releases, keeps other values when the
set draws the combination and falls back to rest otherwise, and skips
disabled instances. Tab moves visible keyboard focus between controls,
Space, Enter, arrows, Home, and End use the focused one, and Escape
takes visible focus off before leaving preview. A Button kind covers
controls that only have states.
* feat: toggle, radio, group, progress, collapsible, and accordion behaviours
Radio group, toggle group, and accordion hold their items in a slot;
each item is an instance with its own behaviour, so a press inside the
slot goes to the group, which turns the pressed item on and the others
off through the item's own interaction. Progress shares the slider's
number handling through rangeControl, and a collapsible shows and hides
its content slot from its trigger, remembering its open state even
when no property draws it. Tabs and groups share arrow-key navigation.
* feat: text field, textarea, and number field behaviours
A behaviour value can now be text, bound to a text property, so
preview types into a copy of the field through the same property path
the editor uses. A bound Filled value switches to the placeholder
variant when the field empties. A number field keeps its own range,
shows its value through a text property, and steps from its increment
and decrement slots and the arrow keys. Text fields show focus from a
click, and the focused control receives every key; Option still types,
and only Cmd or Ctrl combinations stay shortcuts.
* fix: keep behaviour bindings when saving as .fig
Saving as .fig gives component properties new GUIDs, but behaviours
kept the old ids in their plugin data, so every binding read as missing
after reopening. The export now renames the ids behaviours bind with
the same GUIDs, on its own copy of the document.
* fix: let previewed controls resize layout imported from .fig
Layers from a .fig keep the sizes Figma computed, and auto layout
prefers them, so an opened collapsible or accordion item kept its
closed height in preview. When preview shows, hides, or retypes a
layer in a copy, it drops those sizes from the layer's copied ancestors
so auto layout sizes them again; untouched layers keep Figma's sizes.
* fix: publish behaviours and other plugin content with library assets
Every OpenPencil plugin-data field now declares its role: content that
exists only as plugin data (behaviours, OkHCL picks), format copies of
node fields written for files, or bookkeeping about where a document
or node came from. Library snapshots keep a node's content plugin data,
including other plugins' entries, and drop the rest; the asset hash
counts the same entries, so a behaviour-only change is offered as an
update while a .fig round trip still changes nothing.
* feat: name behaviour rows by meaning and create what they need
The Behaviour section named every main value "Value" under a "Values"
heading, and a component without matching properties left an empty
picker with no way forward. Rows are now named for the control (On,
Checked, Pressed, Text), rows the control needs or already uses come
first, and the optional rest folds under More options; a button keeps
its states in view. An empty row creates what it needs in one undo
step: a text layer and text property, Off and On variants on a set, or
a slot frame for a part. The missing chip names the row it means and
takes you there.
* fix(dom-css): position free layers, hug content, and round ellipses
HTML and Tailwind export stacked the layers of frames without auto
layout in block flow, wrote fixed pixel sizes for auto layout frames
set to Hug and for auto-sizing text, and drew ellipses as boxes. Layers
a parent does not lay out are now absolutely positioned at their
coordinates inside a relative frame, hugging axes are left to the
content, and ellipses get a 50% radius.
* feat: run preview as live Reka UI islands over the canvas
Preview simulated controls on the canvas: copies of instances, a
handler per kind, its own key routing, and append-only text. It now
runs them as real components. Each top-level layer that holds an
instance with a behaviour becomes an island: its layers are projected
to DOM through dom-css into a shadow root laid over the pane at its pan
and zoom, and each behaviour mounts its Reka UI primitives on its
layers, so text fields are real inputs and focus, keys, and layout are
the browser's. Core's resolvePlayState shows instances in a state on a
private graph, so the component's variants draw it, and controls are
keyed by layer path so a variant switch keeps their DOM. The canvas
leaves island layers to the islands, and the canvas play runtime and
its key routing are gone.
* fix: derive variant properties from Property=Value component names
figma.combineAsVariants and Combine as variants only derived variant
properties from slash-separated names, so components named as Figma
names variants, such as State=On, Size=Large, became a set with no
properties. Both now derive each named property and its values, after
the slash form.
* feat: script and tool access to behaviours by name
Behaviour contracts follow Reka UI's anatomy: tabs keep their triggers
in the list slot and their content panels in a panels slot, and a slot
of repeated parts names the Reka part of its children. A behaviour
spec names component properties and slots instead of ids and resolves
to the stored behaviour and back, with errors that list what the
component has.
Scripts get an `openpencil` global next to `figma`, in the Figma API's
style: setBehaviour, getBehaviour with bindValue, bindPart, states,
and missing, behaviourKinds, and createSlot. The eval tool, the CLI,
and app automation compile scripts through one compileScript, so the
CLI now returns the last expression as the others do. MCP and AI chat
get set_behaviour, get_behaviour, and create_slot.
* feat: write controls in design JSX with Reka UI's element names
`<Switch.Root modelValue="State">` renders a main component, or a set
when its children are variants, that behaves as a switch, and
`<Switch.Thumb>` the slot that draws its thumb, one slot across the
set's variants. Inputs become the text property of a field, tab
triggers and panels go in their List and Panels slots, and a group's
items are `<RadioGroup.Item of={…} />` instances in its Items slot.
JSX export writes components with behaviours the same way, so they
render back unchanged. The authoring reference documents controls, and
the codegen and chat prompts now include it verbatim instead of
dedenting its code examples.
* chore: format the CLI export test
* docs: document slots, behaviours, preview, and the openpencil API
The components guide covers slots, behaviours, and preview with its
shortcut; scripting covers the openpencil global and eval's last-
expression result; the MCP and AI chat pages list the new tools; the
features overview, README, and roadmap mention working controls. The
chat prompt says how to build a control, and the codegen prompt builds
components with behaviours on their Reka UI primitives.
* chore: format the eval CLI test
* docs: explain behaviours and preview islands, and guide the openpencil API
A development page explains the behaviour model, the four authoring
surfaces, how preview islands turn a control's state into live Reka UI
components, and how to add a kind; the architecture page links it. The
Core guide sets the rules for OpenPencilAPI: Figma-only `figma`,
OpenPencil features on `openpencil` in the same style, one
compileScript, names over ids, and docs with every member. Package
READMEs mention the openpencil global, PlayIslands, Reka-named JSX, and
the behaviour model. Design JSX's behaviour modules move into a
behaviours folder instead of a suffixed sibling.
* refactor: center pasted layers through translate
centerNodesAt repeated translate's loop, which test:dupes reports on
master too.
* fix: validate behaviour ranges and guess on and off by name
A number value now needs max above min and a positive step: the schema,
specs, and the panel reject a range a slider cannot step through. Binding
a variant property guesses on and off by value name, as specs do, and a
boolean property gets no on/off pair. Part bindings are read through
partBinding, a replaced document restarts preview from its designed
state, and the e2e preview shortcut uses ControlOrMeta.
* feat: make the Behaviour section say what to do next
A slider's range fields now carry inline Min, Max, Step, and Start labels.
States offers Add state variants, which adds a Default, Hover, Pressed,
Focus, and Disabled variant and binds them; Add Off and On variants and
Add state variants turn a lone main component into a component set first,
and a part's slot can be added to a set, in every variant under one slot
id. Rows that could do nothing are gone: no empty pickers and no hints to
combine variants by hand, and an unbound Disabled is left to the states.
A warning line names what is still needed and replaces the missing chip,
and the Switch's main value is called Checked.
* fix: keep each slot to one part and keep creating slots at hand
A slot draws one part, so the Behaviour section no longer offers a slot
another part uses, and specs (the openpencil API, tools, and JSX) reject
binding one slot to two parts. A part's picker keeps an action to add a
new slot in its footer, so adding the first slot no longer hides it for
the other parts.
17 KiB
| title | description |
|---|---|
| MCP Server | Connect Claude Code, Cursor, Windsurf, and other MCP clients to OpenPencil for AI-assisted design inspection and editing. |
MCP Server
OpenPencil includes an MCP (Model Context Protocol) server that lets AI coding tools — Claude Code, Cursor, Windsurf, etc. — read and modify designs through the running app.
Two transports: stdio for MCP clients, and Streamable HTTP for browser extensions and scripts. On macOS and Linux, local clients prefer a private Unix domain socket; Windows and unavailable sockets fall back to localhost TCP.
Tool definitions own native Valibot input schemas, execution/mutation metadata, capabilities, and optional interface exposure exclusions. Tools are included by default; exposure: { mcp: false, ai: false, webmcp: false } can exclude them independently from each adapter. Exposure does not bypass execution support or user permissions: WebMCP still requires supported execution and explicit Off, Inspect, or Edit access. AI and MCP consume the same schema through Standard Schema; WebMCP derives its JSON Schema from that input. Numeric strings are accepted consistently across adapters, while non-finite values are rejected. Programmatic integrations use MCP SDK v2; custom tools replace the former params/ParamDef contract with input and execution metadata.
Tool access settings
Use Settings → Tool access (select Local MCP) to search and toggle the local server's tools, individually or by read-only/side-effect group. Group switches affect all group members, even during search. Restore defaults enables the configurable MCP tools again. Existing MCP preferences are preserved separately from the Built-in AI settings.
Restart the MCP server, then reconnect stdio clients, to apply changes. For an externally managed server, restart its owning process. The list reflects the tools discovered from the server; disabling a dedicated tool does not prevent an enabled script tool from performing the same operation. These switches are not a sandbox and do not configure remote MCP servers or WebMCP.
Browser-native WebMCP (experimental)
WebMCP is off by default. Open Settings → MCP → WebMCP and choose Inspect for read-only access or Edit to also allow scoped, undoable changes. Off unregisters all browser tools; changing modes revokes the previous registrations immediately. This preference is independent of local MCP authentication, tool switches, and outbound connections.
For local testing, use a Chrome version exposing document.modelContext, enable chrome://flags/#enable-webmcp-testing, and relaunch the browser. Open a document, enable access in Settings, and connect a WebMCP-capable browser agent or the Model Context Tool Inspector. Settings shows browser support and registration status. See the Chrome WebMCP guide for current availability.
In supported browsers, OpenPencil registers the selected reviewed set of tools directly in the workspace. Browser agents can inspect nodes, JSX, variables, components, and design patterns, and edit existing layer properties and variable values without installing or connecting an MCP server.
Tools target the document and page active when the call starts. Switching tabs does not redirect an in-flight call. Closing the workspace unregisters the tools. Tool inputs are validated and large inspection results require a narrower query. Oversized editing results are omitted with a committed-edit notice rather than reporting a successful edit as failed.
Edits to geometry, paints, layout, text, and variable bindings/values commit synchronously as individual undoable operations. Failed edits roll back, and undo targets the original document/page even after a page switch. Cancellation prevents an edit from starting; cancellation after commit does not reverse it. Font loading finishes separately without holding a mutation transaction open. Atomic editing currently requires a document with at most 10,000 nodes and variables combined; this shared limit also applies when the same editing tools run through app AI/MCP.
This surface does not expose structural creation/deletion, arbitrary JavaScript/JSX execution, image loading, filesystem operations, or credentials. Those tools retain their existing AI/MCP paths. WebMCP is an evolving browser proposal, not universally available; unsupported browsers continue to use OpenPencil normally. The stdio and HTTP integrations below remain independent.
Install
npm install -g @open-pencil/mcp
Stdio (Claude Code, Cursor, etc.)
The stdio server discovers the running OpenPencil app automatically. It prefers the app's Unix domain socket on macOS and Linux and falls back to localhost TCP when needed. Make sure the desktop app is open with a document loaded.
Claude Code
Install the MCP package and register it with Claude Code:
npm install -g @open-pencil/mcp
claude mcp add --scope user open-pencil -- openpencil-mcp
Check the connection:
claude mcp list
Claude Code asks before using each MCP tool unless you allow the server's tools. To auto-approve OpenPencil tools only, add this to ~/.claude/settings.json:
{
"permissions": {
"allow": ["mcp__open-pencil__*"]
}
}
This is narrower than --permission-mode bypassPermissions, which skips prompts for every tool. You can also approve tools interactively from Claude's prompt by choosing “Yes, and don't ask again”.
Example prompt:
Use the open-pencil MCP server to inspect the current page and create a small hero section on the canvas.
Other MCP clients
Add to your MCP config (for example .cursor/mcp.json):
{
"mcpServers": {
"open-pencil": {
"command": "openpencil-mcp"
}
}
}
Or run from source without installing:
::: code-group
{
"mcpServers": {
"open-pencil": {
"command": "bun",
"args": ["/path/to/open-pencil/packages/mcp/src/stdio.ts"]
}
}
}
{
"mcpServers": {
"open-pencil": {
"command": "npx",
"args": ["tsx", "/path/to/open-pencil/packages/mcp/src/stdio.ts"]
}
}
}
:::
HTTP
For browser extensions, scripts, CI, or any HTTP client:
openpencil-mcp-http
Or from source: bun packages/mcp/src/index.ts / npx tsx packages/mcp/src/index.ts
Security defaults:
- Unix socket and discovery files are created with owner-only permissions on macOS and Linux.
- TCP binds to
127.0.0.1and uses port 7600 by default. - Authentication is enabled by default with a generated token stored in the private discovery file.
evalis disabled.- File operations are limited to
OPENPENCIL_MCP_ROOT(defaults to the current working directory) and reject symlink escapes. - Only the desktop app's own origin (
tauri://localhostand itshttp(s)://tauri.localhostvariants) is allowed by default, so a server you start yourself works from the app without extra configuration. SetOPENPENCIL_MCP_CORS_ORIGINto a comma-separated list to allow other origins, such as a worktree dev server.
Set PORT=0 to disable TCP on macOS and Linux. Windows requires TCP. Set OPENPENCIL_MCP_SOCKET to override the Unix socket path, or OPENPENCIL_MCP_DISCOVERY_PATH to override the discovery file location. To provide a stable token, set OPENPENCIL_MCP_AUTH_TOKEN; an explicitly empty value disables authentication and should only be used with a trusted local socket.
Endpoints are available over both active transports:
GET /health— server and app connection status; never returns the auth token.POST /rpc— authenticated live-app automation.POST /mcp— MCP Streamable HTTP. Sessions use themcp-session-idheader.
Workflow
- Discover targets — call
list_documentsfirst when more than one document or page may be open. It returns stabledocument_idand page IDs. - Open —
open_fileto load an existing.fig, ornew_documentfor a blank canvas. These return target metadata for the opened or created document. - Read —
get_page_tree,find_nodes,get_node,list_pages - Create —
create_shape,render(JSX) - Modify —
set_fill,set_stroke,set_layout,update_node,set_effects - Structure —
reparent_node,group_nodes,clone_node,delete_node - Save —
save_fileto write back to.fig - Close —
close_fileto close an open document tab. With unsaved changes it fails unlessunsavedis"save"or"discard"; it never prompts in the app.
undo and redo step back through the agent's own changes, and activate_document brings a tab to the front when the user should see it.
Most tools accept optional document_id and page_id fields. Pass them explicitly for agent workflows instead of relying on the visible active tab/page. create_page only creates a page; call switch_page separately when the workflow should change the active page.
AI Agent Skill
Teach your AI coding agent to use OpenPencil tools:
npx skills add open-pencil/open-pencil
Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports skills. The skill covers the CLI, MCP tools, JSX rendering, eval, and the running app's automation bridge.
Tools
OpenPencil currently registers 100+ shared design tools, plus MCP-only document and prompt operations when applicable.
Document
| Tool | Description |
|---|---|
open_file |
Open a .fig file for editing |
close_file |
Close an open document tab; unsaved: "save" or "discard" decides what happens to unsaved changes |
save_file |
Save the current document to a .fig file |
new_document |
Create a new empty document |
list_documents |
List open app documents/tabs and their pages |
activate_document |
Bring a document tab to the front, optionally on a given page |
History
| Tool | Description |
|---|---|
undo |
Undo the newest change made through MCP or the CLI |
redo |
Redo the newest change undone through MCP or the CLI |
The history is shared with the person in the editor. undo and redo refuse when the newest step was made in the editor, so an agent never reverts the user's work. Each editing tool call is one undo step. An eval script is recorded against its target page, so edits it makes after switching figma.currentPage are not undoable.
Settings
| Tool | Description |
|---|---|
get_settings |
Read editor settings: appearance, snapping, canvas rendering, recovery, AI chat, and design check preferences |
update_settings |
Change settings with a partial object shaped like get_settings output; invalid keys and values are rejected |
Settings tools never expose credentials, AI models, MCP connections, storage, or tool access. The available keys are listed in Controlling the App.
Read
| Tool | Description |
|---|---|
get_selection |
Get currently selected nodes |
get_page_tree |
Get the full node tree of the current page |
get_current_page |
Get the current page name and ID |
get_node |
Get detailed properties of a node by ID |
find_nodes |
Find nodes by name pattern and/or type |
get_components |
List all components in the document |
list_pages |
List all pages |
list_variables |
List design variables |
list_collections |
List variable collections |
list_fonts |
List fonts used in the current page |
list_available_fonts |
List font families the current host can render |
get_font_status |
Report requested faces, loaded sources, active substitutions, why an installed face could not be loaded, and affected nodes |
page_bounds |
Get bounding box of all objects on the current page |
node_bounds |
Get bounding box of a node |
node_ancestors |
Get ancestor chain of a node |
node_children |
Get direct children of a node |
node_tree |
Get the subtree rooted at a node |
node_bindings |
Get variable bindings on a node |
Create
| Tool | Description |
|---|---|
create_shape |
Create a shape (FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION) |
create_vector |
Create a vector node from a path string |
create_slice |
Create an export slice |
create_page |
Create a new page |
render |
Render JSX to design nodes — create entire component trees in one call |
create_component |
Convert a frame/group into a component |
create_instance |
Create an instance of a component |
create_slot |
Make a frame inside a main component a slot |
set_behaviour |
Make a component behave as a Reka UI control, by its property and slot names; null removes it |
get_behaviour |
Read a component's behaviour and what it still misses; without an ID, list every kind |
node_to_component |
Convert an existing node into a component in-place |
Modify
| Tool | Description |
|---|---|
set_fill |
Set fill color (hex) |
set_stroke |
Set stroke color, weight, alignment |
set_effects |
Add shadow or blur effects |
update_node |
Update position, size, opacity, corner radius, text, font |
set_layout |
Set auto-layout (flexbox) — direction, spacing, padding, alignment |
set_constraints |
Set resize constraints |
set_rotation |
Set rotation angle in degrees |
set_opacity |
Set opacity (0–1) |
set_radius |
Set corner radius (uniform or per-corner) |
set_minmax |
Set min/max width and height constraints |
set_text |
Set text content of a TEXT node |
set_font |
Set font family and weight |
set_font_range |
Set font properties on a character range |
set_text_resize |
Set text auto-resize mode (fixed/auto-width/auto-height) |
set_visible |
Show or hide a node |
set_blend |
Set blend mode |
set_locked |
Lock or unlock a node |
set_stroke_align |
Set stroke alignment (inside/center/outside) |
set_text_properties |
Set text layout: alignment, auto-resize, text case, decoration, truncation |
set_layout_child |
Configure auto-layout child: sizing, grow, alignment, absolute positioning |
node_move |
Move a node to a new position |
node_resize |
Resize a node |
node_replace_with |
Replace a node with another node |
arrange |
Align or distribute selected nodes |
Structure
| Tool | Description |
|---|---|
delete_node |
Delete a node |
clone_node |
Duplicate a node |
rename_node |
Rename a node |
reparent_node |
Move a node into a different parent |
select_nodes |
Select nodes by ID |
group_nodes |
Group nodes |
ungroup_node |
Ungroup a group |
flatten_nodes |
Flatten nodes into a single vector |
boolean_union |
Boolean union of two or more nodes |
boolean_subtract |
Boolean subtraction |
boolean_intersect |
Boolean intersection |
boolean_exclude |
Boolean exclusion |
Vector Path
| Tool | Description |
|---|---|
path_get |
Get the path data of a vector node |
path_set |
Set the path data of a vector node |
path_scale |
Scale a vector path |
path_flip |
Flip a vector path horizontally or vertically |
path_move |
Translate a vector path |
Export
| Tool | Description |
|---|---|
export_image |
Export nodes as PNG, JPG, or WEBP. Returns base64-encoded image data |
export_svg |
Export nodes as SVG markup |
Viewport
| Tool | Description |
|---|---|
viewport_get |
Get current viewport position and zoom level |
viewport_set |
Set viewport position and zoom |
viewport_zoom_to_fit |
Zoom viewport to fit specified nodes |
Variables
| Tool | Description |
|---|---|
get_variable |
Get a variable by ID or name |
find_variables |
Find variables by name pattern or type |
create_variable |
Create a new variable in a collection |
set_variable |
Set a variable value in a mode |
delete_variable |
Delete a variable |
bind_variable |
Bind a variable to a node property |
get_collection |
Get a variable collection by ID or name |
create_collection |
Create a new variable collection |
delete_collection |
Delete a variable collection |
Analyze
| Tool | Description |
|---|---|
analyze_colors |
Analyze color palette usage across the document |
analyze_typography |
Analyze font/size/weight distribution |
analyze_spacing |
Analyze gap and padding values |
analyze_clusters |
Detect repeated patterns (potential components) |
lint |
Check accessibility and consistency issues, with fixes and suggestions |
lint_fix |
Apply safe lint fixes, and optionally the first suggestion of each finding |
Diff
| Tool | Description |
|---|---|
diff_create |
Patch that turns one node tree into another, as JSX attribute changes |
diff_jsx |
Structural diff between two nodes as design JSX |
diff_show |
Preview the patch that setting JSX attributes on a node would produce |
diff_apply |
Apply a patch after checking the nodes still match its old values |
diff_visual |
Pixel diff between two rendered nodes, returned as an image |
Navigation
| Tool | Description |
|---|---|
switch_page |
Switch to a page by name or ID |
Escape Hatch
| Tool | Description |
|---|---|
eval |
Execute JavaScript with full Figma Plugin API access |
Note: eval is available over stdio, but disabled in HTTP mode for security.