figma-rust-mcp 2.8.4 → 2.9.5
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 +101 -52
- package/bin/figma-mcp.js +4 -2
- package/package.json +1 -1
- package/plugin/code.js +270 -13
- package/plugin/ui.html +768 -374
- package/scripts/test-rust-mcp.sh +4 -6
package/README.md
CHANGED
|
@@ -1,40 +1,48 @@
|
|
|
1
|
-
# figma-mcp
|
|
1
|
+
# figma-mcp (figma-rust-mcp)
|
|
2
2
|
|
|
3
3
|
High-performance, bidirectional Model Context Protocol (MCP) server written in **Rust** connecting AI coding assistants directly to **Figma Desktop**.
|
|
4
4
|
|
|
5
|
-
Enables AI agents (Claude Code, Cursor, Windsurf,
|
|
5
|
+
Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Zed) to **compile Figma designs into production code**, **extract design tokens**, **draw UI directly on canvas**, and **inspect hierarchy, styles, and assets** — with zero external API keys needed.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## ⚡ Highlights
|
|
10
10
|
|
|
11
11
|
- **Pure Rust Native Performance**: Starts in `< 1ms`, uses `~3MB RAM`, zero GC pauses.
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
12
|
+
- **In-Memory Deep Indexing (`figma_index`)**: Queries layers, components, styles, and tokens in `< 1ms` without slow canvas roundtrips.
|
|
13
|
+
- **Binary IPC & Chunk Streaming**: Powered by **MessagePack** (`rmp-serde`) and progressive subtree chunking for instant transfers of massive design files.
|
|
14
|
+
- **Incremental Diff Updates**: Sub-millisecond live document sync (`upsert_node`) keeps the server index fresh as you edit in Figma.
|
|
15
|
+
- **Instant Design-to-Code (`figma_to_code`)**: Compiles Figma frames into clean, semantic components (**React + Tailwind**, **React Native**, **Vue 3 + Tailwind**, **HTML**, **SwiftUI**).
|
|
16
|
+
- **Design Token Exporter (`figma_get_tokens`)**: Exports variables and styles to **Tailwind config**, **CSS custom properties** (`:root` & dark mode), **TypeScript consts**, or **W3C DTCG Token Studio JSON**.
|
|
17
|
+
- **Batch Asset Extractor (`figma_export_assets` / `figma_export_asset`)**: Extracts SVG icons and raster images directly to project folders (`outputPath`) with auto-generated TypeScript barrel exports (`index.ts`) — zero chat token bloating.
|
|
18
|
+
- **Sandboxed JS Drawing Engine (`figma_write`)**: Powered by **Boa** (pure Rust ECMAScript engine) with built-in asset resolvers and 7 icon packs (*Ionicons, Lucide, Tabler, Bootstrap, Fluent, Phosphor*).
|
|
15
19
|
- **Multi-Tab / Multi-Session Support**: Seamlessly connects to multiple open Figma files simultaneously.
|
|
16
|
-
- **100% Localhost Privacy**: All communication stays strictly on `127.0.0.1:38451
|
|
20
|
+
- **100% Localhost Privacy**: All communication stays strictly on `127.0.0.1:38451`.
|
|
17
21
|
|
|
18
22
|
---
|
|
19
23
|
|
|
20
24
|
## 🏛️ Architecture
|
|
21
25
|
|
|
22
26
|
```text
|
|
23
|
-
|
|
24
|
-
│
|
|
25
|
-
│ figma-mcp
|
|
26
|
-
│ • MCP SSE Transport (/sse, /message)
|
|
27
|
-
│ •
|
|
28
|
-
│ •
|
|
29
|
-
│ •
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
┌────────────────────────────────────────────────────────────────────────┐
|
|
28
|
+
│ Terminal / Standalone Service │
|
|
29
|
+
│ figma-mcp (Pure Rust Engine) │
|
|
30
|
+
│ • MCP SSE Transport (/sse, /message) & Direct HTTP (/mcp) │
|
|
31
|
+
│ • In-Memory Fast Index (<1ms Lookups & Incremental Diffs) │
|
|
32
|
+
│ • Design-to-Code Compiler (React/Tailwind, Vue, RN, SwiftUI) │
|
|
33
|
+
│ • Design Token Transformer (CSS, Tailwind, TypeScript, W3C DTCG) │
|
|
34
|
+
│ • Sandboxed JS Runtime (Boa ECMAScript Engine) │
|
|
35
|
+
│ • Binary MessagePack & Progressive Subtree Chunk Receiver │
|
|
36
|
+
└───────────────▲────────────────────────────────────────▲───────────────┘
|
|
37
|
+
│ (ws://127.0.0.1:38451) │ (http://127.0.0.1:38451/sse)
|
|
38
|
+
│ (MessagePack / JSON IPC) │
|
|
39
|
+
┌─────────┴─────────┐ ┌─────────┴─────────┐
|
|
40
|
+
│ Figma Desktop │ │ AI Assistant(s) │
|
|
41
|
+
│ (Plugin Bridge) │ │ Google Antigravity│
|
|
42
|
+
│ │ │ Cursor / Windsurf │
|
|
43
|
+
│ • Canvas Tracker │ │ Claude Code / Zed │
|
|
44
|
+
│ • SVG/PNG Render │ └───────────────────┘
|
|
45
|
+
└───────────────────┘
|
|
38
46
|
```
|
|
39
47
|
|
|
40
48
|
---
|
|
@@ -43,34 +51,37 @@ Enables AI agents (Claude Code, Cursor, Windsurf, Antigravity, VS Code, Zed) to
|
|
|
43
51
|
|
|
44
52
|
### 1. One-Line Launch with NPX (No Rust toolchain needed)
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
Launch `figma-rust-mcp` on macOS, Linux, or Windows with zero manual setup:
|
|
47
55
|
|
|
48
56
|
```bash
|
|
49
57
|
npx figma-rust-mcp
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
This automatically downloads the prebuilt native binary for your architecture and starts the server.
|
|
60
|
+
This automatically downloads the prebuilt native binary for your OS and architecture and starts the server.
|
|
53
61
|
|
|
54
62
|
---
|
|
55
63
|
|
|
56
64
|
### 2. Auto-Start as Background Service (Recommended for SSE)
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
To make `figma-rust-mcp` start automatically upon OS login and run in the background across all your AI IDEs:
|
|
59
67
|
|
|
60
68
|
```bash
|
|
61
69
|
# Install and start auto-start background service (macOS LaunchAgent, Linux systemd, Windows Task Scheduler)
|
|
62
70
|
npx figma-rust-mcp --install-service
|
|
63
71
|
|
|
64
|
-
#
|
|
72
|
+
# Update/Upgrade to the latest version and hot-reload background service
|
|
73
|
+
npx -y figma-rust-mcp@latest --install-service
|
|
74
|
+
|
|
75
|
+
# Check service status
|
|
65
76
|
npx figma-rust-mcp --service-status
|
|
66
77
|
|
|
67
|
-
# Uninstall service
|
|
78
|
+
# Uninstall background service
|
|
68
79
|
npx figma-rust-mcp --uninstall-service
|
|
69
80
|
```
|
|
70
81
|
|
|
71
82
|
---
|
|
72
83
|
|
|
73
|
-
### 3.
|
|
84
|
+
### 3. Build from Source (Rust)
|
|
74
85
|
|
|
75
86
|
Prerequisites: [Rust toolchain](https://rustup.rs/) (`cargo >= 1.80`).
|
|
76
87
|
|
|
@@ -87,9 +98,9 @@ cargo build --release
|
|
|
87
98
|
|
|
88
99
|
1. Open **Figma Desktop**.
|
|
89
100
|
2. Go to **Plugins** → **Development** → **Import plugin from manifest...**
|
|
90
|
-
3. Select
|
|
101
|
+
3. Select `plugin/manifest.json` from this repository.
|
|
91
102
|
4. Run the plugin (**Plugins** → **Development** → **Figma MCP Bridge**).
|
|
92
|
-
5. The plugin UI will
|
|
103
|
+
5. The plugin UI will display a green dot (**Connected**).
|
|
93
104
|
|
|
94
105
|
---
|
|
95
106
|
|
|
@@ -107,10 +118,9 @@ Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.js
|
|
|
107
118
|
}
|
|
108
119
|
```
|
|
109
120
|
|
|
110
|
-
#### Claude Code / Cursor / Windsurf / VS Code
|
|
111
|
-
You can connect via SSE or stdio:
|
|
121
|
+
#### Claude Code / Cursor / Windsurf / VS Code / Zed
|
|
112
122
|
|
|
113
|
-
**Option A: SSE Transport (Recommended)**
|
|
123
|
+
**Option A: SSE Transport (Recommended for background service)**
|
|
114
124
|
```json
|
|
115
125
|
{
|
|
116
126
|
"mcpServers": {
|
|
@@ -137,46 +147,85 @@ You can connect via SSE or stdio:
|
|
|
137
147
|
|
|
138
148
|
## 🛠️ MCP Tools Reference
|
|
139
149
|
|
|
140
|
-
`figma-mcp`
|
|
150
|
+
`figma-mcp` provides **12 first-class MCP tools**:
|
|
141
151
|
|
|
142
|
-
### `
|
|
143
|
-
|
|
152
|
+
### 1. `figma_status`
|
|
153
|
+
Checks live bridge connection status, connected Figma tabs/files, in-memory index health, queue length, and latency statistics.
|
|
144
154
|
|
|
145
|
-
### `figma_inspect_node`
|
|
146
|
-
Inspects a specific node by ID or name, returning
|
|
155
|
+
### 2. `figma_inspect_node`
|
|
156
|
+
Inspects a specific node by ID or name, returning CSS styles, flex layout rules, tokens, typography, and fills in a clean format (served in `< 1ms` via in-memory index or direct bridge).
|
|
147
157
|
|
|
148
|
-
### `
|
|
149
|
-
|
|
150
|
-
- `
|
|
158
|
+
### 3. `figma_to_code`
|
|
159
|
+
Compiles any Figma node (Frame, Component, Section, or selection) directly into clean, production-ready component code.
|
|
160
|
+
- **`framework`**: `"react-tailwind"` (default), `"react-native"`, `"vue-tailwind"`, `"html"`, `"swiftui"`.
|
|
161
|
+
- **`outputPath`**: Directly saves the generated component to your codebase (e.g. `src/components/UserProfileCard.tsx`).
|
|
162
|
+
- **`componentName`**: Custom component name override.
|
|
163
|
+
|
|
164
|
+
### 4. `figma_get_tokens`
|
|
165
|
+
Exports Figma Variables (Design Tokens), Color Styles, Typography Styles, and Elevation/Shadow Styles directly into frontend code.
|
|
166
|
+
- **`format`**: `"tailwind"` (`tailwind.config.js` theme.extend), `"css"` (`:root` CSS custom properties with light/dark theme modes), `"typescript"` (`tokens.ts`), `"w3c"` (W3C DTCG Token Studio JSON), or `"json"`.
|
|
167
|
+
- **`outputPath`**: Writes directly to disk (e.g. `src/styles/tokens.css` or `tailwind.config.js`).
|
|
168
|
+
- **`collection`** / **`mode`** / **`prefix`**: Optional filters and naming prefixes.
|
|
151
169
|
|
|
152
|
-
### `
|
|
153
|
-
|
|
170
|
+
### 5. `figma_get_selection`
|
|
171
|
+
Token-compressed inspection of currently selected layers/frames on the Figma canvas. Returns compacted layout and typography hierarchy with **60–80% fewer tokens**.
|
|
172
|
+
- **`detail`**: `"compact"` (recommended), `"minimal"`, `"full"`.
|
|
173
|
+
- **`depth`**: Tree depth limit or `"full"`.
|
|
154
174
|
|
|
155
|
-
### `
|
|
175
|
+
### 6. `figma_export_asset`
|
|
176
|
+
Exports any Figma node/layer directly into PNG, JPG, or SVG.
|
|
177
|
+
- **`outputPath`**: When specified (e.g. `src/assets/logo.png`), saves directly to disk and returns minimal JSON metadata instead of bloating context with large base64 strings.
|
|
178
|
+
- **`format`**: `"png"` (default), `"jpg"`, `"svg"`.
|
|
179
|
+
- **`scale`**: Scaling factor (default `2` for high-res).
|
|
180
|
+
|
|
181
|
+
### 7. `figma_export_assets`
|
|
182
|
+
Batch extracts all SVG icons and PNG images from a Figma frame/page directly into project directories.
|
|
183
|
+
- **`iconDir`**: Directory path for SVG icons (e.g. `src/assets/icons`).
|
|
184
|
+
- **`imageDir`**: Directory path for raster images (e.g. `public/images`).
|
|
185
|
+
- **`createBarrel`**: Automatically creates an `index.ts` barrel export file in `iconDir`.
|
|
186
|
+
|
|
187
|
+
### 8. `figma_index`
|
|
188
|
+
Instant `< 1ms` in-memory queries against pre-indexed Figma file structures.
|
|
189
|
+
- **`operation`**:
|
|
190
|
+
- `"status"`: View index health and node counts.
|
|
191
|
+
- `"search_nodes"`: Search nodes by text name, query, and type (`FRAME`, `TEXT`, `COMPONENT`, `INSTANCE`).
|
|
192
|
+
- `"get_node"`: Instant node lookup by ID.
|
|
193
|
+
- `"search_components"`: Find component sets and variants.
|
|
194
|
+
- `"search_styles"`: Find paint, text, and effect styles.
|
|
195
|
+
- `"search_variables"`: Find design token variables by name or collection.
|
|
196
|
+
- `"refresh"`: Trigger full background re-indexing of the canvas.
|
|
197
|
+
|
|
198
|
+
### 9. `figma_read`
|
|
199
|
+
Universal reader for advanced queries:
|
|
200
|
+
- **Design-to-code**: `get_design_context`, `get_css`, `get_component_map`, `get_unmapped_components`.
|
|
201
|
+
- **Inspection & Hierarchy**: `get_selection`, `get_design`, `get_page_nodes`, `get_node_detail`, `scan_design`, `search_nodes`.
|
|
202
|
+
- **Design Systems**: `get_styles`, `get_variables`, `get_local_components`, `get_tokens`.
|
|
203
|
+
- **Visuals & Canvas**: `screenshot`, `export_svg`, `export_image`, `get_viewport`.
|
|
204
|
+
|
|
205
|
+
### 10. `figma_write`
|
|
156
206
|
Executes JavaScript draw commands inside the sandboxed VM to build or modify designs on the Figma canvas.
|
|
157
207
|
- Supports Auto-layout (`layoutMode`, `itemSpacing`, `padding`), typography, fills, strokes, corner radius, drop shadows, and component creation.
|
|
158
208
|
- Supports icon loading: `figma.loadIcon("ionicons", "heart", { size: 24, fill: "#ff4757" })`.
|
|
159
209
|
- Supports image insertion: `figma.loadImage(url, { width: 300, height: 200 })`.
|
|
210
|
+
- Supports Design Tokens: `createVariableCollection`, `createVariable`, `addVariableMode`, `applyVariable`.
|
|
211
|
+
- Supports Prototyping & Reactions: `setReactions`, `getReactions`, `setScrollBehavior`.
|
|
160
212
|
|
|
161
|
-
### `
|
|
162
|
-
|
|
213
|
+
### 11. `figma_rules`
|
|
214
|
+
Audits the current Figma document and generates a complete design system rule sheet (color tokens, typography styles, variables, component catalog) in `< 1ms` from cache.
|
|
163
215
|
|
|
164
|
-
### `figma_docs`
|
|
216
|
+
### 12. `figma_docs`
|
|
165
217
|
Fetches built-in documentation, design rules, layout guidelines, and code examples for `figma_write`.
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
### `figma_rules`
|
|
169
|
-
Audits the current Figma document and generates consistency rules and token definitions to guide AI generation.
|
|
218
|
+
- **`section`**: `"rules"` | `"layout"` | `"api"` | `"tokens"` | `"icons"`
|
|
170
219
|
|
|
171
220
|
---
|
|
172
221
|
|
|
173
222
|
## 💻 Development & Testing
|
|
174
223
|
|
|
175
224
|
```bash
|
|
176
|
-
# Run server
|
|
225
|
+
# Run server in debug mode
|
|
177
226
|
cargo run
|
|
178
227
|
|
|
179
|
-
# Build release binary
|
|
228
|
+
# Build optimized release binary
|
|
180
229
|
cargo build --release
|
|
181
230
|
|
|
182
231
|
# Run automated MCP test suite
|
package/bin/figma-mcp.js
CHANGED
|
@@ -19,7 +19,7 @@ const __dirname = path.dirname(__filename);
|
|
|
19
19
|
|
|
20
20
|
// Read package.json version
|
|
21
21
|
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
22
|
-
let pkgVersion = '2.
|
|
22
|
+
let pkgVersion = '2.9.5';
|
|
23
23
|
let repoOwner = 'BuiHung1612';
|
|
24
24
|
let repoName = 'figma-mcp';
|
|
25
25
|
|
|
@@ -234,6 +234,7 @@ async function installService(binPath) {
|
|
|
234
234
|
<key>ProgramArguments</key>
|
|
235
235
|
<array>
|
|
236
236
|
<string>${binPath}</string>
|
|
237
|
+
<string>--server</string>
|
|
237
238
|
</array>
|
|
238
239
|
<key>RunAtLoad</key>
|
|
239
240
|
<true/>
|
|
@@ -272,7 +273,7 @@ After=network.target
|
|
|
272
273
|
|
|
273
274
|
[Service]
|
|
274
275
|
Type=simple
|
|
275
|
-
ExecStart=${binPath}
|
|
276
|
+
ExecStart=${binPath} --server
|
|
276
277
|
Restart=on-failure
|
|
277
278
|
RestartSec=5
|
|
278
279
|
|
|
@@ -325,6 +326,7 @@ async function uninstallService() {
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
try { execSync(`launchctl unload -w "${plistPath}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
329
|
+
try { execSync(`pkill -f figma-mcp 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
328
330
|
fs.unlinkSync(plistPath);
|
|
329
331
|
|
|
330
332
|
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.5",
|
|
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-26T09:42:07.781Z
|
|
5
5
|
|
|
6
6
|
// ── plugin-src/utils.js ──
|
|
7
7
|
// ─── UTILS ────────────────────────────────────────────────────────────────────
|
|
@@ -1238,7 +1238,7 @@ const handlers = {};
|
|
|
1238
1238
|
|
|
1239
1239
|
handlers.status = async () => ({
|
|
1240
1240
|
connected: true,
|
|
1241
|
-
version: "2.8.
|
|
1241
|
+
version: "2.8.5",
|
|
1242
1242
|
fileName: figma.root.name,
|
|
1243
1243
|
currentPage: figma.currentPage.name,
|
|
1244
1244
|
pageCount: figma.root.children.length,
|
|
@@ -2902,8 +2902,37 @@ handlers.screenshot = async function(params) {
|
|
|
2902
2902
|
return Promise.reject(new Error("[v1.9.1-export] " + exportErr.message + " type=" + node.type + " id=" + node.id));
|
|
2903
2903
|
}
|
|
2904
2904
|
|
|
2905
|
+
var annotations = [];
|
|
2906
|
+
if (params && (params.withAnnotations || params.annotated) && "children" in node && Array.isArray(node.children)) {
|
|
2907
|
+
var annCount = 0;
|
|
2908
|
+
for (var ci = 0; ci < node.children.length; ci++) {
|
|
2909
|
+
var child = node.children[ci];
|
|
2910
|
+
if (child.visible === false) continue;
|
|
2911
|
+
annCount++;
|
|
2912
|
+
annotations.push({
|
|
2913
|
+
index: annCount,
|
|
2914
|
+
id: child.id,
|
|
2915
|
+
name: child.name,
|
|
2916
|
+
type: child.type,
|
|
2917
|
+
x: Math.round(child.x),
|
|
2918
|
+
y: Math.round(child.y),
|
|
2919
|
+
width: Math.round(child.width),
|
|
2920
|
+
height: Math.round(child.height)
|
|
2921
|
+
});
|
|
2922
|
+
if (annCount >= 40) break;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2905
2926
|
try {
|
|
2906
|
-
|
|
2927
|
+
var resObj = {
|
|
2928
|
+
dataUrl: "data:image/png;base64," + uint8ArrayToBase64(bytes),
|
|
2929
|
+
nodeId: node.id,
|
|
2930
|
+
nodeName: node.name,
|
|
2931
|
+
width: node.width,
|
|
2932
|
+
height: node.height
|
|
2933
|
+
};
|
|
2934
|
+
if (annotations.length) resObj.annotations = annotations;
|
|
2935
|
+
return resObj;
|
|
2907
2936
|
} catch(encodeErr) {
|
|
2908
2937
|
return Promise.reject(new Error("[v1.9.1-encode] " + encodeErr.message));
|
|
2909
2938
|
}
|
|
@@ -3061,9 +3090,13 @@ handlers.index_scan = async function() {
|
|
|
3061
3090
|
var variables = results[2];
|
|
3062
3091
|
var components = results[3];
|
|
3063
3092
|
|
|
3093
|
+
var resolvedSid = figma.fileKey || (function() {
|
|
3094
|
+
try { return figma.root.getPluginData("mcp_session_id") || "_default"; } catch(e) { return "_default"; }
|
|
3095
|
+
})();
|
|
3096
|
+
|
|
3064
3097
|
return {
|
|
3065
3098
|
fileName: figma.root ? figma.root.name : "unknown",
|
|
3066
|
-
sessionId:
|
|
3099
|
+
sessionId: resolvedSid,
|
|
3067
3100
|
pageNodes: pageNodes,
|
|
3068
3101
|
styles: styles,
|
|
3069
3102
|
variables: variables,
|
|
@@ -3884,6 +3917,131 @@ handlers.get_variables = async function() {
|
|
|
3884
3917
|
return { collections: collections };
|
|
3885
3918
|
};
|
|
3886
3919
|
|
|
3920
|
+
// ─── EXPORT ASSETS (Batch Icon & Image Extractor) ────────────────────────────
|
|
3921
|
+
handlers.export_assets = async function(params) {
|
|
3922
|
+
var id = params ? (params.id || params.nodeId) : null;
|
|
3923
|
+
var targetNode = null;
|
|
3924
|
+
if (id) targetNode = await findNodeByIdAsync(id);
|
|
3925
|
+
if (!targetNode) {
|
|
3926
|
+
var sel = figma.currentPage.selection;
|
|
3927
|
+
targetNode = sel && sel.length > 0 ? sel[0] : figma.currentPage;
|
|
3928
|
+
}
|
|
3929
|
+
if (!targetNode) throw new Error("No node found to export assets from");
|
|
3930
|
+
|
|
3931
|
+
var icons = [];
|
|
3932
|
+
var images = [];
|
|
3933
|
+
var seenNames = {};
|
|
3934
|
+
|
|
3935
|
+
function sanitizeAssetName(rawName, fallback) {
|
|
3936
|
+
var name = (rawName || fallback)
|
|
3937
|
+
.toLowerCase()
|
|
3938
|
+
.replace(/^icon[s\/_\-\s]+/, '')
|
|
3939
|
+
.replace(/^ic[_\-\s]+/, '')
|
|
3940
|
+
.replace(/[\/\s_\.]+/g, '-')
|
|
3941
|
+
.replace(/[^a-z0-9\-]/g, '')
|
|
3942
|
+
.replace(/^-+|-+$/g, '');
|
|
3943
|
+
if (!name) name = fallback;
|
|
3944
|
+
if (seenNames[name]) {
|
|
3945
|
+
seenNames[name]++;
|
|
3946
|
+
return name + "-" + seenNames[name];
|
|
3947
|
+
}
|
|
3948
|
+
seenNames[name] = 1;
|
|
3949
|
+
return name;
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
function isVectorIcon(nd) {
|
|
3953
|
+
if (!nd || nd.visible === false) return false;
|
|
3954
|
+
var t = nd.type;
|
|
3955
|
+
if (t === "VECTOR" || t === "BOOLEAN_OPERATION" || t === "STAR" || t === "POLYGON") return true;
|
|
3956
|
+
if ((t === "FRAME" || t === "COMPONENT" || t === "INSTANCE" || t === "GROUP") && nd.width <= 96 && nd.height <= 96) {
|
|
3957
|
+
var n = nd.name.toLowerCase();
|
|
3958
|
+
if (n.indexOf("icon") >= 0 || n.indexOf("ic_") >= 0 || n.indexOf("ic-") >= 0) return true;
|
|
3959
|
+
if (nd.children && nd.children.length > 0 && nd.children.length <= 8) {
|
|
3960
|
+
var allVectors = nd.children.every(function(c) {
|
|
3961
|
+
return c.type === "VECTOR" || c.type === "BOOLEAN_OPERATION" || c.type === "LINE" || c.type === "ELLIPSE" || c.type === "RECTANGLE";
|
|
3962
|
+
});
|
|
3963
|
+
if (allVectors) return true;
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
return false;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
function hasImageFill(nd) {
|
|
3970
|
+
if (!nd || !nd.fills || !Array.isArray(nd.fills)) return false;
|
|
3971
|
+
return nd.fills.some(function(f) { return f && f.type === "IMAGE" && f.visible !== false; });
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
var nodesToInspect = [];
|
|
3975
|
+
function collectAssetNodes(nd, depth) {
|
|
3976
|
+
if (!nd || nd.visible === false || depth > 20) return;
|
|
3977
|
+
if (isVectorIcon(nd)) {
|
|
3978
|
+
nodesToInspect.push({ node: nd, kind: "icon" });
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
if (hasImageFill(nd)) {
|
|
3982
|
+
nodesToInspect.push({ node: nd, kind: "image" });
|
|
3983
|
+
}
|
|
3984
|
+
if (nd.children && Array.isArray(nd.children)) {
|
|
3985
|
+
for (var ci = 0; ci < nd.children.length; ci++) {
|
|
3986
|
+
collectAssetNodes(nd.children[ci], depth + 1);
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
if (targetNode === figma.currentPage) {
|
|
3992
|
+
for (var pi = 0; pi < targetNode.children.length; pi++) {
|
|
3993
|
+
collectAssetNodes(targetNode.children[pi], 0);
|
|
3994
|
+
}
|
|
3995
|
+
} else {
|
|
3996
|
+
collectAssetNodes(targetNode, 0);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
var exportLimit = Math.min(nodesToInspect.length, 60);
|
|
4000
|
+
for (var i = 0; i < exportLimit; i++) {
|
|
4001
|
+
var item = nodesToInspect[i];
|
|
4002
|
+
var nd = item.node;
|
|
4003
|
+
var baseName = sanitizeAssetName(nd.name, "asset-" + (i + 1));
|
|
4004
|
+
|
|
4005
|
+
if (item.kind === "icon") {
|
|
4006
|
+
try {
|
|
4007
|
+
var svgBytes = await nd.exportAsync({ format: "SVG" });
|
|
4008
|
+
var svgStr = String.fromCharCode.apply(null, Array.from(svgBytes));
|
|
4009
|
+
icons.push({
|
|
4010
|
+
id: nd.id,
|
|
4011
|
+
name: baseName,
|
|
4012
|
+
fileName: baseName + ".svg",
|
|
4013
|
+
width: Math.round(nd.width),
|
|
4014
|
+
height: Math.round(nd.height),
|
|
4015
|
+
svg: svgStr
|
|
4016
|
+
});
|
|
4017
|
+
} catch (e) {}
|
|
4018
|
+
} else if (item.kind === "image") {
|
|
4019
|
+
try {
|
|
4020
|
+
var pngBytes = await nd.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 2 } });
|
|
4021
|
+
var b64 = uint8ArrayToBase64(pngBytes);
|
|
4022
|
+
images.push({
|
|
4023
|
+
id: nd.id,
|
|
4024
|
+
name: baseName,
|
|
4025
|
+
fileName: baseName + ".png",
|
|
4026
|
+
width: Math.round(nd.width),
|
|
4027
|
+
height: Math.round(nd.height),
|
|
4028
|
+
dataUrl: "data:image/png;base64," + b64
|
|
4029
|
+
});
|
|
4030
|
+
} catch (e) {}
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
return {
|
|
4035
|
+
success: true,
|
|
4036
|
+
sourceNodeId: targetNode.id,
|
|
4037
|
+
sourceNodeName: targetNode.name,
|
|
4038
|
+
totalIcons: icons.length,
|
|
4039
|
+
totalImages: images.length,
|
|
4040
|
+
icons: icons,
|
|
4041
|
+
images: images
|
|
4042
|
+
};
|
|
4043
|
+
};
|
|
4044
|
+
|
|
3887
4045
|
|
|
3888
4046
|
// ── plugin-src/handlers-tokens.js ──
|
|
3889
4047
|
// ─── DESIGN TOKEN OPERATIONS (v1.7.0) ───────────────────────────────────────
|
|
@@ -5148,12 +5306,39 @@ figma.clientStorage.getAsync("mcp_window_size").then(function(saved) {
|
|
|
5148
5306
|
}
|
|
5149
5307
|
}).catch(function() {});
|
|
5150
5308
|
|
|
5309
|
+
// ─── UNIQUE SESSION IDENTIFIER (Multi-tab Support) ───────────────────────────
|
|
5310
|
+
// In Figma, figma.root.id is always "0:0" across all files. To support running
|
|
5311
|
+
// multiple plugins concurrently across multiple tabs/files, generate and persist
|
|
5312
|
+
// a unique document session ID in document pluginData.
|
|
5313
|
+
function getOrCreateSessionId() {
|
|
5314
|
+
if (figma.fileKey) {
|
|
5315
|
+
return figma.fileKey;
|
|
5316
|
+
}
|
|
5317
|
+
try {
|
|
5318
|
+
var stored = figma.root.getPluginData("mcp_session_id");
|
|
5319
|
+
if (stored && stored.length > 0) {
|
|
5320
|
+
return stored;
|
|
5321
|
+
}
|
|
5322
|
+
} catch(e) {}
|
|
5323
|
+
|
|
5324
|
+
var rand = Math.random().toString(36).substring(2, 10);
|
|
5325
|
+
var ts = Date.now().toString(36);
|
|
5326
|
+
var newId = "doc_" + rand + "_" + ts;
|
|
5327
|
+
try {
|
|
5328
|
+
figma.root.setPluginData("mcp_session_id", newId);
|
|
5329
|
+
} catch(e) {}
|
|
5330
|
+
return newId;
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
var currentSessionId = getOrCreateSessionId();
|
|
5334
|
+
var currentFileName = figma.root ? figma.root.name : "Untitled";
|
|
5335
|
+
|
|
5151
5336
|
// Broadcast active file / session metadata on startup
|
|
5152
5337
|
try {
|
|
5153
5338
|
figma.ui.postMessage({
|
|
5154
5339
|
type: "session-info",
|
|
5155
|
-
sessionId:
|
|
5156
|
-
fileName:
|
|
5340
|
+
sessionId: currentSessionId,
|
|
5341
|
+
fileName: currentFileName
|
|
5157
5342
|
});
|
|
5158
5343
|
} catch (e) {}
|
|
5159
5344
|
|
|
@@ -5162,13 +5347,26 @@ figma.on("selectionchange", function() {
|
|
|
5162
5347
|
try {
|
|
5163
5348
|
var sel = figma.currentPage.selection;
|
|
5164
5349
|
var summary = [];
|
|
5350
|
+
var fullNode = null;
|
|
5165
5351
|
for (var i = 0; i < Math.min(sel.length, 5); i++) {
|
|
5166
|
-
|
|
5352
|
+
var n = sel[i];
|
|
5353
|
+
summary.push({
|
|
5354
|
+
id: n.id,
|
|
5355
|
+
name: n.name,
|
|
5356
|
+
type: n.type,
|
|
5357
|
+
width: typeof n.width === "number" ? Math.round(n.width) : undefined,
|
|
5358
|
+
height: typeof n.height === "number" ? Math.round(n.height) : undefined
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
if (sel.length === 1 && typeof nodeToInfo === "function") {
|
|
5362
|
+
try { fullNode = nodeToInfo(sel[0]); } catch(e0) {}
|
|
5167
5363
|
}
|
|
5168
5364
|
figma.ui.postMessage({
|
|
5169
5365
|
type: "selection-change",
|
|
5170
5366
|
count: sel.length,
|
|
5171
|
-
|
|
5367
|
+
pageName: figma.currentPage ? figma.currentPage.name : undefined,
|
|
5368
|
+
selection: summary,
|
|
5369
|
+
fullNode: fullNode
|
|
5172
5370
|
});
|
|
5173
5371
|
} catch (e) {}
|
|
5174
5372
|
});
|
|
@@ -5190,6 +5388,28 @@ function onDocChange(event) {
|
|
|
5190
5388
|
var ids = Array.from(pendingChangedNodeIds);
|
|
5191
5389
|
pendingChangedNodeIds.clear();
|
|
5192
5390
|
|
|
5391
|
+
if (ids.length === 1) {
|
|
5392
|
+
try {
|
|
5393
|
+
var singleNode = await findNodeByIdAsync(ids[0]);
|
|
5394
|
+
if (singleNode) {
|
|
5395
|
+
figma.ui.postMessage({
|
|
5396
|
+
type: "delta-diff",
|
|
5397
|
+
id: singleNode.id,
|
|
5398
|
+
diff: {
|
|
5399
|
+
name: singleNode.name,
|
|
5400
|
+
characters: "characters" in singleNode ? singleNode.characters : undefined,
|
|
5401
|
+
visible: singleNode.visible,
|
|
5402
|
+
width: singleNode.width,
|
|
5403
|
+
height: singleNode.height,
|
|
5404
|
+
x: singleNode.x,
|
|
5405
|
+
y: singleNode.y
|
|
5406
|
+
}
|
|
5407
|
+
});
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5410
|
+
} catch(e1) {}
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5193
5413
|
if (ids.length > 0 && ids.length <= 15) {
|
|
5194
5414
|
// Incremental micro-diff update for fast real-time editing
|
|
5195
5415
|
var diffNodes = [];
|
|
@@ -5209,7 +5429,7 @@ function onDocChange(event) {
|
|
|
5209
5429
|
|
|
5210
5430
|
// Fallback for large batch changes: mark dirty or trigger background sync
|
|
5211
5431
|
figma.ui.postMessage({ type: "document-change" });
|
|
5212
|
-
},
|
|
5432
|
+
}, 500);
|
|
5213
5433
|
} catch (e) {}
|
|
5214
5434
|
}
|
|
5215
5435
|
|
|
@@ -5235,8 +5455,8 @@ setTimeout(async function() {
|
|
|
5235
5455
|
figma.ui.postMessage({
|
|
5236
5456
|
type: "index-update",
|
|
5237
5457
|
data: scanResult,
|
|
5238
|
-
fileName:
|
|
5239
|
-
sessionId:
|
|
5458
|
+
fileName: currentFileName,
|
|
5459
|
+
sessionId: currentSessionId,
|
|
5240
5460
|
startMs: startMs
|
|
5241
5461
|
});
|
|
5242
5462
|
}
|
|
@@ -5288,6 +5508,43 @@ figma.ui.onmessage = async (request) => {
|
|
|
5288
5508
|
return;
|
|
5289
5509
|
}
|
|
5290
5510
|
|
|
5511
|
+
// Zoom to selection when clicked on selection bar
|
|
5512
|
+
if (request.type === "zoom-to-selection") {
|
|
5513
|
+
try {
|
|
5514
|
+
if (figma.currentPage.selection.length > 0) {
|
|
5515
|
+
figma.viewport.scrollAndZoomIntoView(figma.currentPage.selection);
|
|
5516
|
+
figma.notify("Focused on selection", { timeout: 1000 });
|
|
5517
|
+
}
|
|
5518
|
+
} catch(e) {}
|
|
5519
|
+
return;
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
// Export selection image as PNG for UI clipboard/download
|
|
5523
|
+
if (request.type === "export-selection-image") {
|
|
5524
|
+
try {
|
|
5525
|
+
var sel = figma.currentPage.selection;
|
|
5526
|
+
if (!sel || sel.length === 0) {
|
|
5527
|
+
figma.notify("Please select a layer to capture", { timeout: 1500 });
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
var targetNode = sel[0];
|
|
5531
|
+
var scale = request.scale || 2;
|
|
5532
|
+
var bytes = await targetNode.exportAsync({
|
|
5533
|
+
format: "PNG",
|
|
5534
|
+
constraint: { type: "SCALE", value: scale }
|
|
5535
|
+
});
|
|
5536
|
+
figma.ui.postMessage({
|
|
5537
|
+
type: "selection-image-exported",
|
|
5538
|
+
nodeId: targetNode.id,
|
|
5539
|
+
nodeName: targetNode.name,
|
|
5540
|
+
bytes: Array.from(bytes)
|
|
5541
|
+
});
|
|
5542
|
+
} catch (err) {
|
|
5543
|
+
figma.notify("Export failed: " + (err && err.message ? err.message : String(err)), { error: true });
|
|
5544
|
+
}
|
|
5545
|
+
return;
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5291
5548
|
// Handle manual reindex request from UI
|
|
5292
5549
|
if (request.type === "manual-reindex") {
|
|
5293
5550
|
try {
|
|
@@ -5297,8 +5554,8 @@ figma.ui.onmessage = async (request) => {
|
|
|
5297
5554
|
figma.ui.postMessage({
|
|
5298
5555
|
type: "index-update",
|
|
5299
5556
|
data: scanResult,
|
|
5300
|
-
fileName:
|
|
5301
|
-
sessionId:
|
|
5557
|
+
fileName: currentFileName,
|
|
5558
|
+
sessionId: currentSessionId,
|
|
5302
5559
|
startMs: startMs
|
|
5303
5560
|
});
|
|
5304
5561
|
}
|