Figma Edit MCP

Safety Manual — figma-edit-mcp (v2.3.1)

What this is. A safety manual for the project: it states the safety guarantees the system makes, the assumptions under which those guarantees hold, the residual risks it does not cover, and the controls (cross-cutting invariants + a per-tool gate matrix) that implement them. The framing is borrowed — informally — from functional-safety safety manuals (IEC 61508 / ISO 26262), whose job is to state a component’s guarantees and the conditions of safe use. It is adapted to an MCP server and is not a certification artifact.

What this is not. It is not a SECURITY.md vulnerability-disclosure policy — there is no reporting process, supported-versions table, or security contact here. This documents what the software enforces, not how to report a flaw.

Audience. Contributors changing the enforcement code; host / integrator authors wiring the plugin into an agent; and auditors or agents reasoning about which edits are possible.

Applies to: v2.3.1 (the Bind-Variable Guardrails release). It describes the enforcement state as of that release — the v2.1.0 scope-lock / name-verification / batch-atomicity model plus the structural-integrity guards (§1–§16) and the three-axis permission model (§14). Bare §N references point into the v2.2.0 PRD, where those structural guards were specified; sections tagged v2.3.0 §N / v2.3.1 §N point into their own release PRDs.

Ground truth. The enforcement lives in three places, in this order of authority:

  1. The plugin dispatcher figma_plugin/src/main.ts — the per-command gate stack; the only layer an agent cannot bypass.
  2. The handlers under figma_plugin/handlers/ — type/range/structural checks performed during execution.
  3. The MCP input schemas under src/mcp_server/tools/ — Zod shape/enum/range validation, before the WebSocket round-trip.

If this document and the code disagree, the code is correct and this document is stale — fix the doc.


Safety goal & trust model

The system exists to prevent an LLM agent from making unsafe or unintended edits to a live, user-owned Figma document. The agent is untrusted with respect to edit safety: it may hold a stale node ID, hallucinate a name, or misjudge whether an edit is wanted.

The Figma plugin is the trust boundary. It holds the user-granted scope and permissions and refuses anything outside them — returning a structured "Operation Denied: …" error rather than relying on the agent’s judgement. The MCP server and the agent’s host are not trust boundaries (see Assumption AS1). Because the plugin cannot trust the agent’s judgement about whether an edit is safe, it enforces mechanical, checkable properties (where, which node, what type, what protection) and leaves intent to the user (see Residual Risk R2).


Safety guarantees

The system guarantees, subject to the Assumptions below, that:

# Guarantee Implemented by
G1 Bounded write surface — no node write outside the user-selected scope subtree; no node writes at all without a scope link. A2
G2 Right-node assurance — no write proceeds unless the caller-supplied name matches the resolved node’s actual name (every write tool, including reaction_update and variable_delete). A3
G3 Explicit placement — no node is created without an explicit, resolved parent. A4
G4 All-or-nothing batches — a batch containing any invalid member mutates nothing. A5
G5 Reads cannot mutate — discovery/navigation never change the document and are never blocked. A6
G6 Self-preservation — the session’s scope anchor cannot be destroyed or replaced by an edit (node_delete/node_flatten/node_ungroup/create_component). A9 · §1
G7 Respect explicit protection — locked nodes, remote (shared-library) assets, and the interiors of component instances are not edited around. A9 · §2/§4/§7
G8 Least authority for assets — document-global variable and style edits each require an explicit, separate opt-in, independent of node-edit permission. A2 · §14

Assumptions & conditions of safe use

Each guarantee holds only while these conditions hold. Violating one voids the corresponding guarantee — this is the heart of the safety-manual contract.


Residual risks & known limitations

Explicitly not guaranteed — accepted trade-offs a safe integration must account for:

Two limitations present before v2.2.0 are now closed and have moved into the guarantees: document-global asset reach from a node-scoped session is gated by the new permission axes (G8 / §14), and remote-library-asset edits are blocked by a structured pre-check (G7 / §7).


Operator / integrator responsibilities

To keep the guarantees valid, the human and host must:


Part A — Safety guarantees in detail (the enforced invariants)

These cross-cutting invariants are the mechanisms behind the G-claims. Part B says which apply to each tool.

A1. Enforcement is plugin-side; the agent cannot bypass it

All access control runs inside the Figma plugin at execution time and returns a structured "Operation Denied: …" error. The MCP server’s Zod schemas validate shape (types, enums, numeric ranges) but are not a security boundary — they exist for ergonomics and fail-fast input errors (AS6). Per PRD D3, the v2.2.0 guards are plugin-only, not mirrored MCP-side.

A2. The permission / scope model → G1, G8

Connection state carries four fields, set at connect time and locked for the session (changing any requires disconnect + reconnect — AS5):

scopeRootId       // enforcement anchor for node edits; null ⇒ no node edits
allowEditNode     // false | "page" | "node"  — truthy ⇔ scopeRootId set
allowEditVariable // boolean — document-global, independent of scope
allowEditStyle    // boolean — document-global, independent of scope

The three permission axes are independent — none implies another:

Axis Granted by Gate Failure
Node edits a Page/Layer scope link allowEditNode set and target within scopeRootId READ_ONLY_MODE (node-only) / OUTSIDE_SCOPE
Variable edits “Allow … Variables” checkbox allowEditVariable VARIABLE_EDITS_DISABLED
Style edits “Allow … Styles” checkbox allowEditStyle STYLE_EDITS_DISABLED

A3. Every write verifies the resolved name → G2

Every modify tool requires a nodeName; every create tool a parentNodeName; batch tools a name per item. The plugin resolves the node by ID and rejects on mismatch (verifyNodeName; absent name ⇒ rejected). This catches stale/fabricated IDs. Names must be passed back verbatim (AS3). As of v2.2.0 this is universal — reaction_update (§6A) and variable_delete (§6B, both id and collection modes) now verify names, and style_delete’s guard matches the strict rule.

A4. Creation requires an explicit parent → G3

No “current page” fallback. create_shape/create_frame/create_text/create_svg/create_instance require a resolvable parentId + parentNodeName.

A5. Batch atomicity (pre-validate → zero-mutation abort) → G4

Batch tools validate every item (existence, scope, name, type, locked, instance-interior, scope-root) before any mutation. A single bad member aborts the whole batch with zero mutations. Once mutation begins, handlers process sequentially, stop on first failure, and return a completed-vs-failed report — no auto-rollback. (text_set_content, annotation_set, instance_set_overrides, create_component_set, node_delete.)

A6. Reads are never gated → G5

Discovery and navigation ignore the node/variable/style permission axes, scope, and locks: node_info, page_info, all *_list, instance_get_overrides, reaction_list, annotation_list, view_navigate, and node_export_visual (PRD D5). Accepted residual R3: node_export_visual can render an off-scope node.

A7. Node-ID normalization

Figma-URL IDs use dashes (20485-41); the API expects colons (20485:41). The MCP server converts before forwarding; pass URL-format IDs through unchanged.

A8. TOCTOU is an accepted residual → R1

A node can be locked/unlocked, reparented, or deleted by the user between validation and mutation. Guards do not hold a lock across the gap; the handler’s try/catch reports whatever Figma throws.

A9. Structural-integrity guards (cross-cutting) → G6, G7

Four families of plugin-side guard, each returning "Operation Denied: …":


Part B — Controls: per-tool enforcement matrix

Gate order in the dispatcher (most-specific error wins): permission → scope → name → locked → instance-interior / scope-root → handler checks. Shorthand: node-perm = allowEditNode + scope link; var-perm = allowEditVariable; style-perm = allowEditStyle.

B1. Node write tools (single target)

Tool Enforced gate stack
node_set_fill node-perm · scope · name · locked
node_set_stroke node-perm · scope · name · locked
node_set_corner_radius node-perm · scope · name · locked
node_set_effects node-perm · scope · name · locked
node_set_auto_layout node-perm · scope · name · locked · enum checks · FILL needs auto-layout parent (§8) · NONE-frame silent-drop rejected (§8) · BASELINE horizontal-only · counterAxisSpacing WRAP-only
node_rename node-perm · scope · name · locked
node_transform node-perm · scope · name · locked · layout-controlled x/y hard-reject (§9) · resize-resets-sizing warning (§9)
node_bind_variable node-perm · scope · name · locked · unsupported node / mixed paint guard (v2.3.1 §1) · auto-layout precheck (v2.3.1 §3) · SOLID-only paint bind (type-mismatch guard, gated by node-perm not var-perm)
node_apply_style node-perm · scope · name · locked (gated by node-perm, not style-perm)
node_clone node-perm · scope(source) · name · locked(source)
node_flatten node-perm · scope · name · locked · scope-root (§1)
node_ungroup node-perm · scope · name · locked · scope-root (§1) · instance-interior (§4) · must be GROUP
text_set_style node-perm · scope · name · locked · type TEXT · mixed-font load via getStyledTextSegments (§10) · full schema↔handler contract incl. fontName + lineHeight AUTO (§15)
instance_set_property node-perm · scope · name · locked · type INSTANCE · value type validation BOOLEAN/TEXT/VARIANT/INSTANCE_SWAP (§5) · not remote-gated (local override)
reaction_update node-perm · scope · name (§6A) · locked

B2. Node batch tools (per-item pre-validation, zero-mutation abort)

Tool Enforced gate stack (per item unless noted)
node_delete node-perm · scopeRoot present · exists · scope · name · locked · instance-interior (§4) · scope-root (§1)
node_group node-perm · scope · name · same-parent · locked · instance-interior (§4)
text_set_content node-perm · scopeRoot · exists · scope · name · type TEXT · locked · correct characters contract (§16)
annotation_set node-perm · scopeRoot · exists · scope · name · supports-annotations · locked
instance_set_overrides node-perm · scopeRoot · source exists+INSTANCE · per-target exists+scope+name+INSTANCE+locked
create_component_set node-perm · scopeRoot · per-component exists+scope+name+propValues-count+COMPONENT-type · parent scope+name+locked · duplicate-variant uniqueness (§11)

B3. Creation tools (gate on the parent)

Tool Enforced gate stack
create_shape node-perm · parent scope+name+locked+instance-interior (§4) · shape-param checks (arcData=ellipse, pointCount≥3, innerRadius=star) · color 0–1 (Zod)
create_frame node-perm · parent scope+name+locked+instance-interior (§4) · color 0–1 (Zod) · opacity normalized, no NaN (§12)
create_text node-perm · parent scope+name+locked+instance-interior (§4) · color 0–1 (Zod) · opacity normalized, no NaN (§12)
create_svg node-perm · parent scope+name+locked+instance-interior (§4)
create_instance node-perm · parent scope+name+locked+instance-interior (§4)
create_component node-perm · scope · name · locked · scope-root self-destruction (§1)
node_insert_child node-perm · parent scope+name · child scope+name · locked(parent & child) · self/cyclic-parent (§3) · instance-interior, both ids (§4) · index bounds (§13)
create_connection node-perm · connector scope (if set) · per-connection start/end scope+name · locked

B4. Document-global asset tools (gated by the asset permission axes, not positional scope)

Tool Enforced gate stack
variable_manage var-perm (§14) · remote block on UPDATE (§7)
variable_delete var-perm (§14) · ids-xor-collection · required name verification, both modes (§6B) · remote block (§7) · full-document consumer scan refuses in-use deletes
style_manage style-perm (§14) · remote block on edit-existing (§7) · (binding a variable into a style needs only style-perm)
style_delete style-perm (§14) · styleName verification (strict) · remote block (§7)
component_manage_property node-perm · scope · name · locked · COMPONENT/COMPONENT_SET · blocks VARIANT add · value type validation (§5) · variant-member guard (§5) · remote block (§7)
component_delete_property node-perm · scope · name · locked · remote block (§7)

Note: component_*_property edit a main component definition and remain node edits (node-perm + scope + name), plus the §7 remote block. Only variable_*/style_* move onto the new asset permission axes.

B5. Read & navigation tools — ungated (A6)

Tool Requirement Gated?
node_info empty-args → falls back to scopeRootId; node-read-only empty-args returns {nodes:[]} No (read)
page_info, style_list, component_list, variable_list, annotation_list No (read)
instance_get_overrides requires instanceNodeId No (read)
reaction_list requires nodeIds[] No (read)
node_export_visual No (read; accepted off-scope residual R3)
view_navigate resolves ids; rejects DOCUMENT root, mixed page/node, cross-page selection No — deliberately scope-exempt (A6)
get_connect_payload No (handshake; surfaces the three permission axes)

Part C — Input-validation (Zod / schema-level) checks

These run in the MCP server before the plugin and reject malformed input early (not a control — see A1/AS6). Notable ones:


Part D — Structured error codes (reference)

The plugin returns these from main.ts ERRORS and inline throws. Full recovery guidance lives in skills/figma-edit/references/error-playbook.md.

Code / message Meaning
READ_ONLY_MODE No scope link → node writes blocked (asset edits gated separately).
VARIABLE_EDITS_DISABLED / STYLE_EDITS_DISABLED The corresponding asset permission axis is off (§14).
OUTSIDE_SCOPE / PARENT_OUTSIDE_SCOPE / CLONING_SOURCE_NODE_OUTSIDE_SCOPE Target/parent/clone-source not under scopeRootId.
SCOPE_DELETED scopeRootId no longer resolves — the bricking the §1 scope-root guard prevents.
NAME_MISMATCH / PARENT_NAME_MISMATCH Resolved name ≠ supplied name (stale/fabricated id).
"… is locked …" (§2) Target or an ancestor is locked.
"… is inside a component instance …" (§4) Structural edit inside an INSTANCE.
"… is a remote library asset …" (§7) Edit targets a remote style/variable/main-component.
"… is the current Editable Scope root …" (§1) Destructive/replacing op on the scope anchor.
"… cannot be inserted into itself …" / cyclic (§3) Self- or cyclic reparent.
"Sizing 'FILL' requires … Auto-Layout parent" (§8); index-bounds (§13); duplicate-variant (§11); auto-layout child transform (§9) The remaining structured "Operation Denied: …" strings.
Actionable Prechecks node_bind_variable blocks missing auto-layout (v2.3.1 §3) and non-solid paint binds (v2.3.1 §1).
MISSING_* / type errors Parameter/shape/type violations from the dispatcher and handlers.

Maintenance