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 +1 -1
- package/package.json +4 -4
- package/plugin/plugin.js +131 -0
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/
|
|
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.
|
|
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/
|
|
38
|
+
"url": "https://github.com/dmytro-zemliak/Figma-MCP-Write-Bridge.git"
|
|
39
39
|
},
|
|
40
40
|
"bugs": {
|
|
41
|
-
"url": "https://github.com/dmytro-zemliak/
|
|
41
|
+
"url": "https://github.com/dmytro-zemliak/Figma-MCP-Write-Bridge/issues"
|
|
42
42
|
},
|
|
43
|
-
"homepage": "https://github.com/dmytro-zemliak/
|
|
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
|
|