mcp-figma-toolkit 1.0.4 → 1.0.6

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 CHANGED
@@ -70,7 +70,7 @@ The plugin "MCP Figma Toolkit" should now appear under **Plugins → Development
70
70
  ### Alternative: Clone from Source
71
71
 
72
72
  ```bash
73
- git clone https://github.com/dmytro-zemliak/mcp-figma-toolkit.git
73
+ git clone https://github.com/dmytro-zemliak/Figma-MCP-Write-Bridge.git
74
74
  cd mcp-figma-toolkit
75
75
  npm install
76
76
  npm run build
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-figma-toolkit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "MCP server that enables AI agents to manipulate Figma documents - create shapes, text, styles, components, variables, and more",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -35,12 +35,12 @@
35
35
  "license": "MIT",
36
36
  "repository": {
37
37
  "type": "git",
38
- "url": "https://github.com/dmytro-zemliak/mcp-figma-toolkit.git"
38
+ "url": "https://github.com/dmytro-zemliak/Figma-MCP-Write-Bridge.git"
39
39
  },
40
40
  "bugs": {
41
- "url": "https://github.com/dmytro-zemliak/mcp-figma-toolkit/issues"
41
+ "url": "https://github.com/dmytro-zemliak/Figma-MCP-Write-Bridge/issues"
42
42
  },
43
- "homepage": "https://github.com/dmytro-zemliak/mcp-figma-toolkit#readme",
43
+ "homepage": "https://github.com/dmytro-zemliak/Figma-MCP-Write-Bridge#readme",
44
44
  "engines": {
45
45
  "node": ">=18.0.0"
46
46
  },
package/plugin/plugin.js CHANGED
@@ -17,6 +17,45 @@ function reply(replyTo, result, error) {
17
17
  const page = () => figma.currentPage;
18
18
 
19
19
  // ---------- Utilities ----------
20
+ function rgbToHex(r, g, b) {
21
+ const toHex = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0");
22
+ return "#" + toHex(r) + toHex(g) + toHex(b);
23
+ }
24
+
25
+ function serializePaints(paints) {
26
+ if (!paints || !Array.isArray(paints)) return [];
27
+ return paints.map(p => {
28
+ const base = { type: p.type, visible: p.visible !== false, opacity: p.opacity ?? 1, blendMode: p.blendMode };
29
+ if (p.type === "SOLID" && p.color) {
30
+ base.hex = rgbToHex(p.color.r, p.color.g, p.color.b);
31
+ } else if (p.type && p.type.startsWith("GRADIENT_") && p.gradientStops) {
32
+ base.gradientStops = p.gradientStops.map(s => ({
33
+ position: s.position,
34
+ hex: rgbToHex(s.color.r, s.color.g, s.color.b),
35
+ opacity: s.color.a ?? 1
36
+ }));
37
+ } else if (p.type === "IMAGE") {
38
+ base.scaleMode = p.scaleMode;
39
+ base.imageHash = p.imageHash;
40
+ }
41
+ return base;
42
+ });
43
+ }
44
+
45
+ function serializeEffects(effects) {
46
+ if (!effects || !Array.isArray(effects)) return [];
47
+ return effects.map(e => {
48
+ const base = { type: e.type, visible: e.visible !== false, radius: e.radius };
49
+ if (e.offset) base.offset = { x: e.offset.x, y: e.offset.y };
50
+ if (e.spread !== undefined) base.spread = e.spread;
51
+ if (e.color) {
52
+ base.hex = rgbToHex(e.color.r, e.color.g, e.color.b);
53
+ base.opacity = e.color.a ?? 1;
54
+ }
55
+ return base;
56
+ });
57
+ }
58
+
20
59
  function hexToRGB(hex) {
21
60
  const v = String(hex || "").replace("#", "").trim();
22
61
  if (!/^[0-9a-fA-F]{6}$/.test(v)) throw new Error("Invalid hex color");
@@ -644,6 +683,98 @@ function getNodeInfo({ nodeId }) {
644
683
  // Opacity
645
684
  if ("opacity" in node) info.opacity = node.opacity;
646
685
 
686
+ // Blend mode
687
+ if ("blendMode" in node) info.blendMode = node.blendMode;
688
+
689
+ // Visual properties (fills, strokes, effects)
690
+ if ("fills" in node) {
691
+ try { info.fills = serializePaints(node.fills); } catch (_) {}
692
+ }
693
+ if ("strokes" in node) {
694
+ try { info.strokes = serializePaints(node.strokes); } catch (_) {}
695
+ }
696
+ if ("strokeWeight" in node) info.strokeWeight = node.strokeWeight;
697
+ if ("strokeAlign" in node) info.strokeAlign = node.strokeAlign;
698
+
699
+ // Corner radius
700
+ if ("cornerRadius" in node) {
701
+ if (node.cornerRadius !== figma.mixed) {
702
+ info.cornerRadius = node.cornerRadius;
703
+ } else {
704
+ info.cornerRadius = {
705
+ topLeft: node.topLeftRadius,
706
+ topRight: node.topRightRadius,
707
+ bottomLeft: node.bottomLeftRadius,
708
+ bottomRight: node.bottomRightRadius
709
+ };
710
+ }
711
+ }
712
+
713
+ // Effects
714
+ if ("effects" in node) {
715
+ try { info.effects = serializeEffects(node.effects); } catch (_) {}
716
+ }
717
+
718
+ // Auto-layout properties
719
+ if ("layoutMode" in node && node.layoutMode !== "NONE") {
720
+ info.autoLayout = {
721
+ layoutMode: node.layoutMode,
722
+ itemSpacing: node.itemSpacing,
723
+ counterAxisSpacing: node.counterAxisSpacing,
724
+ paddingLeft: node.paddingLeft,
725
+ paddingRight: node.paddingRight,
726
+ paddingTop: node.paddingTop,
727
+ paddingBottom: node.paddingBottom,
728
+ primaryAxisAlignItems: node.primaryAxisAlignItems,
729
+ counterAxisAlignItems: node.counterAxisAlignItems,
730
+ layoutSizingHorizontal: node.layoutSizingHorizontal,
731
+ layoutSizingVertical: node.layoutSizingVertical
732
+ };
733
+ }
734
+
735
+ // Text properties
736
+ if (node.type === "TEXT") {
737
+ info.text = { characters: node.characters };
738
+ try {
739
+ const fs = node.fontSize;
740
+ info.text.fontSize = fs !== figma.mixed ? fs : "MIXED";
741
+ } catch (_) {}
742
+ try {
743
+ const fn = node.fontName;
744
+ info.text.fontName = fn !== figma.mixed ? { family: fn.family, style: fn.style } : "MIXED";
745
+ } catch (_) {}
746
+ if ("textAlignHorizontal" in node) info.text.textAlignHorizontal = node.textAlignHorizontal;
747
+ if ("textAlignVertical" in node) info.text.textAlignVertical = node.textAlignVertical;
748
+ try {
749
+ const lh = node.lineHeight;
750
+ info.text.lineHeight = lh !== figma.mixed ? lh : "MIXED";
751
+ } catch (_) {}
752
+ try {
753
+ const ls = node.letterSpacing;
754
+ info.text.letterSpacing = ls !== figma.mixed ? ls : "MIXED";
755
+ } catch (_) {}
756
+ if ("textAutoResize" in node) info.text.textAutoResize = node.textAutoResize;
757
+ // Text fills (color)
758
+ if ("fills" in node) {
759
+ try { info.text.fills = serializePaints(node.fills); } catch (_) {}
760
+ }
761
+ }
762
+
763
+ // Component info
764
+ if (node.type === "INSTANCE" && node.mainComponent) {
765
+ info.mainComponent = { id: node.mainComponent.id, name: node.mainComponent.name };
766
+ try {
767
+ const props = node.componentProperties;
768
+ if (props && Object.keys(props).length > 0) info.componentProperties = props;
769
+ } catch (_) {}
770
+ }
771
+ if (node.type === "COMPONENT") {
772
+ try {
773
+ const propDefs = node.componentPropertyDefinitions;
774
+ if (propDefs && Object.keys(propDefs).length > 0) info.componentPropertyDefinitions = propDefs;
775
+ } catch (_) {}
776
+ }
777
+
647
778
  return info;
648
779
  }
649
780