figma-console-mcp 1.41.0 → 1.41.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.
@@ -9,7 +9,7 @@
9
9
  // detect stale cached plugins. Bumped by scripts/release.sh ONLY when plugin files
10
10
  // change (see issue #62); server-only releases leave it alone, so it may lag
11
11
  // package.json — that's intentional, not drift.
12
- var PLUGIN_VERSION = '1.39.0'; // Last release in which plugin files changed.
12
+ var PLUGIN_VERSION = '1.41.1'; // Last release in which plugin files changed.
13
13
 
14
14
  console.log('🌉 [Desktop Bridge] Plugin loaded (v' + PLUGIN_VERSION + ')');
15
15
 
@@ -1196,6 +1196,16 @@ figma.ui.onmessage = async (msg) => {
1196
1196
  }
1197
1197
  }
1198
1198
 
1199
+ // No stroked/frame child: the variant itself carries the visuals
1200
+ // (e.g. a button whose fill sits on the component).
1201
+ if (!mainChild) mainChild = variant;
1202
+
1203
+ // Geometry and structure, so variants that differ in size or content
1204
+ // (2 icons vs 6 icons) don't diff as identical (#121).
1205
+ sig.width = Math.round(variant.width * 100) / 100;
1206
+ sig.height = Math.round(variant.height * 100) / 100;
1207
+ sig.children = variant.children ? variant.children.map(function(c) { return c.name; }) : [];
1208
+
1199
1209
  if (mainChild) {
1200
1210
  var child = mainChild;
1201
1211
  // This is the main interactive frame
@@ -1312,13 +1322,39 @@ figma.ui.onmessage = async (msg) => {
1312
1322
  var stateMachine = { states: {}, defaultState: null, cssMapping: {} };
1313
1323
  var defaultSig = null;
1314
1324
 
1315
- // Find the default variant (first size, default state)
1316
- for (var di = 0; di < variants.length; di++) {
1317
- var vName = variants[di].name;
1318
- if (vName.indexOf('state=default') !== -1 && (sizeAxis ? vName.indexOf(sizeAxis + '=') !== -1 : true)) {
1319
- // Pick the first default state variant
1320
- if (!defaultVariant || vName.indexOf('large') !== -1) {
1321
- defaultVariant = variants[di];
1325
+ // Find the default variant (#121). Start from Figma's own default (the
1326
+ // set's top-left variant). With a state axis, prefer the variant that
1327
+ // matches it on every other axis but is in a resting state. Variant
1328
+ // names are "Axis=Value, ..." in the author's casing, so compare values
1329
+ // case-insensitively.
1330
+ function parseAxes(name) {
1331
+ var out = {};
1332
+ var segs = name.split(',');
1333
+ for (var si = 0; si < segs.length; si++) {
1334
+ var kv = segs[si].split('=');
1335
+ if (kv.length === 2) out[kv[0].trim()] = kv[1].trim();
1336
+ }
1337
+ return out;
1338
+ }
1339
+ var figmaDefault = null;
1340
+ try { figmaDefault = node.defaultVariant || null; } catch (e) {}
1341
+ if (!figmaDefault && variants.length > 0) figmaDefault = variants[0];
1342
+ defaultVariant = figmaDefault;
1343
+ if (stateAxis && figmaDefault) {
1344
+ var restingStates = ['default', 'rest', 'resting', 'enabled', 'idle', 'normal', 'base'];
1345
+ var baseAxes = parseAxes(figmaDefault.name);
1346
+ var currentState = (baseAxes[stateAxis] || '').toLowerCase();
1347
+ if (restingStates.indexOf(currentState) === -1) {
1348
+ for (var di = 0; di < variants.length; di++) {
1349
+ var cand = parseAxes(variants[di].name);
1350
+ var sameOtherAxes = true;
1351
+ for (var bk in baseAxes) {
1352
+ if (bk !== stateAxis && cand[bk] !== baseAxes[bk]) { sameOtherAxes = false; break; }
1353
+ }
1354
+ if (sameOtherAxes && restingStates.indexOf((cand[stateAxis] || '').toLowerCase()) !== -1) {
1355
+ defaultVariant = variants[di];
1356
+ break;
1357
+ }
1322
1358
  }
1323
1359
  }
1324
1360
  }
@@ -1352,6 +1388,18 @@ figma.ui.onmessage = async (msg) => {
1352
1388
  if (sig.textToken !== defaultSig.textToken) diff.textToken = sig.textToken || sig.textHex;
1353
1389
  if (sig.opacity !== defaultSig.opacity) diff.opacity = sig.opacity;
1354
1390
  if (JSON.stringify(sig.effects) !== JSON.stringify(defaultSig.effects)) diff.effects = sig.effects;
1391
+ if (sig.width !== defaultSig.width) diff.width = sig.width;
1392
+ if (sig.height !== defaultSig.height) diff.height = sig.height;
1393
+ if (sig.fillHex !== defaultSig.fillHex && !diff.fillToken) diff.fillHex = sig.fillHex;
1394
+ if (sig.strokeHex !== defaultSig.strokeHex && !diff.strokeToken) diff.strokeHex = sig.strokeHex;
1395
+ if (sig.textHex !== defaultSig.textHex && !diff.textToken) diff.textHex = sig.textHex;
1396
+ if (JSON.stringify(sig.children) !== JSON.stringify(defaultSig.children)) {
1397
+ diff.children = {
1398
+ count: sig.children.length,
1399
+ added: sig.children.filter(function(n) { return defaultSig.children.indexOf(n) === -1; }),
1400
+ removed: defaultSig.children.filter(function(n) { return sig.children.indexOf(n) === -1; })
1401
+ };
1402
+ }
1355
1403
  // Visibility changes not in default
1356
1404
  var dvKeys = Object.keys(defaultSig.visibilityChanges);
1357
1405
  var svKeys = Object.keys(sig.visibilityChanges);
@@ -1384,7 +1432,9 @@ figma.ui.onmessage = async (msg) => {
1384
1432
  }
1385
1433
 
1386
1434
  if (defaultVariant) {
1387
- stateMachine.defaultState = 'default';
1435
+ var defaultAxes = parseAxes(defaultVariant.name);
1436
+ stateMachine.defaultState = stateAxis ? (defaultAxes[stateAxis] || 'default') : 'default';
1437
+ stateMachine.defaultVariant = { id: defaultVariant.id, name: defaultVariant.name };
1388
1438
  stateMachine.defaultSignature = defaultSig;
1389
1439
  }
1390
1440
 
@@ -1428,7 +1478,9 @@ figma.ui.onmessage = async (msg) => {
1428
1478
  // ============================================================================
1429
1479
  else if (msg.type === 'DEEP_GET_COMPONENT') {
1430
1480
  try {
1431
- var maxDepth = msg.depth || 10;
1481
+ var maxDepth = msg.depth;
1482
+ if (maxDepth === undefined || maxDepth === null) maxDepth = 10;
1483
+ if (maxDepth <= 0) maxDepth = Infinity; // 0 or negative = unlimited depth
1432
1484
  console.log('🌉 [Desktop Bridge] Deep component fetch: ' + msg.nodeId + ' (depth: ' + maxDepth + ')');
1433
1485
 
1434
1486
  var rootNode = await figma.getNodeByIdAsync(msg.nodeId);
@@ -1523,11 +1575,36 @@ figma.ui.onmessage = async (msg) => {
1523
1575
  } catch (e) {}
1524
1576
  if (n.opacity !== undefined && n.opacity < 1) props.opacity = n.opacity;
1525
1577
 
1578
+ // Mixed corner radius -> per-corner radii (full info instead of dropping)
1579
+ try {
1580
+ if (n.cornerRadius === figma.mixed) {
1581
+ props.cornerRadius = 'MIXED';
1582
+ props.cornerRadiuses = {
1583
+ topLeft: n.topLeftRadius,
1584
+ topRight: n.topRightRadius,
1585
+ bottomRight: n.bottomRightRadius,
1586
+ bottomLeft: n.bottomLeftRadius
1587
+ };
1588
+ }
1589
+ } catch (e) {}
1590
+ // Mixed stroke weight -> per-side weights
1591
+ try {
1592
+ if (n.strokeWeight === figma.mixed) {
1593
+ props.strokeWeight = 'MIXED';
1594
+ props.strokeWeights = {
1595
+ top: n.strokeTopWeight,
1596
+ right: n.strokeRightWeight,
1597
+ bottom: n.strokeBottomWeight,
1598
+ left: n.strokeLeftWeight
1599
+ };
1600
+ }
1601
+ } catch (e) {}
1602
+
1526
1603
  // Typography
1527
1604
  if (n.type === 'TEXT') {
1528
1605
  try { props.characters = n.characters; } catch (e) {}
1529
1606
  try { if (n.fontSize !== figma.mixed) props.fontSize = n.fontSize; } catch (e) {}
1530
- try { if (n.fontName !== figma.mixed) props.fontFamily = n.fontName.family; props.fontStyle = n.fontName.style; } catch (e) {}
1607
+ try { if (n.fontName !== figma.mixed) { props.fontFamily = n.fontName.family; props.fontStyle = n.fontName.style; } } catch (e) {}
1531
1608
  try { if (n.fontWeight !== figma.mixed) props.fontWeight = n.fontWeight; } catch (e) {}
1532
1609
  try { if (n.lineHeight !== figma.mixed) props.lineHeight = n.lineHeight; } catch (e) {}
1533
1610
  try { if (n.letterSpacing !== figma.mixed) props.letterSpacing = n.letterSpacing; } catch (e) {}
@@ -1537,6 +1614,31 @@ figma.ui.onmessage = async (msg) => {
1537
1614
  try { if (n.textTruncation && n.textTruncation !== 'DISABLED') props.textTruncation = n.textTruncation; } catch (e) {}
1538
1615
  try { if (n.textCase && n.textCase !== 'ORIGINAL') props.textCase = n.textCase; } catch (e) {}
1539
1616
  try { if (n.textDecoration && n.textDecoration !== 'NONE') props.textDecoration = n.textDecoration; } catch (e) {}
1617
+
1618
+ // Expand mixed per-character styles into full per-range segments (no info loss).
1619
+ try {
1620
+ var _typoFields = ['fontName','fontSize','fontWeight','textDecoration','textCase','lineHeight','letterSpacing','fills','textStyleId','fillStyleId'];
1621
+ var _anyMixed = false;
1622
+ for (var _ti = 0; _ti < _typoFields.length; _ti++) {
1623
+ try { if (n[_typoFields[_ti]] === figma.mixed) { _anyMixed = true; break; } } catch (e2) {}
1624
+ }
1625
+ if (_anyMixed && typeof n.getStyledTextSegments === 'function') {
1626
+ props.styledSegments = n.getStyledTextSegments(_typoFields).map(function (s) {
1627
+ var o = { characters: s.characters, start: s.start, end: s.end };
1628
+ try { if (s.fontName) { o.fontFamily = s.fontName.family; o.fontStyle = s.fontName.style; } } catch (e3) {}
1629
+ if (s.fontSize !== undefined) o.fontSize = s.fontSize;
1630
+ if (s.fontWeight !== undefined) o.fontWeight = s.fontWeight;
1631
+ if (s.lineHeight !== undefined) o.lineHeight = s.lineHeight;
1632
+ if (s.letterSpacing !== undefined) o.letterSpacing = s.letterSpacing;
1633
+ if (s.textDecoration !== undefined) o.textDecoration = s.textDecoration;
1634
+ if (s.textCase !== undefined) o.textCase = s.textCase;
1635
+ if (s.fills !== undefined) o.fills = s.fills;
1636
+ if (s.textStyleId) o.textStyleId = s.textStyleId;
1637
+ if (s.fillStyleId) o.fillStyleId = s.fillStyleId;
1638
+ return o;
1639
+ });
1640
+ }
1641
+ } catch (e) {}
1540
1642
  }
1541
1643
 
1542
1644
  // Design tokens (resolved to names)
@@ -1662,16 +1764,22 @@ figma.ui.onmessage = async (msg) => {
1662
1764
 
1663
1765
  var result = walkNode(rootNode, 0);
1664
1766
  result._variableMapSize = Object.keys(varNameMap).length;
1665
- result._maxDepthUsed = maxDepth;
1767
+ result._maxDepthUsed = (maxDepth === Infinity) ? 'unlimited' : maxDepth;
1666
1768
 
1667
- var resultJson = JSON.stringify(result);
1769
+ // figma.mixed is a Symbol; it cannot cross figma.ui.postMessage
1770
+ // (structuredClone throws "Cannot unwrap symbol"). Serialize with a
1771
+ // replacer that turns any Symbol into the string "MIXED", then send the
1772
+ // parsed (symbol-free) clone instead of the raw node tree.
1773
+ var resultJson = JSON.stringify(result, function (key, val) {
1774
+ return typeof val === 'symbol' ? 'MIXED' : val;
1775
+ });
1668
1776
  console.log('🌉 [Desktop Bridge] Deep component data: ' + Math.round(resultJson.length / 1024) + 'KB, vars resolved: ' + Object.keys(varNameMap).length);
1669
1777
 
1670
1778
  figma.ui.postMessage({
1671
1779
  type: 'DEEP_GET_COMPONENT_RESULT',
1672
1780
  requestId: msg.requestId,
1673
1781
  success: true,
1674
- data: result
1782
+ data: JSON.parse(resultJson)
1675
1783
  });
1676
1784
 
1677
1785
  } catch (error) {
@@ -1733,6 +1841,8 @@ figma.ui.onmessage = async (msg) => {
1733
1841
 
1734
1842
  // Helper to extract component set data with all variants
1735
1843
  function extractComponentSetData(node) {
1844
+ // The getter recomputes on every access; read it once per set
1845
+ var setPropDefs = node.componentPropertyDefinitions;
1736
1846
  var variantAxes = {};
1737
1847
  var variants = [];
1738
1848
 
@@ -1795,8 +1905,8 @@ figma.ui.onmessage = async (msg) => {
1795
1905
  variantAxes: axes,
1796
1906
  variants: variants,
1797
1907
  defaultVariant: variants.length > 0 ? variants[0] : null,
1798
- properties: node.componentPropertyDefinitions ? Object.keys(node.componentPropertyDefinitions).map(function(propName) {
1799
- var propDef = node.componentPropertyDefinitions[propName];
1908
+ properties: setPropDefs ? Object.keys(setPropDefs).map(function(propName) {
1909
+ var propDef = setPropDefs[propName];
1800
1910
  return {
1801
1911
  name: propName,
1802
1912
  type: propDef.type,
@@ -1807,60 +1917,30 @@ figma.ui.onmessage = async (msg) => {
1807
1917
  };
1808
1918
  }
1809
1919
 
1810
- // Recursively search for components
1811
- function findComponents(node) {
1812
- if (!node) return;
1813
-
1814
- if (node.type === 'COMPONENT_SET') {
1815
- componentSets.push(extractComponentSetData(node));
1816
- } else if (node.type === 'COMPONENT') {
1817
- // Only add standalone components (not variants inside component sets)
1818
- if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
1819
- components.push(extractComponentData(node, false));
1820
- }
1821
- }
1822
-
1823
- // Recurse into children — skip unresolvable slot sublayers
1824
- if (node.children) {
1825
- node.children.forEach(function(child) {
1826
- try { findComponents(child); } catch (e) { /* slot sublayer — skip */ }
1827
- });
1828
- }
1829
- }
1830
-
1831
1920
  // Load all pages first (required before accessing children)
1832
1921
  console.log('🌉 [Desktop Bridge] Loading all pages...');
1833
1922
  await figma.loadAllPagesAsync();
1834
1923
 
1835
- // Process pages in batches with event loop yields to prevent UI freeze
1836
- // This is critical for large design systems that could otherwise crash
1837
- var pages = figma.root.children;
1838
- var PAGE_BATCH_SIZE = 3; // Process 3 pages at a time
1839
- var totalPages = pages.length;
1840
-
1841
- console.log('🌉 [Desktop Bridge] Processing ' + totalPages + ' pages in batches of ' + PAGE_BATCH_SIZE + '...');
1842
-
1843
- for (var pageIndex = 0; pageIndex < totalPages; pageIndex += PAGE_BATCH_SIZE) {
1844
- var batchEnd = Math.min(pageIndex + PAGE_BATCH_SIZE, totalPages);
1845
- var batchPages = [];
1846
- for (var j = pageIndex; j < batchEnd; j++) {
1847
- batchPages.push(pages[j]);
1848
- }
1849
-
1850
- // Process this batch of pages
1851
- batchPages.forEach(function(page) {
1852
- findComponents(page);
1853
- });
1854
-
1855
- // Log progress for large files
1856
- if (totalPages > PAGE_BATCH_SIZE) {
1857
- console.log('🌉 [Desktop Bridge] Processed pages ' + (pageIndex + 1) + '-' + batchEnd + ' of ' + totalPages + ' (found ' + components.length + ' components so far)');
1858
- }
1859
-
1860
- // Yield to event loop between batches to prevent UI freeze and allow cancellation
1861
- if (batchEnd < totalPages) {
1862
- await new Promise(function(resolve) { setTimeout(resolve, 0); });
1863
- }
1924
+ // Native search instead of a JS walk over every node (#120): the walk
1925
+ // crossed into the Plugin API for each of the file's nodes, including
1926
+ // every layer inside every instance (89k nodes: 1.7s vs 0.13s here, and
1927
+ // far worse on heavier files).
1928
+ var found = figma.root.findAllWithCriteria({ types: ['COMPONENT_SET', 'COMPONENT'] });
1929
+ console.log('🌉 [Desktop Bridge] Found ' + found.length + ' component nodes; extracting...');
1930
+ for (var fi = 0; fi < found.length; fi++) {
1931
+ var fnode = found[fi];
1932
+ try {
1933
+ if (fnode.type === 'COMPONENT_SET') {
1934
+ componentSets.push(extractComponentSetData(fnode));
1935
+ } else if (!fnode.parent || fnode.parent.type !== 'COMPONENT_SET') {
1936
+ // Standalone components only (variants are reported under their set)
1937
+ components.push(extractComponentData(fnode, false));
1938
+ }
1939
+ } catch (e) { /* unresolvable slot sublayer — skip */ }
1940
+ // No setTimeout yields here: plugin timers are throttled when Figma is
1941
+ // in the background (up to ~1/min under Chromium's intensive
1942
+ // throttling), so a handful of yields turned a ~1.5s extraction into
1943
+ // the 300s timeout reported in #120.
1864
1944
  }
1865
1945
 
1866
1946
  console.log('🌉 [Desktop Bridge] Found ' + components.length + ' components and ' + componentSets.length + ' component sets');
@@ -1003,8 +1003,8 @@
1003
1003
  window.deepGetComponent = (nodeId, depth) => {
1004
1004
  return window.sendPluginCommand('DEEP_GET_COMPONENT', {
1005
1005
  nodeId: nodeId,
1006
- depth: depth || 10
1007
- }, 30000)
1006
+ depth: (depth === undefined || depth === null) ? 10 : depth
1007
+ }, (typeof depth === 'number' && depth <= 0) ? 120000 : 30000)
1008
1008
  .catch(function(err) { return { success: false, error: err.message || String(err) }; });
1009
1009
  };
1010
1010
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-console-mcp",
3
- "version": "1.41.0",
3
+ "version": "1.41.2",
4
4
  "description": "The most comprehensive MCP server for Figma — design tokens, variables, components, write tools, version history diff, accessibility audits, FigJam, Slides, and more. Local (WebSocket Desktop Bridge plugin) and Cloudflare Workers (paired + remote) modes.",
5
5
  "type": "module",
6
6
  "main": "dist/local.js",