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 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, Antigravity, VS Code, Zed) to **draw UI directly on the Figma canvas** and **read existing designs back** as structured JSON trees, design tokens, CSS, and screenshots — with zero external API keys needed.
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
- - **Bidirectional Bridge**: AI can both write UI (draw vectors, components, frames, autolayout) and read designs (extract hierarchy, compute styles, capture canvas).
13
- - **Sandboxed JS Runtime**: Powered by **Boa** (pure Rust ECMAScript engine) for safe execution of design scripts.
14
- - **Built-in Asset CDN**: Auto-fetches and normalizes SVG icons from 7 icon libraries (Ionicons, Lucide, Tabler, Bootstrap, Fluent, Phosphor).
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` via HTTP long-polling.
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
- │ Terminal (Standalone Server) │
25
- │ figma-mcp │
26
- │ • MCP SSE Transport (/sse, /message) │
27
- │ • Sandboxed JS Runtime (Boa) │
28
- │ • Built-in Asset & Icon Resolver │
29
- │ • Async WebSocket & HTTP Long-Polling Bridge (Axum) │
30
- └───────────────▲─────────────────────────▲───────────────┘
31
- │ (ws://127.0.0.1:38451) │ (http://127.0.0.1:38451/sse)
32
- │ │
33
- ┌─────────┴─────────┐ ┌─────────┴─────────┐
34
- │ Figma Desktop │ │ AI Assistant(s) │
35
- │ (Plugin Bridge) │ │ Antigravity/Cursor│
36
- └───────────────────┘ │ Claude Code / Zed │
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
- You can launch `figma-rust-mcp` on any Mac, Linux, or Windows machine with zero setup:
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
- If you use SSE transport across multiple AI IDEs (Antigravity, Claude, Cursor, Windsurf) and want `figma-rust-mcp` to **automatically start on OS login and keep running in the background**:
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
- # Check background service status
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. Or Build from Source (Rust)
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 the file: `plugin/manifest.json` from this repository.
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 show a green dot (**Connected**).
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` exposes 7 first-class MCP tools:
150
+ `figma-mcp` provides **12 first-class MCP tools**:
141
151
 
142
- ### `figma_get_selection`
143
- Ultra-fast, token-compressed inspection of currently selected layers/frames on the Figma canvas. Returns compacted layout and typography hierarchy with 60-80% fewer tokens.
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 complete design context, CSS, auto-layout rules, and typography.
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
- ### `figma_export_asset`
149
- Exports any Figma node/layer directly into PNG, JPG, or SVG.
150
- - `outputPath`: When specified (e.g. `src/assets/logo.png`), saves directly to disk and returns minimal JSON metadata instead of filling context window with large base64 strings!
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
- ### `figma_status`
153
- Checks live bridge connection status, connected Figma tabs, active file name, and latency statistics.
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
- ### `figma_write`
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
- ### `figma_read`
162
- Universal reader for advanced queries: `get_design`, `scan_design`, `screenshot`, `export_svg`, `get_variables`, `get_styles`, `search_nodes`. Supports `outputPath` for file exports.
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
- - `section`: `"all"` | `"rules"` | `"layout"` | `"api"` | `"tokens"` | `"icons"`
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 directly in debug mode
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.8.4';
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.8.4",
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-26T04:26:26.667Z
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.3",
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
- return { dataUrl: "data:image/png;base64," + uint8ArrayToBase64(bytes), nodeId: node.id, width: node.width, height: node.height };
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: figma.root ? figma.root.id : "_default",
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: figma.root.id,
5156
- fileName: figma.root.name
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
- summary.push({ id: sel[i].id, name: sel[i].name, type: sel[i].type });
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
- selection: summary
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
- }, 800);
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: figma.root ? figma.root.name : "unknown",
5239
- sessionId: figma.root ? figma.root.id : "_default",
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: figma.root ? figma.root.name : "unknown",
5301
- sessionId: figma.root ? figma.root.id : "_default",
5557
+ fileName: currentFileName,
5558
+ sessionId: currentSessionId,
5302
5559
  startMs: startMs
5303
5560
  });
5304
5561
  }