figma-rust-mcp 3.0.2 → 3.0.4
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/package.json +1 -1
- package/plugin/code.js +76 -21
- package/plugin/ui.html +122 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
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-29T14:34:20.483Z
|
|
5
5
|
|
|
6
6
|
// ── plugin-src/utils.js ──
|
|
7
7
|
// ─── UTILS ────────────────────────────────────────────────────────────────────
|
|
@@ -1348,7 +1348,7 @@ const handlers = {};
|
|
|
1348
1348
|
|
|
1349
1349
|
handlers.status = async () => ({
|
|
1350
1350
|
connected: true,
|
|
1351
|
-
version: "3.0.
|
|
1351
|
+
version: "3.0.4",
|
|
1352
1352
|
fileName: figma.root.name,
|
|
1353
1353
|
currentPage: figma.currentPage.name,
|
|
1354
1354
|
pageCount: figma.root.children.length,
|
|
@@ -1359,19 +1359,54 @@ handlers.listPages = async () =>
|
|
|
1359
1359
|
figma.root.children.map(p => ({ id: p.id, name: p.name }));
|
|
1360
1360
|
|
|
1361
1361
|
handlers.setPage = async (params) => {
|
|
1362
|
-
|
|
1363
|
-
|
|
1362
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
1363
|
+
try { await figma.loadAllPagesAsync(); } catch (e) {}
|
|
1364
|
+
}
|
|
1365
|
+
params = params || {};
|
|
1366
|
+
var name = typeof params === "string" ? params : (params.name || params.pageName || params.title || (typeof params.page === "string" ? params.page : (params.page && typeof params.page.name === "string" ? params.page.name : null)));
|
|
1367
|
+
var id = (typeof params === "object" && params !== null) ? (params.id || params.pageId || (params.page && typeof params.page.id === "string" ? params.page.id : null)) : null;
|
|
1368
|
+
var index = (typeof params === "number") ? params : ((typeof params === "object" && params !== null) ? (params.index !== undefined ? params.index : params.pageIndex) : undefined);
|
|
1369
|
+
|
|
1364
1370
|
var page = null;
|
|
1365
|
-
if (id)
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (!page
|
|
1371
|
+
if (id) {
|
|
1372
|
+
page = figma.root.children.find(function(p) { return p.id === id; });
|
|
1373
|
+
}
|
|
1374
|
+
if (!page && name) {
|
|
1375
|
+
page = figma.root.children.find(function(p) { return p.name === name; });
|
|
1376
|
+
if (!page) {
|
|
1377
|
+
// Case-insensitive & trimmed match fallback
|
|
1378
|
+
var lowerName = String(name).trim().toLowerCase();
|
|
1379
|
+
page = figma.root.children.find(function(p) { return p.name.trim().toLowerCase() === lowerName; });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (!page && typeof index === "number" && index >= 0 && index < figma.root.children.length) {
|
|
1383
|
+
page = figma.root.children[index];
|
|
1384
|
+
}
|
|
1385
|
+
if (!page && !name && !id && index === undefined && figma.root.children.length === 1) {
|
|
1386
|
+
page = figma.root.children[0];
|
|
1387
|
+
}
|
|
1388
|
+
if (!page) {
|
|
1389
|
+
var requested = name || id || (index !== undefined ? ("index " + index) : null);
|
|
1390
|
+
var available = figma.root.children.map(function(p) { return p.name; }).join(", ");
|
|
1391
|
+
if (requested) {
|
|
1392
|
+
throw new Error("Page not found: \"" + requested + "\". Available: " + available);
|
|
1393
|
+
} else {
|
|
1394
|
+
throw new Error("setPage requires a valid page name, id, or index. Available: " + available);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1369
1397
|
await figma.setCurrentPageAsync(page);
|
|
1370
1398
|
return { id: page.id, name: page.name };
|
|
1371
1399
|
};
|
|
1372
1400
|
|
|
1373
|
-
handlers.createPage = async (
|
|
1374
|
-
|
|
1401
|
+
handlers.createPage = async (params) => {
|
|
1402
|
+
if (typeof figma.loadAllPagesAsync === "function") {
|
|
1403
|
+
try { await figma.loadAllPagesAsync(); } catch (e) {}
|
|
1404
|
+
}
|
|
1405
|
+
var name = typeof params === "string" ? params : (params && (params.name || params.pageName || params.title));
|
|
1406
|
+
if (!name || typeof name !== "string") {
|
|
1407
|
+
throw new Error("createPage requires a valid page name string");
|
|
1408
|
+
}
|
|
1409
|
+
const existing = figma.root.children.find(p => p.name === name || p.name.trim().toLowerCase() === name.trim().toLowerCase());
|
|
1375
1410
|
if (existing) return { id: existing.id, name: existing.name, existed: true };
|
|
1376
1411
|
const page = figma.createPage();
|
|
1377
1412
|
page.name = name;
|
|
@@ -1379,13 +1414,15 @@ handlers.createPage = async ({ name }) => {
|
|
|
1379
1414
|
};
|
|
1380
1415
|
|
|
1381
1416
|
handlers.query = async (params) => {
|
|
1382
|
-
|
|
1417
|
+
params = params || {};
|
|
1418
|
+
var type = params.type;
|
|
1419
|
+
var name = params.name || params.nodeName || params.query || params.q;
|
|
1420
|
+
var id = params.id || params.nodeId;
|
|
1383
1421
|
var parentId = params.parentId, limit = params.limit || 500;
|
|
1384
1422
|
if (id) {
|
|
1385
1423
|
const n = await findNodeByIdAsync(id);
|
|
1386
1424
|
return n ? [nodeToInfo(n)] : [];
|
|
1387
1425
|
}
|
|
1388
|
-
if (!type && !name) throw new Error("query requires at least one of: type, name, id");
|
|
1389
1426
|
// BUG-03: parentId filter scopes search to a subtree; default cap raised 100 → 500.
|
|
1390
1427
|
var scope = figma.currentPage;
|
|
1391
1428
|
if (parentId) {
|
|
@@ -1394,10 +1431,14 @@ handlers.query = async (params) => {
|
|
|
1394
1431
|
if (typeof parent.findAll !== "function") return [];
|
|
1395
1432
|
scope = parent;
|
|
1396
1433
|
}
|
|
1434
|
+
if (!type && !name) {
|
|
1435
|
+
var allChildren = scope.children || [];
|
|
1436
|
+
return allChildren.slice(0, limit).map(nodeToInfo);
|
|
1437
|
+
}
|
|
1397
1438
|
const results = scope.findAll(n => {
|
|
1398
|
-
if (type && name) return n.type === type && n.name === name;
|
|
1439
|
+
if (type && name) return n.type === type && (n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase())));
|
|
1399
1440
|
if (type) return n.type === type;
|
|
1400
|
-
return n.name === name;
|
|
1441
|
+
return n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase()));
|
|
1401
1442
|
});
|
|
1402
1443
|
return results.slice(0, limit).map(nodeToInfo);
|
|
1403
1444
|
};
|
|
@@ -3094,23 +3135,26 @@ async function exportNodeSvg(node) {
|
|
|
3094
3135
|
|
|
3095
3136
|
// export_svg — export node as SVG string
|
|
3096
3137
|
handlers.export_svg = async function(params) {
|
|
3097
|
-
var id = params ? params.id : null;
|
|
3098
|
-
var nodeName = params ? params.name : null;
|
|
3138
|
+
var id = params ? (params.id || params.nodeId) : null;
|
|
3139
|
+
var nodeName = params ? (params.name || params.nodeName) : null;
|
|
3099
3140
|
var node = null;
|
|
3100
3141
|
if (id) node = await findNodeByIdAsync(id);
|
|
3101
3142
|
if (!node && nodeName) {
|
|
3102
3143
|
node = findNodeByName(nodeName);
|
|
3103
3144
|
}
|
|
3145
|
+
if (!node && figma.currentPage.selection.length > 0) {
|
|
3146
|
+
node = figma.currentPage.selection[0];
|
|
3147
|
+
}
|
|
3104
3148
|
if (!node) node = figma.currentPage;
|
|
3105
|
-
if (!node) throw new Error("Node not found");
|
|
3149
|
+
if (!node) throw new Error("Node not found for export");
|
|
3106
3150
|
var svg = await exportNodeSvg(node);
|
|
3107
3151
|
return { svg: svg, nodeId: node.id, width: Math.round(node.width), height: Math.round(node.height) };
|
|
3108
3152
|
};
|
|
3109
3153
|
|
|
3110
3154
|
// export_image — export node as base64 PNG/JPG (for saving to disk, not for inline display)
|
|
3111
3155
|
handlers.export_image = async function(params) {
|
|
3112
|
-
var id = params ? params.id : null;
|
|
3113
|
-
var nodeName = params ? params.name : null;
|
|
3156
|
+
var id = params ? (params.id || params.nodeId) : null;
|
|
3157
|
+
var nodeName = params ? (params.name || params.nodeName) : null;
|
|
3114
3158
|
var format = (params && params.format) ? params.format.toUpperCase() : "PNG";
|
|
3115
3159
|
var scale = (params && params.scale) ? params.scale : 2;
|
|
3116
3160
|
|
|
@@ -3121,6 +3165,16 @@ handlers.export_image = async function(params) {
|
|
|
3121
3165
|
if (!node && nodeName) {
|
|
3122
3166
|
node = findNodeByName(nodeName);
|
|
3123
3167
|
}
|
|
3168
|
+
if (!node && figma.currentPage.selection.length > 0) {
|
|
3169
|
+
node = figma.currentPage.selection[0];
|
|
3170
|
+
}
|
|
3171
|
+
if (!node) {
|
|
3172
|
+
var children = figma.currentPage.children || [];
|
|
3173
|
+
var exportableTypes = ["FRAME", "COMPONENT", "COMPONENT_SET", "SECTION", "INSTANCE", "GROUP"];
|
|
3174
|
+
for (var i = 0; i < children.length; i++) {
|
|
3175
|
+
if (exportableTypes.indexOf(children[i].type) !== -1) { node = children[i]; break; }
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3124
3178
|
if (!node) throw new Error("Node not found for export");
|
|
3125
3179
|
|
|
3126
3180
|
// BUG-05 fix: same as screenshot — force render before export, then put the
|
|
@@ -4165,8 +4219,9 @@ handlers.get_variables = async function() {
|
|
|
4165
4219
|
return { collections: collections, resolvedTokens: resolvedTokensMap };
|
|
4166
4220
|
};
|
|
4167
4221
|
|
|
4168
|
-
handlers.
|
|
4169
|
-
handlers.
|
|
4222
|
+
handlers.get_variable_tokens = handlers.get_variables;
|
|
4223
|
+
handlers.get_tokens = handlers.get_variables;
|
|
4224
|
+
handlers.tokens = handlers.get_variables;
|
|
4170
4225
|
|
|
4171
4226
|
// ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
|
|
4172
4227
|
handlers.export_assets = async function(params) {
|
package/plugin/ui.html
CHANGED
|
@@ -457,6 +457,18 @@
|
|
|
457
457
|
}
|
|
458
458
|
.t-tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
|
|
459
459
|
.t-tab.active { color: var(--text-primary); background: var(--surface-card); box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
|
|
460
|
+
.t-tab.tab-err.active { color: var(--ghost-rose); }
|
|
461
|
+
.tab-badge-err {
|
|
462
|
+
font-size: 7.5px;
|
|
463
|
+
font-weight: 700;
|
|
464
|
+
color: var(--ghost-rose);
|
|
465
|
+
background: rgba(251, 113, 133, 0.18);
|
|
466
|
+
border-radius: var(--radius-full);
|
|
467
|
+
padding: 0 4px;
|
|
468
|
+
margin-left: 2px;
|
|
469
|
+
line-height: 1.3;
|
|
470
|
+
display: inline-block;
|
|
471
|
+
}
|
|
460
472
|
|
|
461
473
|
.terminal-meta { display: flex; align-items: center; gap: 6px; }
|
|
462
474
|
|
|
@@ -810,9 +822,16 @@
|
|
|
810
822
|
<button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
|
|
811
823
|
<button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
|
|
812
824
|
<button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
|
|
825
|
+
<button class="t-tab tab-err" id="tab-errors" onclick="setLogFilter('errors', this)">Errors <span class="tab-badge-err" id="tab-err-badge" style="display:none">0</span></button>
|
|
813
826
|
</div>
|
|
814
827
|
<div class="terminal-meta">
|
|
815
828
|
<span class="event-badge" id="log-count">0 ev</span>
|
|
829
|
+
<button class="act-btn icon-only" onclick="copyLogs()" title="Copy console logs (respects active tab)" style="height:18px;width:18px;border-radius:3px;">
|
|
830
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
831
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
832
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
833
|
+
</svg>
|
|
834
|
+
</button>
|
|
816
835
|
<button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
|
|
817
836
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
818
837
|
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
|
@@ -933,15 +952,35 @@
|
|
|
933
952
|
if (el) el.classList.add("active");
|
|
934
953
|
|
|
935
954
|
var lines = logEl.querySelectorAll(".log-line");
|
|
955
|
+
var visibleCount = 0;
|
|
936
956
|
lines.forEach(function(line) {
|
|
957
|
+
var match = false;
|
|
937
958
|
if (filter === 'all') {
|
|
938
|
-
|
|
959
|
+
match = true;
|
|
939
960
|
} else if (filter === 'writes') {
|
|
940
|
-
|
|
961
|
+
match = line.dataset.type === 'write';
|
|
941
962
|
} else if (filter === 'reads') {
|
|
942
|
-
|
|
963
|
+
match = line.dataset.type === 'read';
|
|
964
|
+
} else if (filter === 'errors') {
|
|
965
|
+
match = line.dataset.cls === 'err';
|
|
943
966
|
}
|
|
967
|
+
line.style.display = match ? "flex" : "none";
|
|
968
|
+
if (match) visibleCount++;
|
|
944
969
|
});
|
|
970
|
+
|
|
971
|
+
if (logEmptyEl) {
|
|
972
|
+
if (visibleCount === 0 && totalLogEvents > 0) {
|
|
973
|
+
logEmptyEl.style.display = "flex";
|
|
974
|
+
var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
|
|
975
|
+
if (emptyText) emptyText.textContent = filter === 'errors' ? "No errors logged" : "No matching logs";
|
|
976
|
+
} else if (totalLogEvents === 0) {
|
|
977
|
+
logEmptyEl.style.display = "flex";
|
|
978
|
+
var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
|
|
979
|
+
if (emptyText) emptyText.textContent = "Listening on MCP bridge…";
|
|
980
|
+
} else {
|
|
981
|
+
logEmptyEl.style.display = "none";
|
|
982
|
+
}
|
|
983
|
+
}
|
|
945
984
|
}
|
|
946
985
|
|
|
947
986
|
async function fetchServerStats() {
|
|
@@ -1224,8 +1263,76 @@
|
|
|
1224
1263
|
}, 500);
|
|
1225
1264
|
}
|
|
1226
1265
|
|
|
1266
|
+
var totalErrors = 0;
|
|
1267
|
+
|
|
1268
|
+
function updateErrorBadge() {
|
|
1269
|
+
var badge = document.getElementById("tab-err-badge");
|
|
1270
|
+
if (!badge) return;
|
|
1271
|
+
if (totalErrors > 0) {
|
|
1272
|
+
badge.textContent = totalErrors;
|
|
1273
|
+
badge.style.display = "inline-block";
|
|
1274
|
+
} else {
|
|
1275
|
+
badge.style.display = "none";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function copyTextToClipboard(text, successMsg) {
|
|
1280
|
+
if (!text) return;
|
|
1281
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1282
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
1283
|
+
showToast(successMsg || "Copied!", "✓");
|
|
1284
|
+
}).catch(function() {
|
|
1285
|
+
fallbackCopy(text, successMsg);
|
|
1286
|
+
});
|
|
1287
|
+
} else {
|
|
1288
|
+
fallbackCopy(text, successMsg);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function fallbackCopy(text, successMsg) {
|
|
1293
|
+
try {
|
|
1294
|
+
var ta = document.createElement("textarea");
|
|
1295
|
+
ta.value = text;
|
|
1296
|
+
ta.style.position = "fixed";
|
|
1297
|
+
ta.style.left = "-9999px";
|
|
1298
|
+
document.body.appendChild(ta);
|
|
1299
|
+
ta.select();
|
|
1300
|
+
document.execCommand("copy");
|
|
1301
|
+
document.body.removeChild(ta);
|
|
1302
|
+
showToast(successMsg || "Copied!", "✓");
|
|
1303
|
+
} catch(e) {
|
|
1304
|
+
showToast("Copy failed", "✗");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function copyLogs() {
|
|
1309
|
+
var lines = logEl.querySelectorAll(".log-line");
|
|
1310
|
+
var visibleTexts = [];
|
|
1311
|
+
lines.forEach(function(line) {
|
|
1312
|
+
if (line.style.display !== "none") {
|
|
1313
|
+
var ts = line.querySelector(".log-ts");
|
|
1314
|
+
var msg = line.querySelector(".log-msg");
|
|
1315
|
+
var text = (ts ? ts.textContent + " " : "") + (msg ? msg.textContent : line.textContent);
|
|
1316
|
+
visibleTexts.push(text.trim());
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
if (visibleTexts.length === 0) {
|
|
1321
|
+
showToast(activeLogFilter === 'errors' ? "No errors to copy" : "No logs to copy", "ℹ");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
var fullText = visibleTexts.join("\n");
|
|
1326
|
+
var label = activeLogFilter === 'errors' ? "Errors copied (" + visibleTexts.length + ")!" : "Logs copied (" + visibleTexts.length + ")!";
|
|
1327
|
+
copyTextToClipboard(fullText, label);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1227
1330
|
function log(msg, cls, opType) {
|
|
1228
1331
|
totalLogEvents++;
|
|
1332
|
+
if (cls === 'err') {
|
|
1333
|
+
totalErrors++;
|
|
1334
|
+
updateErrorBadge();
|
|
1335
|
+
}
|
|
1229
1336
|
if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
|
|
1230
1337
|
|
|
1231
1338
|
if (logEmptyEl) logEmptyEl.style.display = "none";
|
|
@@ -1235,6 +1342,9 @@
|
|
|
1235
1342
|
var line = document.createElement("div");
|
|
1236
1343
|
line.className = "log-line";
|
|
1237
1344
|
if (opType) line.dataset.type = opType;
|
|
1345
|
+
if (cls) line.dataset.cls = cls;
|
|
1346
|
+
line.title = "Click to copy";
|
|
1347
|
+
line.style.cursor = "pointer";
|
|
1238
1348
|
|
|
1239
1349
|
var ts = document.createElement("span");
|
|
1240
1350
|
ts.className = "log-ts";
|
|
@@ -1252,10 +1362,17 @@
|
|
|
1252
1362
|
line.appendChild(prompt);
|
|
1253
1363
|
line.appendChild(content);
|
|
1254
1364
|
|
|
1365
|
+
line.addEventListener("click", function() {
|
|
1366
|
+
var text = (ts ? ts.textContent + " " : "") + (content ? content.textContent : line.textContent);
|
|
1367
|
+
copyTextToClipboard(text.trim(), "Copied line!");
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1255
1370
|
if (activeLogFilter === 'writes' && opType !== 'write') {
|
|
1256
1371
|
line.style.display = "none";
|
|
1257
1372
|
} else if (activeLogFilter === 'reads' && opType !== 'read') {
|
|
1258
1373
|
line.style.display = "none";
|
|
1374
|
+
} else if (activeLogFilter === 'errors' && cls !== 'err') {
|
|
1375
|
+
line.style.display = "none";
|
|
1259
1376
|
}
|
|
1260
1377
|
|
|
1261
1378
|
logEl.appendChild(line);
|
|
@@ -1273,6 +1390,8 @@
|
|
|
1273
1390
|
function clearLog() {
|
|
1274
1391
|
logEl.innerHTML = "";
|
|
1275
1392
|
totalLogEvents = 0;
|
|
1393
|
+
totalErrors = 0;
|
|
1394
|
+
updateErrorBadge();
|
|
1276
1395
|
if (logCountEl) logCountEl.textContent = "0 ev";
|
|
1277
1396
|
var empty = document.createElement("div");
|
|
1278
1397
|
empty.id = "log-empty";
|