figma-console-mcp 1.19.0 → 1.19.2

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
@@ -51,9 +51,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
51
51
  | Real-time monitoring (console, selection) | ✅ | ❌ | ❌ |
52
52
  | Desktop Bridge plugin | ✅ | ✅ | ❌ |
53
53
  | Requires Node.js | Yes | **No** | No |
54
- | **Total tools available** | **89+** | **43** | **22** |
54
+ | **Total tools available** | **90+** | **43** | **22** |
55
55
 
56
- > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 89+ tools with real-time monitoring.
56
+ > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 90+ tools with real-time monitoring.
57
57
 
58
58
  ---
59
59
 
@@ -61,7 +61,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
61
61
 
62
62
  **Best for:** Designers who want full AI-assisted design capabilities.
63
63
 
64
- **What you get:** All 89+ tools including design creation, variable management, and component instantiation.
64
+ **What you get:** All 90+ tools including design creation, variable management, and component instantiation.
65
65
 
66
66
  #### Prerequisites
67
67
 
@@ -123,7 +123,7 @@ If you're not sure where to put the JSON configuration above, here's where each
123
123
  #### Step 3: Connect to Figma Desktop
124
124
 
125
125
  **Desktop Bridge Plugin:**
126
- 1. Open Figma Desktop normally (no special flags needed)
126
+ 1. Open Figma Desktop normally (no special flags needed) and open a file
127
127
  2. Go to **Plugins → Development → Import plugin from manifest...**
128
128
  3. Select `~/.figma-console-mcp/plugin/manifest.json` (stable path, auto-created by the MCP server)
129
129
  4. Run the plugin in your Figma file — the bootloader finds the MCP server and loads the latest UI automatically
@@ -156,7 +156,7 @@ Create a simple frame with a blue background
156
156
 
157
157
  **Best for:** Developers who want to modify source code or contribute to the project.
158
158
 
159
- **What you get:** Same 89+ tools as NPX, plus full source code access.
159
+ **What you get:** Same 90+ tools as NPX, plus full source code access.
160
160
 
161
161
  #### Quick Setup
162
162
 
@@ -302,7 +302,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
302
302
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
303
303
  |---------|-------------------|------------|-----------|------------|
304
304
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
305
- | **Total tools** | **89+** | **43** | **89+** | **22** (read-only) |
305
+ | **Total tools** | **90+** | **43** | **90+** | **22** (read-only) |
306
306
  | **Design creation** | ✅ | ✅ | ✅ | ❌ |
307
307
  | **Variable management** | ✅ | ✅ | ✅ | ❌ |
308
308
  | **Component instantiation** | ✅ | ✅ | ✅ | ❌ |
@@ -317,7 +317,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
317
317
  | **Automatic updates** | ✅ (`@latest`) | ✅ | Manual (`git pull`) | ✅ |
318
318
  | **Source code access** | ❌ | ❌ | ✅ | ❌ |
319
319
 
320
- > **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 89+ tools.
320
+ > **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 90+ tools.
321
321
 
322
322
  **📖 [Complete Feature Comparison](docs/mode-comparison.md)**
323
323
 
@@ -649,7 +649,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
649
649
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
650
650
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
651
651
  - The plugin scans all ports in the range and connects to every active server it finds
652
- - All 89+ tools work through the WebSocket transport
652
+ - All 90+ tools work through the WebSocket transport
653
653
 
654
654
  **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).
655
655
 
@@ -786,7 +786,7 @@ The architecture supports adding new apps with minimal boilerplate — each app
786
786
 
787
787
  ## 🛤️ Roadmap
788
788
 
789
- **Current Status:** v1.17.0 (Stable) - Production-ready with FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, 89+ tools, Comments API, and MCP Apps
789
+ **Current Status:** v1.17.0 (Stable) - Production-ready with FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, 90+ tools, Comments API, and MCP Apps
790
790
 
791
791
  **Recent Releases:**
792
792
  - [x] **v1.17.0** - Figma Slides Support: 15 new tools for managing presentations — slides, transitions, content, reordering, and navigation. Inspired by Toni Haidamous (PR #11).
@@ -142,6 +142,12 @@ export class CloudWebSocketConnector {
142
142
  async getAnnotationCategories() {
143
143
  return this.sendCommand('GET_ANNOTATION_CATEGORIES', {}, 5000);
144
144
  }
145
+ async deepGetComponent(nodeId, depth) {
146
+ return this.sendCommand('DEEP_GET_COMPONENT', { nodeId, depth: depth || 10 }, 30000);
147
+ }
148
+ async analyzeComponentSet(nodeId) {
149
+ return this.sendCommand('ANALYZE_COMPONENT_SET', { nodeId }, 30000);
150
+ }
145
151
  async addComponentProperty(nodeId, propertyName, type, defaultValue, options) {
146
152
  const params = { nodeId, propertyName, propertyType: type, defaultValue };
147
153
  if (options?.preferredValues)
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Deep Component Extraction MCP Tool
3
+ *
4
+ * Provides unlimited-depth component tree extraction via the Desktop Bridge
5
+ * Plugin API. Returns full visual properties, resolved design token names,
6
+ * instance references (mainComponent), prototype reactions, and annotations
7
+ * at every level of the tree.
8
+ *
9
+ * This complements figma_get_component_for_development (REST API, depth 4)
10
+ * with deeper, richer data when the Desktop Bridge plugin is connected.
11
+ */
12
+ import { z } from "zod";
13
+ import { createChildLogger } from "./logger.js";
14
+ const logger = createChildLogger({ component: "deep-component-tools" });
15
+ export function registerDeepComponentTools(server, getDesktopConnector) {
16
+ server.tool("figma_get_component_for_development_deep", "Get a deeply nested component tree with full visual properties, resolved design token names, instance references, prototype interactions, and annotations at every level. Uses the Desktop Bridge Plugin API for unlimited depth traversal — essential for complex components like data tables, nested menus, date pickers, and compound form fields where the standard depth-4 REST API tool misses deeper structure. Returns boundVariables resolved to actual token names (not just IDs), mainComponent references for INSTANCE nodes, and reactions for interaction states. Requires Desktop Bridge plugin. For simpler components (depth ≤ 4), use figma_get_component_for_development instead.", {
17
+ nodeId: z
18
+ .string()
19
+ .describe("Component node ID to extract (e.g., '695:313')"),
20
+ depth: z.preprocess((v) => (typeof v === "string" ? Number(v) : v), z.number().optional().default(10)).describe("Maximum tree depth to traverse (default: 10, max: 20). Use higher values for deeply nested components."),
21
+ }, async ({ nodeId, depth = 10 }) => {
22
+ try {
23
+ const clampedDepth = Math.min(Math.max(depth, 1), 20);
24
+ logger.info({ nodeId, depth: clampedDepth }, "Deep component extraction");
25
+ const connector = await getDesktopConnector();
26
+ const result = await connector.deepGetComponent(nodeId, clampedDepth);
27
+ if (!result || (result.success === false)) {
28
+ throw new Error(result?.error || "Failed to extract component");
29
+ }
30
+ const data = result.data || result;
31
+ // Measure response size and warn if large
32
+ const responseJson = JSON.stringify(data);
33
+ const sizeKB = Math.round(responseJson.length / 1024);
34
+ const response = {
35
+ nodeId,
36
+ component: data,
37
+ metadata: {
38
+ purpose: "deep_component_development",
39
+ treeDepth: clampedDepth,
40
+ responseSizeKB: sizeKB,
41
+ variablesResolved: data._variableMapSize || 0,
42
+ note: [
43
+ `Deep component tree extracted via Plugin API (depth ${clampedDepth}).`,
44
+ "boundVariables are resolved to token names, collections, and codeSyntax.",
45
+ "INSTANCE nodes include mainComponent references (key, name, component set).",
46
+ "Use this data to generate production-quality, token-aware, accessible code.",
47
+ sizeKB > 200 ? "Response is large — consider targeting a specific child node for deeper analysis." : null,
48
+ ].filter(Boolean).join(" "),
49
+ },
50
+ };
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: JSON.stringify(response),
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ logger.error({ error }, "Deep component extraction failed");
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify({
68
+ error: "deep_component_failed",
69
+ message: `Cannot extract deep component. ${message}`,
70
+ hint: "This tool requires the Desktop Bridge plugin to be running in Figma. For REST API fallback (depth 4), use figma_get_component_for_development.",
71
+ }),
72
+ },
73
+ ],
74
+ isError: true,
75
+ };
76
+ }
77
+ });
78
+ // -----------------------------------------------------------------------
79
+ // Tool: figma_analyze_component_set
80
+ // -----------------------------------------------------------------------
81
+ server.tool("figma_analyze_component_set", "Analyze a Figma COMPONENT_SET to extract variant state machine and cross-variant diffs for code generation. Returns: (1) variant axes (size, state) with all values, (2) CSS pseudo-class mappings for interaction states (hover→:hover, focus→:focus-visible, disabled→:disabled, error→[aria-invalid]), (3) visual diff from default state per variant (only changed properties — fill token, stroke token, stroke weight, text color, opacity, effects, visibility), (4) component property definitions mapped to code props (BOOLEAN→boolean, TEXT→string, INSTANCE_SWAP→slot/ReactNode). Use this on the parent COMPONENT_SET node, not individual variants. Requires Desktop Bridge plugin.", {
82
+ nodeId: z
83
+ .string()
84
+ .describe("COMPONENT_SET node ID (the parent of all variants, e.g., '214:274')"),
85
+ }, async ({ nodeId }) => {
86
+ try {
87
+ logger.info({ nodeId }, "Analyzing component set");
88
+ const connector = await getDesktopConnector();
89
+ const result = await connector.analyzeComponentSet(nodeId);
90
+ if (!result || (result.success === false)) {
91
+ throw new Error(result?.error || "Failed to analyze component set");
92
+ }
93
+ const data = result.data || result;
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: JSON.stringify({
99
+ nodeId,
100
+ analysis: data,
101
+ metadata: {
102
+ purpose: "variant_state_machine",
103
+ note: "Use cssMapping to implement interaction states as CSS pseudo-classes/attributes. diffFromDefault shows only what changes per variant — apply as style overrides. componentProps maps to your component's TypeScript interface.",
104
+ },
105
+ }),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ logger.error({ error }, "Component set analysis failed");
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: JSON.stringify({
118
+ error: "analyze_component_set_failed",
119
+ message: `Cannot analyze component set. ${message}`,
120
+ hint: "This tool requires the Desktop Bridge plugin and a COMPONENT_SET node ID (not an individual variant). Use figma_search_components to find component sets.",
121
+ }),
122
+ },
123
+ ],
124
+ isError: true,
125
+ };
126
+ }
127
+ });
128
+ }
@@ -2127,7 +2127,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2127
2127
  logger.info({ fileKey, nodeId, canonicalSource, enrich }, "Starting design-code parity check");
2128
2128
  const api = await getFigmaAPI();
2129
2129
  // Fetch component node
2130
- const nodesResponse = await api.getNodes(fileKey, [nodeId], { depth: 2 });
2130
+ const nodesResponse = await api.getNodes(fileKey, [nodeId], { depth: 4 });
2131
2131
  const nodeData = nodesResponse?.nodes?.[nodeId];
2132
2132
  if (!nodeData?.document) {
2133
2133
  throw new Error(`Node ${nodeId} not found in file ${fileKey}`);
@@ -152,20 +152,116 @@ export class EnrichmentService {
152
152
  }
153
153
  enriched.styles_used = stylesUsed;
154
154
  }
155
- // Extract variables used
156
- if (component.boundVariables) {
157
- const varsUsed = [];
158
- // This would need actual variable resolution
159
- enriched.variables_used = varsUsed;
160
- }
161
- // Detect hardcoded values (simplified for now)
162
- // TODO: Implement full hardcoded value detection
163
- enriched.hardcoded_values = [];
155
+ // Extract variables used and detect hardcoded values by walking the node tree
156
+ const varsUsed = [];
157
+ const hardcodedValues = [];
158
+ const variables = this.extractVariablesMap(data);
159
+ // Walk node tree to find boundVariables and hardcoded values
160
+ const walkForTokens = (node, path = "") => {
161
+ if (!node)
162
+ return;
163
+ const nodePath = path ? `${path} > ${node.name || node.id}` : (node.name || node.id);
164
+ const bv = node.boundVariables || {};
165
+ // Check fills
166
+ if (node.fills && Array.isArray(node.fills)) {
167
+ for (let i = 0; i < node.fills.length; i++) {
168
+ const fill = node.fills[i];
169
+ if (fill.type === "SOLID" && fill.visible !== false) {
170
+ const fillBv = bv.fills;
171
+ if (fillBv && (Array.isArray(fillBv) ? fillBv[i] : fillBv)) {
172
+ const varRef = Array.isArray(fillBv) ? fillBv[i] : fillBv;
173
+ if (varRef?.id) {
174
+ const varInfo = variables.get(varRef.id);
175
+ varsUsed.push({
176
+ variableId: varRef.id,
177
+ variableName: varInfo?.name || varRef.id,
178
+ property: "fill",
179
+ nodePath,
180
+ });
181
+ }
182
+ }
183
+ else {
184
+ // Hardcoded fill
185
+ const c = fill.color;
186
+ if (c) {
187
+ hardcodedValues.push({
188
+ property: "fill",
189
+ value: `rgb(${Math.round(c.r * 255)}, ${Math.round(c.g * 255)}, ${Math.round(c.b * 255)})`,
190
+ nodePath,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // Check strokes
198
+ if (node.strokes && Array.isArray(node.strokes)) {
199
+ for (let i = 0; i < node.strokes.length; i++) {
200
+ const stroke = node.strokes[i];
201
+ if (stroke.type === "SOLID" && stroke.visible !== false) {
202
+ const strokeBv = bv.strokes;
203
+ if (strokeBv && (Array.isArray(strokeBv) ? strokeBv[i] : strokeBv)) {
204
+ const varRef = Array.isArray(strokeBv) ? strokeBv[i] : strokeBv;
205
+ if (varRef?.id) {
206
+ const varInfo = variables.get(varRef.id);
207
+ varsUsed.push({
208
+ variableId: varRef.id,
209
+ variableName: varInfo?.name || varRef.id,
210
+ property: "stroke",
211
+ nodePath,
212
+ });
213
+ }
214
+ }
215
+ else {
216
+ const c = stroke.color;
217
+ if (c) {
218
+ hardcodedValues.push({
219
+ property: "stroke",
220
+ value: `rgb(${Math.round(c.r * 255)}, ${Math.round(c.g * 255)}, ${Math.round(c.b * 255)})`,
221
+ nodePath,
222
+ });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // Check spacing/sizing tokens
229
+ const spacingProps = ["itemSpacing", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "cornerRadius"];
230
+ for (const prop of spacingProps) {
231
+ if (node[prop] !== undefined && node[prop] !== 0) {
232
+ if (bv[prop]?.id) {
233
+ const varInfo = variables.get(bv[prop].id);
234
+ varsUsed.push({
235
+ variableId: bv[prop].id,
236
+ variableName: varInfo?.name || bv[prop].id,
237
+ property: prop,
238
+ nodePath,
239
+ });
240
+ }
241
+ else {
242
+ hardcodedValues.push({
243
+ property: prop,
244
+ value: node[prop],
245
+ nodePath,
246
+ });
247
+ }
248
+ }
249
+ }
250
+ // Recurse into children
251
+ if (node.children && Array.isArray(node.children)) {
252
+ for (const child of node.children) {
253
+ walkForTokens(child, nodePath);
254
+ }
255
+ }
256
+ };
257
+ walkForTokens(component);
258
+ enriched.variables_used = varsUsed;
259
+ enriched.hardcoded_values = hardcodedValues;
164
260
  // Calculate token coverage
165
- const totalProps = (enriched.styles_used?.length || 0) + (enriched.variables_used?.length || 0) + (enriched.hardcoded_values?.length || 0);
166
- const tokenProps = (enriched.styles_used?.length || 0) + (enriched.variables_used?.length || 0);
261
+ const totalProps = varsUsed.length + (enriched.styles_used?.length || 0) + hardcodedValues.length;
262
+ const tokenProps = varsUsed.length + (enriched.styles_used?.length || 0);
167
263
  enriched.token_coverage =
168
- totalProps > 0 ? Math.round((tokenProps / totalProps) * 100) : 0;
264
+ totalProps > 0 ? Math.round((tokenProps / totalProps) * 100) : 100;
169
265
  this.logger.info({ componentId: component.id, coverage: enriched.token_coverage }, "Component enrichment complete");
170
266
  return enriched;
171
267
  }
@@ -341,8 +341,8 @@ export class FigmaAPI {
341
341
  /**
342
342
  * Helper: Get component metadata with properties
343
343
  */
344
- async getComponentData(fileKey, nodeId) {
345
- const response = await this.getNodes(fileKey, [nodeId], { depth: 2 });
344
+ async getComponentData(fileKey, nodeId, depth = 4) {
345
+ const response = await this.getNodes(fileKey, [nodeId], { depth });
346
346
  return response.nodes?.[nodeId];
347
347
  }
348
348
  /**
@@ -937,6 +937,38 @@ export class FigmaDesktopConnector {
937
937
  throw error;
938
938
  }
939
939
  }
940
+ /**
941
+ * Analyze a component set — variant state machine + cross-variant diff
942
+ */
943
+ async analyzeComponentSet(nodeId) {
944
+ logger.info({ nodeId }, 'Analyzing component set via plugin UI');
945
+ const frame = await this.findPluginUIFrame();
946
+ try {
947
+ const result = await frame.evaluate(`window.analyzeComponentSet(${JSON.stringify(nodeId)})`);
948
+ logger.info({ success: result?.success }, 'Component set analysis complete');
949
+ return result;
950
+ }
951
+ catch (error) {
952
+ logger.error({ error, nodeId }, 'Component set analysis failed');
953
+ throw error;
954
+ }
955
+ }
956
+ /**
957
+ * Deep component extraction via plugin UI
958
+ */
959
+ async deepGetComponent(nodeId, depth) {
960
+ logger.info({ nodeId, depth }, 'Deep component fetch via plugin UI');
961
+ const frame = await this.findPluginUIFrame();
962
+ try {
963
+ const result = await frame.evaluate(`window.deepGetComponent(${JSON.stringify(nodeId)}, ${JSON.stringify(depth || 10)})`);
964
+ logger.info({ success: result?.success }, 'Deep component data retrieved');
965
+ return result;
966
+ }
967
+ catch (error) {
968
+ logger.error({ error, nodeId }, 'Deep component fetch failed');
969
+ throw error;
970
+ }
971
+ }
940
972
  /**
941
973
  * Add a component property
942
974
  */