figma-console-mcp 1.34.0 β†’ 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **Your design system as an API.** Model Context Protocol server that bridges design and developmentβ€”giving AI assistants complete access to Figma for **extraction**, **creation**, **debugging**, and **bidirectional token sync**.
10
10
 
11
- > **πŸ†• Bidirectional Token Sync v2 + DTCG 2025.10 (v1.34.0):** `figma_import_tokens` now applies the *whole* diff plan β€” creates, renames, alias re-targeting, and replace-gated deletes β€” for a true codeβ†’Figma round-trip; exports speak DTCG 2025.10 on request (opt-in, legacy default byte-identical); scopes/codeSyntax round-trip; `figma_setup_design_tokens` accepts alias values; and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. **Plugin re-import required** (`code.js` + `ui.html` changed). [See what's new β†’](CHANGELOG.md#1340---2026-07-03)
11
+ > **πŸ†• Figma Slots Support (v1.35.0):** Create, inspect, populate, and reset Figma Slots (GA at Config 2026) programmatically β€” `figma_create_slot` adds slots to components (variants included), `figma_append_to_slot` populates instance slots with cloned or new content, plus `figma_get_slots`, `figma_reset_slot`, and `figma_add_slot_property`. Live-validated against the GA Plugin API; closes the most-requested issue [#29](https://github.com/southleft/figma-console-mcp/issues/29), based on community PR [#77](https://github.com/southleft/figma-console-mcp/pull/77). **Plugin re-import required** (`code.js` + `ui.html` changed). [See what's new β†’](CHANGELOG.md#1350---2026-07-09)
12
12
 
13
13
  ## What is this?
14
14
 
@@ -55,9 +55,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
55
55
  | Real-time monitoring (console, selection) | βœ… | ❌ | ❌ |
56
56
  | Desktop Bridge plugin | βœ… | βœ… | ❌ |
57
57
  | Requires Node.js | Yes | **No** | No |
58
- | **Total tools available** | **107** | **96** | **9** |
58
+ | **Total tools available** | **112** | **101** | **9** |
59
59
 
60
- > **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (96 tools) from web AI clients without Node.js. NPX/Local Git gives the full 107 tools with real-time monitoring.
60
+ > **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (101 tools) from web AI clients without Node.js. NPX/Local Git gives the full 112 tools with real-time monitoring.
61
61
 
62
62
  ---
63
63
 
@@ -65,7 +65,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
65
65
 
66
66
  **Best for:** Designers who want full AI-assisted design capabilities.
67
67
 
68
- **What you get:** All 107 tools including design creation, variable management, and component instantiation.
68
+ **What you get:** All 112 tools including design creation, variable management, and component instantiation.
69
69
 
70
70
  #### Prerequisites
71
71
 
@@ -162,7 +162,7 @@ Create a simple frame with a blue background
162
162
 
163
163
  **Best for:** Developers who want to modify source code or contribute to the project.
164
164
 
165
- **What you get:** Same 107 tools as NPX, plus full source code access.
165
+ **What you get:** Same 112 tools as NPX, plus full source code access.
166
166
 
167
167
  #### Quick Setup
168
168
 
@@ -251,7 +251,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
251
251
 
252
252
  **Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs β€” no Node.js required.
253
253
 
254
- **What you get:** 96 tools including full write access β€” design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
254
+ **What you get:** 101 tools including full write access β€” design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
255
255
 
256
256
  #### Prerequisites
257
257
 
@@ -308,7 +308,7 @@ AI Client β†’ Cloud MCP Server β†’ Durable Object Relay β†’ Desktop Bridge Plugi
308
308
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
309
309
  |---------|-------------------|------------|-----------|------------|
310
310
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
311
- | **Total tools** | **107** | **96** | **107** | **9** (read-only) |
311
+ | **Total tools** | **112** | **101** | **112** | **9** (read-only) |
312
312
  | **Design creation** | βœ… | βœ… | βœ… | ❌ |
313
313
  | **Variable management** | βœ… | βœ… | βœ… | ❌ |
314
314
  | **Component instantiation** | βœ… | βœ… | βœ… | ❌ |
@@ -323,7 +323,7 @@ AI Client β†’ Cloud MCP Server β†’ Durable Object Relay β†’ Desktop Bridge Plugi
323
323
  | **Automatic updates** | βœ… (`@latest`) | βœ… | Manual (`git pull`) | βœ… |
324
324
  | **Source code access** | ❌ | ❌ | βœ… | ❌ |
325
325
 
326
- > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 107 tools.
326
+ > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 112 tools.
327
327
 
328
328
  **πŸ“– [Complete Feature Comparison](docs/mode-comparison.md)**
329
329
 
@@ -431,6 +431,13 @@ When you first use design system tools:
431
431
  - Supports markdown formatting for rich documentation
432
432
  - Descriptions appear in Dev Mode for developers
433
433
 
434
+ ### 🧩 Figma Slots (Local Mode + Cloud Mode)
435
+ - `figma_create_slot` - **Add a slot to a component** via the GA `createSlot()` API β€” the linked SLOT property is created automatically; works on standalone components and variants inside a component set
436
+ - `figma_get_slots` - List slots on a component, component set (aggregated across variants), or instance β€” ids, names, property keys, dimensions, and current children
437
+ - `figma_append_to_slot` - **Populate an instance's slot** by cloning an existing node or creating new content (`setProperties` rejects slot values by design β€” this is the population path)
438
+ - `figma_reset_slot` - Clear a slot's content on an instance
439
+ - `figma_add_slot_property` - Retrofit an existing frame as a slot via a manual SLOT property binding, with `description` and `preferredValues`
440
+
434
441
  ### πŸ” Design-Code Parity (All Modes)
435
442
  - `figma_check_design_parity` - Compare Figma component specs against code implementation, producing a scored diff report with actionable fix items
436
443
  - `figma_generate_component_doc` - Generate platform-agnostic markdown documentation by merging Figma design data with code-side info
@@ -673,7 +680,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
673
680
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
674
681
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
675
682
  - The plugin scans all ports in the range and connects to every active server it finds
676
- - All 107 tools work through the WebSocket transport
683
+ - All 112 tools work through the WebSocket transport
677
684
 
678
685
  **Multiple files:** The WebSocket server supports multiple simultaneous plugin connections β€” one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs).
679
686
 
@@ -812,9 +819,10 @@ The architecture supports adding new apps with minimal boilerplate β€” each app
812
819
 
813
820
  ## πŸ›€οΈ Roadmap
814
821
 
815
- **Current Status:** v1.34.0 (Stable) - Production-ready. Latest: Bidirectional Token Sync v2 + DTCG 2025.10 β€” `figma_import_tokens` now applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5Γ— is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog β€” fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **107 tools** (Local) / **96 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
822
+ **Current Status:** v1.35.0 (Stable) - Production-ready. Latest: Figma Slots write support β€” create, inspect, populate, and reset Slots (GA at Config 2026) via 5 new tools (`figma_create_slot`, `figma_get_slots`, `figma_append_to_slot`, `figma_reset_slot`, `figma_add_slot_property`), live-validated against the GA Plugin API and based on community PR #77. On top of v1.34.0's Bidirectional Token Sync v2 + DTCG 2025.10 β€” `figma_import_tokens` applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5Γ— is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog β€” fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **112 tools** (Local) / **101 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
816
823
 
817
824
  **Recent Releases:**
825
+ - [x] **v1.35.0** - Figma Slots write support, closing [#29](https://github.com/southleft/figma-console-mcp/issues/29) (the tracker's most-requested feature) via community PR [#77](https://github.com/southleft/figma-console-mcp/pull/77) by @simonesalvucci, updated to the GA API. Five new tools: `figma_create_slot` (slot + auto-linked SLOT property; variants inside component sets supported β€” the beta restriction was lifted at GA), `figma_get_slots`, `figma_append_to_slot` (clone or create content into instance slots; snaps clones to the slot origin), `figma_reset_slot`, and `figma_add_slot_property` (retrofit an existing frame as a slot). Hardened by live validation: VARIANT `defaultValue` passthrough restored (Figma requires non-empty), destructive-path reorder in the append handler (content validated before `clearExisting` empties anything), relay whitelist fix for `slot` payloads. **Plugin re-import required** (`code.js` + `ui.html` changed).
818
826
  - [x] **v1.34.0** - Bidirectional Token Sync v2 + DTCG 2025.10. `figma_import_tokens` now applies the *complete* diff plan: missing collections and variables are created (with modes, inferred/recorded types, and values set in dependency order β€” aliases in a second pass), token-path renames route to the update phase by round-trip variable ID (no more create+delete pairs that would permanently destroy the original under `replace`), reference values write real `{ type: "VARIABLE_ALIAS", id }` payloads via a four-tier resolver, and deletes are strictly gated behind `strategy: "replace"`. `figma_export_tokens` gains `dtcgDialect: "2025"` (object-form colors from full-precision floats, object dimensions) while the legacy default stays byte-identical; import accepts both dialects unconditionally with dialect-insensitive diff normalization. Variable `scopes` + `codeSyntax` round-trip through `$extensions["figma-console-mcp"]`. `figma_setup_design_tokens` accepts DTCG brace references (`"{color.blue.600}"`) that resolve to real aliases, including forward references. New tool `figma_create_component_set` builds a variant set from an axes matrix (or combines existing components) with `Prop=Value` naming, optional auto-arranged grid, and variant keys in the response β€” with count-scaled timeouts and rollback on failure. **Plugin re-import required** (`code.js` + `ui.html` changed β€” the component-set handler and relay). 183 tests across the token/write-tools suites.
819
827
  - [x] **v1.33.2** - Version-handshake false-positive fix. The v1.33.0 handshake compared the plugin's reported version against the server's *package* version, so server-only releases (like the v1.33.1 dependency sweep) flagged every up-to-date plugin as stale and pushed the re-import banner for files that hadn't changed. The server now compares against the `PLUGIN_VERSION` embedded in the `figma-desktop-bridge/code.js` it ships β€” exactly what a re-import would install β€” and `PLUGIN_VERSION` itself now means "last release in which plugin files changed" (release tooling bumps it only when `figma-desktop-bridge/` actually changed since the last tag). `figma_get_status` gains `transport.websocket.bundledPluginVersion`; `figma_diagnose` blames the right version. No new tools, **no plugin re-import required** (one-time exception: if you re-imported at v1.33.1, the banner appears once more β€” clear it with one final re-import). 1245 tests passing (9 new).
820
828
  - [x] **v1.33.1** - Security dependency sweep. All runtime and critical npm audit alerts resolved via in-range bumps (`ws` 8.21.0, `hono` 4.12.27, `undici` 7.28.0, `handlebars` 4.7.9 β€” the lone critical, dev-only β€” plus `lodash`, `path-to-regexp`, `basic-ftp`, `fast-uri`, `vite`). `wrangler` deliberately held at 4.72.0 because newer versions require Node β‰₯22; the only residual audit findings are inside wrangler/miniflare's dev-time toolchain, which never ships in the npm package or Worker bundle. Supersedes dependabot PRs #81/#82/#84. No code changes, no API changes, no plugin re-import. 1236 tests passing unchanged.
@@ -154,6 +154,8 @@ export class CloudWebSocketConnector {
154
154
  const params = { nodeId, propertyName, propertyType: type, defaultValue };
155
155
  if (options?.preferredValues)
156
156
  params.preferredValues = options.preferredValues;
157
+ if (options?.description)
158
+ params.description = options.description;
157
159
  return this.sendCommand('ADD_COMPONENT_PROPERTY', params);
158
160
  }
159
161
  async editComponentProperty(nodeId, propertyName, newValue) {
@@ -199,6 +201,21 @@ export class CloudWebSocketConnector {
199
201
  return this.sendCommand('CREATE_COMPONENT_SET', params, timeout);
200
202
  }
201
203
  // ============================================================================
204
+ // Slot operations
205
+ // ============================================================================
206
+ async createSlot(nodeId, options) {
207
+ return this.sendCommand('CREATE_SLOT', { nodeId, ...options });
208
+ }
209
+ async getSlots(nodeId) {
210
+ return this.sendCommand('GET_SLOTS', { nodeId });
211
+ }
212
+ async appendToSlot(params) {
213
+ return this.sendCommand('APPEND_TO_SLOT', params);
214
+ }
215
+ async resetSlot(params) {
216
+ return this.sendCommand('RESET_SLOT', params);
217
+ }
218
+ // ============================================================================
202
219
  // Node manipulation
203
220
  // ============================================================================
204
221
  async resizeNode(nodeId, width, height, withConstraints = true) {
@@ -3229,7 +3229,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
3229
3229
  // Tool 16: Set Instance Properties (Desktop Bridge)
3230
3230
  // Updates component properties on an instance using setProperties()
3231
3231
  // This is the correct way to update TEXT/BOOLEAN/VARIANT properties on component instances
3232
- server.tool("figma_set_instance_properties", "Update component properties on a component instance. IMPORTANT: Use this tool instead of trying to edit text nodes directly when working with component instances. Components often expose TEXT, BOOLEAN, INSTANCE_SWAP, and VARIANT properties that control their content. Direct text node editing may fail silently if the component uses properties. This tool handles the #nodeId suffix pattern automatically. Requires Desktop Bridge connection.", {
3232
+ server.tool("figma_set_instance_properties", "Update component properties on a component instance. IMPORTANT: Use this tool instead of trying to edit text nodes directly when working with component instances. Components often expose TEXT, BOOLEAN, INSTANCE_SWAP, and VARIANT properties that control their content. SLOT properties CANNOT be set here β€” use figma_append_to_slot to populate slot content. Direct text node editing may fail silently if the component uses properties. This tool handles the #nodeId suffix pattern automatically. Requires Desktop Bridge connection.", {
3233
3233
  nodeId: z
3234
3234
  .string()
3235
3235
  .describe("ID of the INSTANCE node to update (e.g., '1:234'). Must be a component instance, not a regular frame."),
@@ -0,0 +1,334 @@
1
+ import { z } from "zod";
2
+ import { createChildLogger } from "./logger.js";
3
+ const logger = createChildLogger({ component: "slot-tools" });
4
+ const SLOT_LAYOUT_MODES = ["NONE", "HORIZONTAL", "VERTICAL"];
5
+ const SLOT_CHILD_NODE_TYPES = [
6
+ "FRAME",
7
+ "RECTANGLE",
8
+ "ELLIPSE",
9
+ "TEXT",
10
+ "LINE",
11
+ "POLYGON",
12
+ "STAR",
13
+ "VECTOR",
14
+ ];
15
+ const preferredValueSchema = z.object({
16
+ type: z.enum(["COMPONENT", "COMPONENT_SET"]).describe("Type of preferred value"),
17
+ key: z.string().describe("Component or component set key"),
18
+ });
19
+ /**
20
+ * Register MCP tools for Figma Slots (open beta).
21
+ * Requires Desktop Bridge and a Figma Desktop version with SlotNode API support.
22
+ */
23
+ export function registerSlotTools(server, getDesktopConnector) {
24
+ server.tool("figma_create_slot", `Create a SlotNode inside a component using createSlot(). Automatically creates a linked SLOT component property named after the slot (renaming the slot renames the property). Slots are freeform drop zones for instance content β€” more flexible than INSTANCE_SWAP. Works on standalone COMPONENTs and variant COMPONENTs inside a COMPONENT_SET (call once per variant). GRID layout is not allowed on slots. Requires Desktop Bridge.`, {
25
+ nodeId: z
26
+ .string()
27
+ .describe("The COMPONENT node ID to add a slot to"),
28
+ name: z
29
+ .string()
30
+ .optional()
31
+ .describe("Slot layer name (e.g. 'Content', 'Footer')"),
32
+ width: z
33
+ .number()
34
+ .optional()
35
+ .describe("Initial slot width in pixels"),
36
+ height: z
37
+ .number()
38
+ .optional()
39
+ .describe("Initial slot height in pixels"),
40
+ layoutMode: z
41
+ .enum(SLOT_LAYOUT_MODES)
42
+ .optional()
43
+ .describe("Auto-layout mode for the slot (GRID is not supported)"),
44
+ }, async ({ nodeId, name, width, height, layoutMode }) => {
45
+ try {
46
+ const connector = await getDesktopConnector();
47
+ const result = await connector.createSlot(nodeId, {
48
+ name,
49
+ width,
50
+ height,
51
+ layoutMode,
52
+ });
53
+ if (!result.success) {
54
+ throw new Error(result.error || "Failed to create slot");
55
+ }
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: JSON.stringify({
61
+ success: true,
62
+ slot: result.slot,
63
+ hint: "Use figma_get_slots to inspect slots. Populate instance slots with figma_append_to_slot (not figma_set_instance_properties).",
64
+ }),
65
+ },
66
+ ],
67
+ };
68
+ }
69
+ catch (error) {
70
+ logger.error({ error }, "figma_create_slot failed");
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: JSON.stringify({
76
+ error: error instanceof Error ? error.message : String(error),
77
+ hint: "Target a COMPONENT node (standalone or a variant inside a COMPONENT_SET). Ensure Figma Desktop supports Slots.",
78
+ }),
79
+ },
80
+ ],
81
+ isError: true,
82
+ };
83
+ }
84
+ });
85
+ server.tool("figma_get_slots", `List SlotNode children on a component, component set, or instance. Returns slot IDs, names, property keys, dimensions, and current child nodes. Use on instances before figma_append_to_slot to discover slot names/IDs.`, {
86
+ nodeId: z
87
+ .string()
88
+ .describe("COMPONENT, COMPONENT_SET, or INSTANCE node ID"),
89
+ }, async ({ nodeId }) => {
90
+ try {
91
+ const connector = await getDesktopConnector();
92
+ const result = await connector.getSlots(nodeId);
93
+ if (!result.success) {
94
+ throw new Error(result.error || "Failed to get slots");
95
+ }
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: JSON.stringify({ success: true, ...(result.data ?? {}) }),
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ catch (error) {
106
+ logger.error({ error }, "figma_get_slots failed");
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: JSON.stringify({
112
+ error: error instanceof Error ? error.message : String(error),
113
+ hint: "Ensure the node is a component or instance with slots, and Desktop Bridge is connected.",
114
+ }),
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+ });
121
+ server.tool("figma_append_to_slot", `Add content to a slot on a component instance. Clones sourceNodeId into the slot, or creates a new node (nodeType). SLOT content cannot be set via figma_set_instance_properties β€” use this tool instead. Widgets, stickies, and raw ComponentNodes cannot be appended to slots.`, {
122
+ slotId: z
123
+ .string()
124
+ .optional()
125
+ .describe("Direct SlotNode ID (from figma_get_slots)"),
126
+ instanceId: z
127
+ .string()
128
+ .optional()
129
+ .describe("Instance ID β€” use with slotName when slotId is unknown"),
130
+ slotName: z
131
+ .string()
132
+ .optional()
133
+ .describe("Slot layer name on the instance (e.g. 'Content')"),
134
+ sourceNodeId: z
135
+ .string()
136
+ .optional()
137
+ .describe("Node to clone into the slot (default: clone=true)"),
138
+ nodeType: z
139
+ .enum(SLOT_CHILD_NODE_TYPES)
140
+ .optional()
141
+ .describe("Create a new node in the slot instead of cloning"),
142
+ properties: z
143
+ .record(z.string(), z.union([z.string(), z.number()]))
144
+ .optional()
145
+ .describe("Properties for created nodes: name, text, width, height"),
146
+ clone: z
147
+ .boolean()
148
+ .optional()
149
+ .default(true)
150
+ .describe("When using sourceNodeId, clone the node (default true). Set false to move."),
151
+ clearExisting: z
152
+ .boolean()
153
+ .optional()
154
+ .default(false)
155
+ .describe("Remove existing slot children before appending"),
156
+ }, async (args) => {
157
+ try {
158
+ if (!args.slotId && !(args.instanceId && args.slotName)) {
159
+ throw new Error("Provide slotId OR (instanceId + slotName)");
160
+ }
161
+ if (!args.sourceNodeId && !args.nodeType) {
162
+ throw new Error("Provide sourceNodeId (clone into slot) or nodeType (create new content)");
163
+ }
164
+ const connector = await getDesktopConnector();
165
+ const result = await connector.appendToSlot(args);
166
+ if (!result.success) {
167
+ throw new Error(result.error || "Failed to append to slot");
168
+ }
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: JSON.stringify({
174
+ success: true,
175
+ slot: result.slot,
176
+ appendedNode: result.appendedNode,
177
+ hint: "Use figma_capture_screenshot to verify slot content visually.",
178
+ }),
179
+ },
180
+ ],
181
+ };
182
+ }
183
+ catch (error) {
184
+ logger.error({ error }, "figma_append_to_slot failed");
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: JSON.stringify({
190
+ error: error instanceof Error ? error.message : String(error),
191
+ hints: [
192
+ "Use figma_get_slots on the instance to find slot IDs/names",
193
+ "Clone instances or frames β€” not raw ComponentNodes",
194
+ "SLOT properties cannot be set via figma_set_instance_properties",
195
+ ],
196
+ }),
197
+ },
198
+ ],
199
+ isError: true,
200
+ };
201
+ }
202
+ });
203
+ server.tool("figma_reset_slot", `Reset a slot on a component instance to its default (empty) state from the main component. Uses SlotNode.resetSlot().`, {
204
+ slotId: z
205
+ .string()
206
+ .optional()
207
+ .describe("Direct SlotNode ID"),
208
+ instanceId: z
209
+ .string()
210
+ .optional()
211
+ .describe("Instance ID β€” use with slotName when slotId is unknown"),
212
+ slotName: z
213
+ .string()
214
+ .optional()
215
+ .describe("Slot layer name on the instance"),
216
+ }, async ({ slotId, instanceId, slotName }) => {
217
+ try {
218
+ if (!slotId && !(instanceId && slotName)) {
219
+ throw new Error("Provide slotId OR (instanceId + slotName)");
220
+ }
221
+ const connector = await getDesktopConnector();
222
+ const result = await connector.resetSlot({ slotId, instanceId, slotName });
223
+ if (!result.success) {
224
+ throw new Error(result.error || "Failed to reset slot");
225
+ }
226
+ return {
227
+ content: [
228
+ {
229
+ type: "text",
230
+ text: JSON.stringify({
231
+ success: true,
232
+ slot: result.slot,
233
+ }),
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ catch (error) {
239
+ logger.error({ error }, "figma_reset_slot failed");
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: JSON.stringify({
245
+ error: error instanceof Error ? error.message : String(error),
246
+ }),
247
+ },
248
+ ],
249
+ isError: true,
250
+ };
251
+ }
252
+ });
253
+ server.tool("figma_add_slot_property", `Manually add a SLOT component property and bind it to an existing frame (alternative to figma_create_slot). The frame must be a direct child of the component, must not use GRID layout, and must not be nested inside another slot. Supports description and preferredValues.`, {
254
+ nodeId: z.string().describe("COMPONENT or COMPONENT_SET node ID"),
255
+ propertyName: z.string().describe("Slot property name (e.g. 'Content')"),
256
+ frameNodeId: z
257
+ .string()
258
+ .describe("Frame node ID to bind as the slot content area"),
259
+ description: z
260
+ .string()
261
+ .optional()
262
+ .describe("Slot property description (SLOT-only)"),
263
+ preferredValues: z
264
+ .array(preferredValueSchema)
265
+ .optional()
266
+ .describe("Preferred components for slot content (SLOT-only)"),
267
+ }, async ({ nodeId, propertyName, frameNodeId, description, preferredValues }) => {
268
+ try {
269
+ const connector = await getDesktopConnector();
270
+ const code = `
271
+ const component = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
272
+ if (!component || (component.type !== 'COMPONENT' && component.type !== 'COMPONENT_SET')) {
273
+ throw new Error('Node must be COMPONENT or COMPONENT_SET');
274
+ }
275
+ const frame = await figma.getNodeByIdAsync(${JSON.stringify(frameNodeId)});
276
+ if (!frame || frame.type !== 'FRAME') {
277
+ throw new Error('frameNodeId must be a FRAME node');
278
+ }
279
+ // For a COMPONENT the frame must be its direct child. For a COMPONENT_SET the
280
+ // property lives on the set but the frame lives inside a variant COMPONENT β€”
281
+ // accept a frame whose parent is a variant of this set.
282
+ const directChild = frame.parent === component;
283
+ const variantChild = component.type === 'COMPONENT_SET' &&
284
+ frame.parent && frame.parent.type === 'COMPONENT' && frame.parent.parent === component;
285
+ if (!directChild && !variantChild) {
286
+ throw new Error(component.type === 'COMPONENT_SET'
287
+ ? 'Frame must be a direct child of one of the set\\'s variant components'
288
+ : 'Frame must be a direct child of the component');
289
+ }
290
+ if (frame.layoutMode === 'GRID') {
291
+ throw new Error('GRID layoutMode is not allowed on slot frames');
292
+ }
293
+ const options = {};
294
+ ${description ? `options.description = ${JSON.stringify(description)};` : ""}
295
+ ${preferredValues ? `options.preferredValues = ${JSON.stringify(preferredValues)};` : ""}
296
+ const propKey = component.addComponentProperty(${JSON.stringify(propertyName)}, 'SLOT', '', Object.keys(options).length ? options : undefined);
297
+ // Merge β€” assigning a fresh object would wipe the frame's existing bindings
298
+ // (e.g. a BOOLEAN property driving 'visible').
299
+ frame.componentPropertyReferences = Object.assign({}, frame.componentPropertyReferences, { slotContentId: propKey });
300
+ return { propertyKey: propKey, frameId: frame.id, frameName: frame.name };
301
+ `;
302
+ const result = await connector.executeCodeViaUI(code, 10000);
303
+ if (!result.success) {
304
+ throw new Error(result.error || "Failed to add slot property");
305
+ }
306
+ return {
307
+ content: [
308
+ {
309
+ type: "text",
310
+ text: JSON.stringify({
311
+ success: true,
312
+ result: result.result,
313
+ hint: "Prefer figma_create_slot() for new slots β€” it creates the SlotNode and property automatically.",
314
+ }),
315
+ },
316
+ ],
317
+ };
318
+ }
319
+ catch (error) {
320
+ logger.error({ error }, "figma_add_slot_property failed");
321
+ return {
322
+ content: [
323
+ {
324
+ type: "text",
325
+ text: JSON.stringify({
326
+ error: error instanceof Error ? error.message : String(error),
327
+ }),
328
+ },
329
+ ],
330
+ isError: true,
331
+ };
332
+ }
333
+ });
334
+ }
@@ -35,7 +35,7 @@ const logger = createChildLogger({ component: "tokens-tools" });
35
35
  * on every exported token document. Kept in sync with package.json by
36
36
  * scripts/release.sh β€” see step 3 of the release flow.
37
37
  */
38
- const MCP_VERSION = "1.34.0";
38
+ const MCP_VERSION = "1.35.0";
39
39
  const EXPORT_TOOL_DESCRIPTION = `Export Figma variables to design token files in your codebase. Bidirectional with figma_import_tokens β€” together they replace Style Dictionary and Tokens Studio's export pipeline for the popular styling methods.
40
40
 
41
41
  FULLY-IMPLEMENTED OUTPUT FORMATS:
@@ -196,6 +196,8 @@ export class WebSocketConnector {
196
196
  const params = { nodeId, propertyName, propertyType: type, defaultValue };
197
197
  if (options?.preferredValues)
198
198
  params.preferredValues = options.preferredValues;
199
+ if (options?.description)
200
+ params.description = options.description;
199
201
  return this.wsServer.sendCommand('ADD_COMPONENT_PROPERTY', params);
200
202
  }
201
203
  async editComponentProperty(nodeId, propertyName, newValue) {
@@ -231,6 +233,21 @@ export class WebSocketConnector {
231
233
  return this.wsServer.sendCommand('CREATE_COMPONENT_SET', params, componentSetTimeoutMs(params));
232
234
  }
233
235
  // ============================================================================
236
+ // Slot operations
237
+ // ============================================================================
238
+ async createSlot(nodeId, options) {
239
+ return this.wsServer.sendCommand('CREATE_SLOT', { nodeId, ...options });
240
+ }
241
+ async getSlots(nodeId) {
242
+ return this.wsServer.sendCommand('GET_SLOTS', { nodeId });
243
+ }
244
+ async appendToSlot(params) {
245
+ return this.wsServer.sendCommand('APPEND_TO_SLOT', params);
246
+ }
247
+ async resetSlot(params) {
248
+ return this.wsServer.sendCommand('RESET_SLOT', params);
249
+ }
250
+ // ============================================================================
234
251
  // Node manipulation
235
252
  // ============================================================================
236
253
  async resizeNode(nodeId, width, height, withConstraints = true) {
@@ -1076,21 +1076,52 @@ After instantiating components, use figma_take_screenshot to verify the result l
1076
1076
  }
1077
1077
  });
1078
1078
  // Tool: Add Component Property
1079
- server.tool("figma_add_component_property", "Add a new component property to a component or component set. Properties enable dynamic content and behavior in component instances. Supported types: BOOLEAN (toggle), TEXT (string), INSTANCE_SWAP (component swap), VARIANT (variant selection).", {
1079
+ server.tool("figma_add_component_property", "Add a new component property to a component or component set. Properties enable dynamic content and behavior in component instances. Supported types: BOOLEAN (toggle), TEXT (string), INSTANCE_SWAP (component swap), VARIANT (variant selection), SLOT (freeform slot β€” prefer figma_create_slot for new slots).", {
1080
1080
  nodeId: z.string().describe("The component or component set node ID"),
1081
1081
  propertyName: z
1082
1082
  .string()
1083
- .describe("Name for the new property (e.g., 'Show Icon', 'Button Label')"),
1083
+ .describe("Name for the new property (e.g., 'Show Icon', 'Button Label', 'Content')"),
1084
1084
  type: z
1085
- .enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT"])
1086
- .describe("Property type: BOOLEAN for toggles, TEXT for strings, INSTANCE_SWAP for component swaps, VARIANT for variant selection"),
1085
+ .enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT", "SLOT"])
1086
+ .describe("Property type: BOOLEAN for toggles, TEXT for strings, INSTANCE_SWAP for component swaps, VARIANT for variant selection, SLOT for freeform slot areas"),
1087
1087
  defaultValue: z
1088
1088
  .union([z.string(), z.number(), z.boolean()])
1089
- .describe("Default value for the property. BOOLEAN: true/false, TEXT: string, INSTANCE_SWAP: component key, VARIANT: variant value"),
1090
- }, async ({ nodeId, propertyName, type, defaultValue }) => {
1089
+ .optional()
1090
+ .describe("Default value. Required for TEXT (string), INSTANCE_SWAP (component id), and VARIANT (a non-empty variant option name β€” Figma rejects empty). Optional for BOOLEAN (defaults false). Omit for SLOT."),
1091
+ description: z
1092
+ .string()
1093
+ .optional()
1094
+ .describe("Property description (SLOT properties only)"),
1095
+ preferredValues: z
1096
+ .array(z.object({
1097
+ type: z.enum(["COMPONENT", "COMPONENT_SET"]),
1098
+ key: z.string(),
1099
+ }))
1100
+ .optional()
1101
+ .describe("Preferred components (INSTANCE_SWAP and SLOT only)"),
1102
+ }, async ({ nodeId, propertyName, type, defaultValue, description, preferredValues }) => {
1091
1103
  try {
1092
1104
  const connector = await getDesktopConnector();
1093
- const result = await connector.addComponentProperty(nodeId, propertyName, type, defaultValue);
1105
+ const options = {};
1106
+ if (description)
1107
+ options.description = description;
1108
+ if (preferredValues)
1109
+ options.preferredValues = preferredValues;
1110
+ // SLOT is the only type that takes no default. VARIANT requires a
1111
+ // non-empty default (Figma rejects ''), and TEXT/INSTANCE_SWAP fail
1112
+ // deep in the plugin with opaque errors when the default is missing β€”
1113
+ // validate here so the caller gets an actionable message.
1114
+ if ((type === "TEXT" || type === "INSTANCE_SWAP" || type === "VARIANT") &&
1115
+ (defaultValue === undefined || defaultValue === "")) {
1116
+ throw new Error(`defaultValue is required for ${type} properties` +
1117
+ (type === "INSTANCE_SWAP"
1118
+ ? " (a component id)"
1119
+ : type === "VARIANT"
1120
+ ? " (a non-empty variant option name)"
1121
+ : ""));
1122
+ }
1123
+ const resolvedDefault = type === "SLOT" ? "" : (defaultValue ?? (type === "BOOLEAN" ? false : ""));
1124
+ const result = await connector.addComponentProperty(nodeId, propertyName, type, resolvedDefault, Object.keys(options).length > 0 ? options : undefined);
1094
1125
  if (!result.success) {
1095
1126
  throw new Error(result.error || "Failed to add property");
1096
1127
  }
@@ -1184,7 +1215,7 @@ After instantiating components, use figma_take_screenshot to verify the result l
1184
1215
  }
1185
1216
  });
1186
1217
  // Tool: Delete Component Property
1187
- server.tool("figma_delete_component_property", "Delete a component property. Only works with BOOLEAN, TEXT, and INSTANCE_SWAP properties (not VARIANT). This is a destructive operation.", {
1218
+ server.tool("figma_delete_component_property", "Delete a component property. Works with BOOLEAN, TEXT, INSTANCE_SWAP, and SLOT properties (not VARIANT). This is a destructive operation.", {
1188
1219
  nodeId: z.string().describe("The component or component set node ID"),
1189
1220
  propertyName: z
1190
1221
  .string()
@@ -1216,7 +1247,7 @@ After instantiating components, use figma_take_screenshot to verify the result l
1216
1247
  type: "text",
1217
1248
  text: JSON.stringify({
1218
1249
  error: error instanceof Error ? error.message : String(error),
1219
- hint: "Cannot delete VARIANT properties. Only BOOLEAN, TEXT, and INSTANCE_SWAP can be deleted.",
1250
+ hint: "Cannot delete VARIANT properties. BOOLEAN, TEXT, INSTANCE_SWAP, and SLOT can be deleted.",
1220
1251
  }),
1221
1252
  },
1222
1253
  ],