figma-rust-mcp 3.0.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-rust-mcp",
3
- "version": "3.0.3",
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-29T13:39:20.933Z
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.3",
1351
+ version: "3.0.4",
1352
1352
  fileName: figma.root.name,
1353
1353
  currentPage: figma.currentPage.name,
1354
1354
  pageCount: figma.root.children.length,
@@ -1414,13 +1414,15 @@ handlers.createPage = async (params) => {
1414
1414
  };
1415
1415
 
1416
1416
  handlers.query = async (params) => {
1417
- var type = params.type, name = params.name, id = params.id;
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;
1418
1421
  var parentId = params.parentId, limit = params.limit || 500;
1419
1422
  if (id) {
1420
1423
  const n = await findNodeByIdAsync(id);
1421
1424
  return n ? [nodeToInfo(n)] : [];
1422
1425
  }
1423
- if (!type && !name) throw new Error("query requires at least one of: type, name, id");
1424
1426
  // BUG-03: parentId filter scopes search to a subtree; default cap raised 100 → 500.
1425
1427
  var scope = figma.currentPage;
1426
1428
  if (parentId) {
@@ -1429,10 +1431,14 @@ handlers.query = async (params) => {
1429
1431
  if (typeof parent.findAll !== "function") return [];
1430
1432
  scope = parent;
1431
1433
  }
1434
+ if (!type && !name) {
1435
+ var allChildren = scope.children || [];
1436
+ return allChildren.slice(0, limit).map(nodeToInfo);
1437
+ }
1432
1438
  const results = scope.findAll(n => {
1433
- 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())));
1434
1440
  if (type) return n.type === type;
1435
- return n.name === name;
1441
+ return n.name === name || (typeof name === "string" && n.name.toLowerCase().includes(name.toLowerCase()));
1436
1442
  });
1437
1443
  return results.slice(0, limit).map(nodeToInfo);
1438
1444
  };
@@ -3129,23 +3135,26 @@ async function exportNodeSvg(node) {
3129
3135
 
3130
3136
  // export_svg — export node as SVG string
3131
3137
  handlers.export_svg = async function(params) {
3132
- var id = params ? params.id : null;
3133
- var nodeName = params ? params.name : null;
3138
+ var id = params ? (params.id || params.nodeId) : null;
3139
+ var nodeName = params ? (params.name || params.nodeName) : null;
3134
3140
  var node = null;
3135
3141
  if (id) node = await findNodeByIdAsync(id);
3136
3142
  if (!node && nodeName) {
3137
3143
  node = findNodeByName(nodeName);
3138
3144
  }
3145
+ if (!node && figma.currentPage.selection.length > 0) {
3146
+ node = figma.currentPage.selection[0];
3147
+ }
3139
3148
  if (!node) node = figma.currentPage;
3140
- if (!node) throw new Error("Node not found");
3149
+ if (!node) throw new Error("Node not found for export");
3141
3150
  var svg = await exportNodeSvg(node);
3142
3151
  return { svg: svg, nodeId: node.id, width: Math.round(node.width), height: Math.round(node.height) };
3143
3152
  };
3144
3153
 
3145
3154
  // export_image — export node as base64 PNG/JPG (for saving to disk, not for inline display)
3146
3155
  handlers.export_image = async function(params) {
3147
- var id = params ? params.id : null;
3148
- var nodeName = params ? params.name : null;
3156
+ var id = params ? (params.id || params.nodeId) : null;
3157
+ var nodeName = params ? (params.name || params.nodeName) : null;
3149
3158
  var format = (params && params.format) ? params.format.toUpperCase() : "PNG";
3150
3159
  var scale = (params && params.scale) ? params.scale : 2;
3151
3160
 
@@ -3156,6 +3165,16 @@ handlers.export_image = async function(params) {
3156
3165
  if (!node && nodeName) {
3157
3166
  node = findNodeByName(nodeName);
3158
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
+ }
3159
3178
  if (!node) throw new Error("Node not found for export");
3160
3179
 
3161
3180
  // BUG-05 fix: same as screenshot — force render before export, then put the
@@ -4200,8 +4219,9 @@ handlers.get_variables = async function() {
4200
4219
  return { collections: collections, resolvedTokens: resolvedTokensMap };
4201
4220
  };
4202
4221
 
4203
- handlers.get_tokens = handlers.get_variable_tokens;
4204
- handlers.tokens = handlers.get_variable_tokens;
4222
+ handlers.get_variable_tokens = handlers.get_variables;
4223
+ handlers.get_tokens = handlers.get_variables;
4224
+ handlers.tokens = handlers.get_variables;
4205
4225
 
4206
4226
  // ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
4207
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
- line.style.display = "flex";
959
+ match = true;
939
960
  } else if (filter === 'writes') {
940
- line.style.display = line.dataset.type === 'write' ? "flex" : "none";
961
+ match = line.dataset.type === 'write';
941
962
  } else if (filter === 'reads') {
942
- line.style.display = line.dataset.type === 'read' ? "flex" : "none";
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";