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.
- package/README.md +12 -1
- package/package.json +1 -1
- 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 **
|
|
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.
|
|
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-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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
|
|
3476
|
+
var variable = await getVariableSafeAsync(alias.id);
|
|
3322
3477
|
if (variable) {
|
|
3323
3478
|
entry.name = variable.name;
|
|
3324
3479
|
entry.resolvedType = variable.resolvedType;
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
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
|
|
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
|
-
|
|
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) ────────────────────────────
|