* feat(MCP): follow agent activity in canvas * feat(collab): show MCP, ACP, and harness sessions as agents MCP clients worked on the document unseen: only the built-in chat had a presence, and following agent activity meant a separate setting that moved the viewport after every MCP tool. Each MCP session now shows as an agent with a callsign in its owner's color, like the chat: the MCP server forwards the session and the client's name with each tool call, the app's own ACP and Pi harness chats mark their sessions with a header, and the agent points at the layers each call reads or changes on their page. It rests after a quiet spell, leaves when its session ends or the server disconnects, and collaborators see it through awareness. Following it works like following anyone else, from the avatars, so the default-on follow setting and its viewport fitting go. Co-authored-by: Victor Wads <victor@wads.dev> * feat(ai): move the chat's agent through JSX as it streams While the built-in chat streamed a render call, its preview grew on the canvas but the agent stood still until the tool finished. The preview now reports, after each update, the element that appeared last and the bounds of what the JSX builds; the agent's cursor follows that element and its outline traces the preview, for collaborators too, until the tool runs and the agent outlines the real layers. Peers' outlines are validated and capped like their selections. Co-authored-by: Victor Wads <victor@wads.dev> * feat(collab): glide cursors and the followed view instead of jumping People's and agents' cursors jumped to each new point, which with throttled awareness and an agent streaming JSX made them stutter, and following re-centered the view in one jump on every update. Cursors now ease to each new point from wherever they are drawn, and following pans and zooms the view there the same way, stopping in place when you take over. With animations off or reduced motion, both move at once. Each cursor carries an id so it keeps its glide between updates. Co-authored-by: Victor Wads <victor@wads.dev> * test(collab): cover MCP agents and the streaming agent in the browser Test runs send MCP requests through the bridge's own command handler, so a browser test can show an MCP session as an agent to the editor and to a collaborator without a separate server. The streaming JSX test checks that the chat's agent cursor and outline follow the newest element. Co-authored-by: Victor Wads <victor@wads.dev> * docs: describe agent sessions, streaming cursors, and gliding follow Co-authored-by: Victor Wads <victor@wads.dev> * fix(ai): keep the streaming agent's name off the text it writes The chat's agent sat at the newest streamed element's top-left corner, so its name label covered the text being written. It now sits at the element's trailing corner, where content grows. Co-authored-by: Victor Wads <victor@wads.dev> * fix(collab): end only a closed connection's own MCP sessions When the app's connection to the MCP server closed, every MCP session's agent left, including sessions that never came over that connection, such as a second editor's. The bridge now remembers the sessions each connection carried and ends only those. Co-authored-by: Victor Wads <victor@wads.dev> * feat(ai): follow your agents automatically while they work Agents spun up from the chat or an MCP client worked out of sight and then went idle, so people had to find what changed, and the agent skill told agents to move the user's view and selection after every edit. A Follow agents toggle in the AI panel's header, on by default, now has the view follow our agents from the start of each run: whichever starts first, then the next one at work once the followed agent rests. Leaving the page, moving the view, Escape, or Stop following leaves that agent alone until it rests; people are never followed this way. The skill no longer asks agents to select and zoom to their work. Co-authored-by: Victor Wads <victor@wads.dev> * fix(collab): keep an MCP agent when a restarted bridge carries its session Restarting MCP disconnects the old bridge and opens a new one at once, but the browser reports the old socket's close only after its close handshake. A tool call that reached the new connection in between was undone by that close, which ended the session and removed its agent. Sessions now record every connection their calls came over, across bridges, and end only when the last of them closes. The docs no longer say that following an agent keeps it from editing out of sight: following moves your view, and the stale variables and follow bullets the changelog's union merge brought back are removed. Co-authored-by: Victor Wads <victor@wads.dev> * test(collab): record what the bridge sends instead of an empty fake method Co-authored-by: Victor Wads <victor@wads.dev> --------- Co-authored-by: Danila Poyarkov <dev@dannote.net>
18 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.
Each MCP session shows in the app as an agent with a callsign, like the built-in chat: its cursor and outline sit on the layers each tool reads or changes, it rests after a quiet spell and leaves when the session ends, and people can follow it from their avatar. Collaborators in a shared room see it too.
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.