figma-rust-mcp 3.0.0 → 3.0.1

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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/package.json +1 -1
  3. package/plugin/code.js +189 -17
package/README.md CHANGED
@@ -147,7 +147,7 @@ Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.js
147
147
 
148
148
  ## 🛠️ MCP Tools Reference
149
149
 
150
- `figma-mcp` provides **14 first-class MCP tools**:
150
+ `figma-mcp` provides **15 first-class MCP tools**:
151
151
 
152
152
  ### 1. `figma_status`
153
153
  Checks live bridge connection status, connected Figma tabs/files, in-memory index health, queue length, and latency statistics.
@@ -230,6 +230,17 @@ Scans your local codebase directories (`src/components`, `components/ui`) to dis
230
230
  - **`projectDir`**: Base directory of your project (default: current directory `"."`).
231
231
  - **`nodeId`**: Optional Figma node ID to check specific match against local components.
232
232
 
233
+ ### 15. `figma_prepare_design`
234
+ **All-In-One Multimodal Grounding Pack for AI**: The ultimate single-call tool for code generation that eliminates missing UI elements and icon guesswork.
235
+ - Simultaneously extracts **100% of visible text elements** (greeting, badges, timestamps, labels).
236
+ - Automatically **exports all SVG vector icons** into your local project directory (`iconDir`) and provides exact TypeScript import statements.
237
+ - Captures high-res **visual canvas screenshots** saved locally for immediate multimodal LLM inspection.
238
+ - Discovers existing components in your codebase for direct reuse.
239
+ - Generates an actionable **Implementation Checklist** ensuring complete design fidelity in 1 single shot.
240
+ - **`nodeId`**: Target Figma screen/frame ID.
241
+ - **`iconDir`**: Target directory for SVG icons (e.g. `src/assets/icons` or `assets/images`).
242
+ - **`projectDir`**: Base directory for component discovery.
243
+
233
244
  ---
234
245
 
235
246
  ## 💻 Development & Testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-rust-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
5
5
  "mcpName": "io.github.BuiHung1612/figma-mcp",
6
6
  "type": "module",
package/plugin/code.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // figma-mcp — Figma Plugin main thread v1.2.1
2
2
  // Handles both WRITE (draw UI) and READ (extract design) operations.
3
3
  // AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
4
- // Generated: 2026-08-27T08:12:15.747Z
4
+ // Generated: 2026-08-29T04:54:16.262Z
5
5
 
6
6
  // ── plugin-src/utils.js ──
7
7
  // ─── UTILS ────────────────────────────────────────────────────────────────────
@@ -113,6 +113,109 @@ function solidStroke(hex, strokeOpacity) {
113
113
  return [stroke];
114
114
  }
115
115
 
116
+ // ── Variable & Token Deep Resolver ───────────────────────────────────────────
117
+ var variableCache = new Map();
118
+
119
+ async function getVariableSafeAsync(id) {
120
+ if (!id) return null;
121
+ if (variableCache.has(id)) return variableCache.get(id);
122
+ try {
123
+ if (figma.variables && typeof figma.variables.getVariableByIdAsync === "function") {
124
+ var v = await figma.variables.getVariableByIdAsync(id);
125
+ if (v) variableCache.set(id, v);
126
+ return v;
127
+ }
128
+ } catch(e) {}
129
+ return null;
130
+ }
131
+
132
+ // Recursively resolves a variable (and its VARIABLE_ALIAS chains) to concrete primitive value & hex
133
+ async function resolveVariableValueAsync(variableOrId, contextNodeOrModeMap, depth, visited) {
134
+ if (!variableOrId) return null;
135
+ depth = depth || 0;
136
+ visited = visited || {};
137
+ if (depth > 6) return null;
138
+
139
+ var variable = null;
140
+ if (typeof variableOrId === "string") {
141
+ if (visited[variableOrId]) return null;
142
+ visited[variableOrId] = true;
143
+ variable = await getVariableSafeAsync(variableOrId);
144
+ } else {
145
+ variable = variableOrId;
146
+ if (variable && variable.id) {
147
+ if (visited[variable.id]) return null;
148
+ visited[variable.id] = true;
149
+ }
150
+ }
151
+
152
+ if (!variable || !variable.valuesByMode) return null;
153
+
154
+ // Determine active mode
155
+ var modeId = null;
156
+ var colId = variable.variableCollectionId;
157
+ if (contextNodeOrModeMap) {
158
+ if (typeof contextNodeOrModeMap === "object" && contextNodeOrModeMap.resolvedVariableModes) {
159
+ modeId = contextNodeOrModeMap.resolvedVariableModes[colId];
160
+ } else if (typeof contextNodeOrModeMap === "object" && contextNodeOrModeMap[colId]) {
161
+ modeId = contextNodeOrModeMap[colId];
162
+ } else if (typeof contextNodeOrModeMap === "string") {
163
+ modeId = contextNodeOrModeMap;
164
+ }
165
+ }
166
+ var availableModes = Object.keys(variable.valuesByMode);
167
+ if (!modeId || variable.valuesByMode[modeId] === undefined) {
168
+ modeId = availableModes.length > 0 ? availableModes[0] : null;
169
+ }
170
+ if (!modeId) return null;
171
+
172
+ var rawVal = variable.valuesByMode[modeId];
173
+ if (rawVal === undefined || rawVal === null) return null;
174
+
175
+ // If alias, follow recursively down to primitive token
176
+ if (typeof rawVal === "object" && rawVal.type === "VARIABLE_ALIAS" && rawVal.id) {
177
+ var targetResult = await resolveVariableValueAsync(rawVal.id, contextNodeOrModeMap, depth + 1, visited);
178
+ return {
179
+ type: "ALIAS",
180
+ name: variable.name,
181
+ variableId: variable.id,
182
+ resolvedType: variable.resolvedType,
183
+ targetId: rawVal.id,
184
+ targetName: targetResult ? (targetResult.primitiveName || targetResult.name) : null,
185
+ primitiveName: targetResult ? (targetResult.primitiveName || targetResult.name) : variable.name,
186
+ resolvedValue: targetResult ? targetResult.resolvedValue : null,
187
+ hex: targetResult ? targetResult.hex : null,
188
+ raw: rawVal,
189
+ };
190
+ }
191
+
192
+ // If RGBA color
193
+ if (typeof rawVal === "object" && "r" in rawVal && "g" in rawVal && "b" in rawVal) {
194
+ var hex = rgbToHex(rawVal);
195
+ var alpha = rawVal.a !== undefined ? Math.round(rawVal.a * 100) / 100 : 1;
196
+ return {
197
+ type: "COLOR",
198
+ name: variable.name,
199
+ variableId: variable.id,
200
+ resolvedType: "COLOR",
201
+ resolvedValue: hex,
202
+ hex: hex,
203
+ alpha: alpha,
204
+ primitiveName: variable.name,
205
+ };
206
+ }
207
+
208
+ // Primitive value
209
+ return {
210
+ type: variable.resolvedType || typeof rawVal,
211
+ name: variable.name,
212
+ variableId: variable.id,
213
+ resolvedType: variable.resolvedType || typeof rawVal,
214
+ resolvedValue: rawVal,
215
+ primitiveName: variable.name,
216
+ };
217
+ }
218
+
116
219
  // Paint/effect helpers (buildFillArray, buildGradientPaint, buildEffect,
117
220
  // applyEffects, applyCornerRadii) live in paint-and-effects.js.
118
221
 
@@ -1245,7 +1348,7 @@ const handlers = {};
1245
1348
 
1246
1349
  handlers.status = async () => ({
1247
1350
  connected: true,
1248
- version: "3.0.0",
1351
+ version: "3.0.1",
1249
1352
  fileName: figma.root.name,
1250
1353
  currentPage: figma.currentPage.name,
1251
1354
  pageCount: figma.root.children.length,
@@ -3198,6 +3301,26 @@ handlers.get_node_detail = async function(params) {
3198
3301
  } else if (f.type === "IMAGE") {
3199
3302
  fd.scaleMode = f.scaleMode || "FILL";
3200
3303
  }
3304
+
3305
+ // Link bound variable token if present
3306
+ try {
3307
+ if (node.boundVariables && node.boundVariables.fills) {
3308
+ var fBindings = Array.isArray(node.boundVariables.fills) ? node.boundVariables.fills : [node.boundVariables.fills];
3309
+ if (fBindings[fi] && fBindings[fi].id) {
3310
+ var boundVar = await resolveVariableValueAsync(fBindings[fi].id, node, 0);
3311
+ if (boundVar) {
3312
+ fd.boundToken = {
3313
+ id: fBindings[fi].id,
3314
+ name: boundVar.name,
3315
+ primitive: boundVar.primitiveName,
3316
+ resolvedHex: boundVar.hex || boundVar.resolvedValue,
3317
+ };
3318
+ if (boundVar.hex) fd.color = boundVar.hex;
3319
+ }
3320
+ }
3321
+ }
3322
+ } catch(e3) {}
3323
+
3201
3324
  detail.fills.push(fd);
3202
3325
  }
3203
3326
  }
@@ -3207,14 +3330,46 @@ handlers.get_node_detail = async function(params) {
3207
3330
  try {
3208
3331
  if (node.strokes && node.strokes.length) {
3209
3332
  var dStrokes = node.strokes;
3333
+ var strokeBindings = (node.boundVariables && node.boundVariables.strokes) ? (Array.isArray(node.boundVariables.strokes) ? node.boundVariables.strokes : [node.boundVariables.strokes]) : [];
3334
+
3210
3335
  if (dStrokes.length === 1 && dStrokes[0].type === "SOLID") {
3211
3336
  detail.stroke = rgbToHex(dStrokes[0].color);
3337
+ try {
3338
+ if (strokeBindings[0] && strokeBindings[0].id) {
3339
+ var boundStrokeVar = await resolveVariableValueAsync(strokeBindings[0].id, node, 0);
3340
+ if (boundStrokeVar) {
3341
+ detail.strokeBoundToken = {
3342
+ id: strokeBindings[0].id,
3343
+ name: boundStrokeVar.name,
3344
+ primitive: boundStrokeVar.primitiveName,
3345
+ resolvedHex: boundStrokeVar.hex || boundStrokeVar.resolvedValue,
3346
+ };
3347
+ if (boundStrokeVar.hex) detail.stroke = boundStrokeVar.hex;
3348
+ }
3349
+ }
3350
+ } catch(e4) {}
3212
3351
  } else {
3213
- detail.strokes = dStrokes.map(function(s) {
3352
+ detail.strokes = [];
3353
+ for (var si = 0; si < dStrokes.length; si++) {
3354
+ var s = dStrokes[si];
3214
3355
  var sd = { type: s.type };
3215
3356
  if (s.type === "SOLID") sd.color = rgbToHex(s.color);
3216
- return sd;
3217
- });
3357
+ try {
3358
+ if (strokeBindings[si] && strokeBindings[si].id) {
3359
+ var bsv = await resolveVariableValueAsync(strokeBindings[si].id, node, 0);
3360
+ if (bsv) {
3361
+ sd.boundToken = {
3362
+ id: strokeBindings[si].id,
3363
+ name: bsv.name,
3364
+ primitive: bsv.primitiveName,
3365
+ resolvedHex: bsv.hex || bsv.resolvedValue,
3366
+ };
3367
+ if (bsv.hex) sd.color = bsv.hex;
3368
+ }
3369
+ }
3370
+ } catch(e5) {}
3371
+ detail.strokes.push(sd);
3372
+ }
3218
3373
  }
3219
3374
  detail.strokeWeight = node.strokeWeight;
3220
3375
  detail.strokeAlign = node.strokeAlign;
@@ -3302,7 +3457,7 @@ handlers.get_node_detail = async function(params) {
3302
3457
  try { detail.textAlign = node.textAlignHorizontal ? node.textAlignHorizontal.toLowerCase() : null; } catch(e) {}
3303
3458
  }
3304
3459
 
3305
- // P1: Resolve bound variables → name + resolvedType + value (not just IDs)
3460
+ // P1: Resolve bound variables → name + resolvedType + value (deep recursive resolution)
3306
3461
  try {
3307
3462
  if (node.boundVariables) {
3308
3463
  var bv = {};
@@ -3318,16 +3473,18 @@ handlers.get_node_detail = async function(params) {
3318
3473
  if (!alias || !alias.id) continue;
3319
3474
  var entry = { id: alias.id };
3320
3475
  try {
3321
- var variable = await figma.variables.getVariableByIdAsync(alias.id);
3476
+ var variable = await getVariableSafeAsync(alias.id);
3322
3477
  if (variable) {
3323
3478
  entry.name = variable.name;
3324
3479
  entry.resolvedType = variable.resolvedType;
3325
- // Get first mode value as preview
3326
- var modeIds = Object.keys(variable.valuesByMode);
3327
- if (modeIds.length > 0) {
3328
- var val = variable.valuesByMode[modeIds[0]];
3329
- if (val && typeof val === "object" && "r" in val) entry.value = rgbToHex(val);
3330
- else entry.value = val;
3480
+ var resolvedToken = await resolveVariableValueAsync(variable, node, 0);
3481
+ if (resolvedToken) {
3482
+ entry.value = resolvedToken.resolvedValue !== undefined ? resolvedToken.resolvedValue : (resolvedToken.hex || resolvedToken.value);
3483
+ if (resolvedToken.type === "ALIAS") {
3484
+ entry.aliasTarget = resolvedToken.targetName;
3485
+ entry.primitiveName = resolvedToken.primitiveName;
3486
+ entry.resolvedValue = resolvedToken.resolvedValue;
3487
+ }
3331
3488
  }
3332
3489
  }
3333
3490
  } catch(e2) {}
@@ -3944,22 +4101,37 @@ handlers.get_variables = async function() {
3944
4101
  }
3945
4102
  } catch(e) { /* fall back to per-id lookups */ }
3946
4103
 
4104
+ var resolvedTokensMap = {};
4105
+
3947
4106
  for (var ci = 0; ci < localCollections.length; ci++) {
3948
4107
  var col = localCollections[ci];
3949
4108
  var variables = [];
3950
4109
  for (var vi = 0; vi < col.variableIds.length; vi++) {
3951
4110
  var vid = col.variableIds[vi];
3952
- var v = bulkLoaded ? varsById[vid] : await figma.variables.getVariableByIdAsync(vid);
4111
+ var v = bulkLoaded ? varsById[vid] : await getVariableSafeAsync(vid);
3953
4112
  if (!v) continue;
3954
4113
  var values = {};
3955
4114
  for (var modeId in v.valuesByMode) {
3956
4115
  if (Object.prototype.hasOwnProperty.call(v.valuesByMode, modeId)) {
3957
4116
  var val = v.valuesByMode[modeId];
3958
- // Convert color values to hex
3959
4117
  if (val && typeof val === "object" && "r" in val && "g" in val && "b" in val) {
3960
- values[modeId] = rgbToHex(val);
4118
+ var hexVal = rgbToHex(val);
4119
+ values[modeId] = hexVal;
4120
+ resolvedTokensMap[v.name] = hexVal;
4121
+ } else if (val && typeof val === "object" && val.type === "VARIABLE_ALIAS" && val.id) {
4122
+ var resolvedAlias = await resolveVariableValueAsync(val.id, modeId, 1);
4123
+ var finalHexOrVal = resolvedAlias ? (resolvedAlias.resolvedValue || resolvedAlias.hex || resolvedAlias.value) : null;
4124
+ values[modeId] = {
4125
+ type: "VARIABLE_ALIAS",
4126
+ aliasId: val.id,
4127
+ aliasName: resolvedAlias ? resolvedAlias.name : null,
4128
+ primitiveName: resolvedAlias ? resolvedAlias.primitiveName : null,
4129
+ resolvedValue: finalHexOrVal,
4130
+ };
4131
+ if (finalHexOrVal) resolvedTokensMap[v.name] = finalHexOrVal;
3961
4132
  } else {
3962
4133
  values[modeId] = val;
4134
+ if (val !== undefined && val !== null) resolvedTokensMap[v.name] = val;
3963
4135
  }
3964
4136
  }
3965
4137
  }
@@ -3984,7 +4156,7 @@ handlers.get_variables = async function() {
3984
4156
  } catch(e) {
3985
4157
  return { error: "Variables API not available: " + e.message, collections: [] };
3986
4158
  }
3987
- return { collections: collections };
4159
+ return { collections: collections, resolvedTokens: resolvedTokensMap };
3988
4160
  };
3989
4161
 
3990
4162
  // ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────