figma-rust-mcp 4.0.1 → 5.0.0
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 +32 -13
- package/bin/agent-config.js +2 -2
- package/bin/figma-rust-mcp.js +13 -4
- package/package.json +2 -1
- package/plugin/manifest.json +25 -13
- package/plugin/ui.html +37 -29
- package/scripts/bench.mjs +37 -0
- package/scripts/test-plugin-tokens.mjs +21 -7
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
9
9
|
## ⚡ Highlights
|
|
10
10
|
|
|
11
11
|
- **Lossless Rust Tree Compression**: Compact/minimal tree responses share identical style bundles when the complete JSON becomes smaller. Node IDs, hierarchy, text, geometry and state differences remain intact. Full detail bypasses Rust compression; no heuristic merging of screens or repeated items.
|
|
12
|
-
- **Pure Rust Native Performance**:
|
|
12
|
+
- **Pure Rust Native Performance**: Single native binary, no runtime or GC. Measure it on your machine with `npm run bench` or the plugin's **Bench** tab (see [Benchmarks](#-benchmarks)).
|
|
13
13
|
- **Scoped In-Memory Index (`figma_index`)**: Indexes page roots first and loads frame descendants on demand. Search reports the indexed scope and completeness.
|
|
14
14
|
- **Binary IPC & Chunk Streaming**: Powered by **MessagePack** (`rmp-serde`) and progressive subtree chunking for instant transfers of massive design files.
|
|
15
15
|
- **Revisioned Updates**: Property patches update affected nodes; revision gaps trigger a fresh snapshot.
|
|
@@ -18,7 +18,7 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
18
18
|
- **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.
|
|
19
19
|
- **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*).
|
|
20
20
|
- **Multi-Tab / Multi-Session Support**: Seamlessly connects to multiple open Figma files simultaneously.
|
|
21
|
-
- **100% Localhost Privacy**: All communication stays strictly on `127.0.0.1:
|
|
21
|
+
- **100% Localhost Privacy**: All communication stays strictly on `127.0.0.1:41730`.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -30,13 +30,13 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
30
30
|
│ figma-rust-mcp (Pure Rust Engine) │
|
|
31
31
|
│ • MCP Streamable HTTP (/mcp) & legacy SSE (/sse, /message) │
|
|
32
32
|
│ • Dynamic Runtime Server (/plugin/code.js, /plugin/ui.html) │
|
|
33
|
-
│ • In-Memory Fast Index (
|
|
33
|
+
│ • In-Memory Fast Index (Scoped Lookups & Incremental Diffs) │
|
|
34
34
|
│ • Design-to-Code Compiler (React/Tailwind, Vue, RN, SwiftUI) │
|
|
35
35
|
│ • Design Token Transformer (CSS, Tailwind, TypeScript, W3C DTCG) │
|
|
36
36
|
│ • Sandboxed JS Runtime (Boa ECMAScript Engine) │
|
|
37
37
|
│ • Binary MessagePack & Progressive Subtree Chunk Receiver │
|
|
38
38
|
└───────────────▲────────────────────────────────────────▲───────────────┘
|
|
39
|
-
│ (ws://127.0.0.1:
|
|
39
|
+
│ (ws://127.0.0.1:41730/ws) │ (http://127.0.0.1:41730/mcp)
|
|
40
40
|
│ (Dynamic Code Streaming & Hot-Reload) │ (JSON-RPC 2.0 / Streamable HTTP)
|
|
41
41
|
┌─────────┴─────────┐ ┌─────────┴─────────┐
|
|
42
42
|
│ Figma Desktop │ │ AI Assistant(s) │
|
|
@@ -50,9 +50,10 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
50
50
|
|
|
51
51
|
### 🔌 How the Zero-Touch Connection Works:
|
|
52
52
|
1. **Permanent Thin Loader**: You only import the Figma plugin **once** (`~/.figma-rust-mcp/plugin/manifest.json`).
|
|
53
|
-
2. **Dynamic Runtime Streaming**: Upon launch, the Thin Loader contacts `http://127.0.0.1:
|
|
54
|
-
3. **Live WebSocket Hot-Reload**: The plugin connects to `ws://127.0.0.1:
|
|
53
|
+
2. **Dynamic Runtime Streaming**: Upon launch, the Thin Loader contacts `http://127.0.0.1:41730/plugin/code.js` to fetch and execute the latest runtime in memory.
|
|
54
|
+
3. **Live WebSocket Hot-Reload**: The plugin connects to `ws://127.0.0.1:41730/ws`. When you upgrade `figma-rust-mcp` or restart the daemon, the plugin automatically detects the new server version and hot-reloads seamlessly — **no need to re-import or restart the plugin in Figma**.
|
|
55
55
|
4. **Auto-Reconnect & Offline Buffer**: If Figma is opened before the Rust service starts, the plugin displays a waiting screen and connects automatically the instant the service is up.
|
|
56
|
+
5. **Port Range & Auto-Switch**: The bridge uses ports `41730–41739`. On start it reuses a running server of the same version, skips any port held by another app or a different figma-rust-mcp version, and starts on the first free port. The plugin scans the same range, so it follows automatically. MCP clients configured by URL point at `41730`; the server prints a warning with the new port if it had to move.
|
|
56
57
|
|
|
57
58
|
---
|
|
58
59
|
|
|
@@ -148,13 +149,13 @@ Use the `/mcp` endpoint. It accepts `POST` JSON-RPC requests directly:
|
|
|
148
149
|
|
|
149
150
|
```toml
|
|
150
151
|
[mcp_servers.figma-rust-mcp]
|
|
151
|
-
url = "http://127.0.0.1:
|
|
152
|
+
url = "http://127.0.0.1:41730/mcp"
|
|
152
153
|
```
|
|
153
154
|
|
|
154
155
|
For Codex CLI, the equivalent command is:
|
|
155
156
|
|
|
156
157
|
```bash
|
|
157
|
-
codex mcp add figma-rust-mcp --url http://127.0.0.1:
|
|
158
|
+
codex mcp add figma-rust-mcp --url http://127.0.0.1:41730/mcp
|
|
158
159
|
```
|
|
159
160
|
|
|
160
161
|
#### Google Antigravity (SSE Transport)
|
|
@@ -163,7 +164,7 @@ Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.js
|
|
|
163
164
|
{
|
|
164
165
|
"mcpServers": {
|
|
165
166
|
"figma-rust-mcp": {
|
|
166
|
-
"serverUrl": "http://127.0.0.1:
|
|
167
|
+
"serverUrl": "http://127.0.0.1:41730/sse"
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
}
|
|
@@ -176,7 +177,7 @@ These clients can use the Streamable HTTP `/mcp` endpoint:
|
|
|
176
177
|
{
|
|
177
178
|
"mcpServers": {
|
|
178
179
|
"figma-rust-mcp": {
|
|
179
|
-
"url": "http://127.0.0.1:
|
|
180
|
+
"url": "http://127.0.0.1:41730/mcp"
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
}
|
|
@@ -200,7 +201,7 @@ Alternatively, configure a stdio subprocess via NPX:
|
|
|
200
201
|
Check the runtime bundle currently served to Figma Desktop:
|
|
201
202
|
|
|
202
203
|
```bash
|
|
203
|
-
curl http://127.0.0.1:
|
|
204
|
+
curl http://127.0.0.1:41730/plugin/version
|
|
204
205
|
```
|
|
205
206
|
|
|
206
207
|
The response includes `version` and `runtimeHash`. If the hash changes after an
|
|
@@ -283,7 +284,7 @@ Batch extracts all SVG icons and PNG images from a Figma frame/page directly int
|
|
|
283
284
|
- **`createBarrel`**: Automatically creates an `index.ts` barrel export file in `iconDir`.
|
|
284
285
|
|
|
285
286
|
### 8. `figma_index`
|
|
286
|
-
|
|
287
|
+
In-memory queries against pre-indexed Figma file structures (latency for your file: plugin **Bench** tab).
|
|
287
288
|
- **`operation`**:
|
|
288
289
|
- `"status"`: View index health and node counts.
|
|
289
290
|
- `"search_nodes"`: Search nodes by text name, query, and type (`FRAME`, `TEXT`, `COMPONENT`, `INSTANCE`).
|
|
@@ -315,7 +316,7 @@ Executes JavaScript draw commands inside the sandboxed VM to build or modify des
|
|
|
315
316
|
- Supports Prototyping & Reactions: `setReactions`, `getReactions`, `setScrollBehavior`.
|
|
316
317
|
|
|
317
318
|
### 11. `figma_rules`
|
|
318
|
-
Audits the current Figma document and generates a complete design system rule sheet (color tokens, typography styles, variables, component catalog)
|
|
319
|
+
Audits the current Figma document and generates a complete design system rule sheet (color tokens, typography styles, variables, component catalog) from the in-memory cache.
|
|
319
320
|
|
|
320
321
|
### 12. `figma_docs`
|
|
321
322
|
Fetches built-in documentation, design rules, layout guidelines, and code examples for `figma_write`.
|
|
@@ -419,6 +420,24 @@ Revision gaps request resynchronization. Page switches rebuild the shallow
|
|
|
419
420
|
index without preloading every page. Exact-read caching evicts one least
|
|
420
421
|
recently used entry when its 64-entry capacity is reached.
|
|
421
422
|
|
|
423
|
+
## 📏 Benchmarks
|
|
424
|
+
|
|
425
|
+
`npm run bench` builds the release binary, spawns it on a random port and prints:
|
|
426
|
+
|
|
427
|
+
| Metric | How it is measured |
|
|
428
|
+
|---|---|
|
|
429
|
+
| Cold start | `spawn` → first successful `GET /health`, from outside the process |
|
|
430
|
+
| Startup | `main()` entry → HTTP listener bound |
|
|
431
|
+
| RAM | Resident set size (macOS/Linux; not reported on Windows) |
|
|
432
|
+
| MCP `tools/list` | 200 in-process dispatches, p50/p95 |
|
|
433
|
+
| Index search | 200 searches over the current tab's index, p50/p95 (plugin tab only) |
|
|
434
|
+
| Plugin round trip | 10 `get_viewport` calls through Figma, p50/p95 (plugin tab only) |
|
|
435
|
+
|
|
436
|
+
The plugin's **Bench** tab runs the same report (`POST /benchmark`) against the
|
|
437
|
+
live server, so index and plugin numbers reflect your real file. Sample run on an
|
|
438
|
+
Apple Silicon Mac (v4.0.1): cold start 45–530ms, startup 26–126ms, RSS ~7.6MB,
|
|
439
|
+
`tools/list` p50 0.07ms. Your numbers will differ; quote your own.
|
|
440
|
+
|
|
422
441
|
## 💻 Development & Testing
|
|
423
442
|
|
|
424
443
|
```bash
|
package/bin/agent-config.js
CHANGED
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
5
5
|
|
|
6
6
|
const SERVER_NAME = 'figma-rust-mcp';
|
|
7
|
-
const HTTP_URL = 'http://127.0.0.1:
|
|
7
|
+
const HTTP_URL = 'http://127.0.0.1:41730/mcp';
|
|
8
8
|
|
|
9
9
|
function existsAny(paths) {
|
|
10
10
|
return paths.some(candidate => candidate && fs.existsSync(candidate));
|
|
@@ -78,7 +78,7 @@ function upsertCodex(filePath) {
|
|
|
78
78
|
|
|
79
79
|
export function configureAgent(agent) {
|
|
80
80
|
const entry = agent.id === 'antigravity'
|
|
81
|
-
? { serverUrl: 'http://127.0.0.1:
|
|
81
|
+
? { serverUrl: 'http://127.0.0.1:41730/sse' }
|
|
82
82
|
: agent.id === 'claude' || agent.id === 'vscode'
|
|
83
83
|
? { type: 'http', url: HTTP_URL }
|
|
84
84
|
: { url: HTTP_URL };
|
package/bin/figma-rust-mcp.js
CHANGED
|
@@ -435,6 +435,14 @@ function isNewer(latest, current) {
|
|
|
435
435
|
return false;
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
// --upgrade stays on the installed major; a new major is announced but never auto-installed.
|
|
439
|
+
function updateNotice(latest) {
|
|
440
|
+
if (latest.split('.')[0] !== pkgVersion.split('.')[0]) {
|
|
441
|
+
return `\n\x1b[36m⚡ v${latest} is a new major release with breaking changes (see CHANGELOG).\x1b[0m\n\x1b[36m Opt in: npm install -g figma-rust-mcp@latest\x1b[0m\n\n`;
|
|
442
|
+
}
|
|
443
|
+
return `\n\x1b[36m⚡ Update available: v${pkgVersion} → v${latest}\x1b[0m\n\x1b[36m Run: npx figma-rust-mcp --upgrade\x1b[0m\n\n`;
|
|
444
|
+
}
|
|
445
|
+
|
|
438
446
|
function checkForUpdatesAsync() {
|
|
439
447
|
const updateStateFile = path.join(os.homedir(), '.figma-rust-mcp', 'update-check.json');
|
|
440
448
|
try {
|
|
@@ -443,7 +451,7 @@ function checkForUpdatesAsync() {
|
|
|
443
451
|
const now = Date.now();
|
|
444
452
|
if (state.lastChecked && (now - state.lastChecked < 6 * 3600 * 1000)) {
|
|
445
453
|
if (state.latestVersion && isNewer(state.latestVersion, pkgVersion)) {
|
|
446
|
-
process.stderr.write(
|
|
454
|
+
process.stderr.write(updateNotice(state.latestVersion));
|
|
447
455
|
}
|
|
448
456
|
return;
|
|
449
457
|
}
|
|
@@ -467,7 +475,7 @@ function checkForUpdatesAsync() {
|
|
|
467
475
|
latestVersion: latest
|
|
468
476
|
}, null, 2));
|
|
469
477
|
if (latest && isNewer(latest, pkgVersion)) {
|
|
470
|
-
process.stderr.write(
|
|
478
|
+
process.stderr.write(updateNotice(latest));
|
|
471
479
|
}
|
|
472
480
|
}
|
|
473
481
|
} catch (_) {}
|
|
@@ -480,8 +488,9 @@ function checkForUpdatesAsync() {
|
|
|
480
488
|
async function upgrade() {
|
|
481
489
|
console.log('\x1b[36m[figma-rust-mcp]\x1b[0m Checking for updates and upgrading figma-rust-mcp...');
|
|
482
490
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
491
|
+
const major = pkgVersion ? pkgVersion.split('.')[0] : 'latest';
|
|
492
|
+
execSync(`npm install -g figma-rust-mcp@${major}`, { stdio: 'inherit' });
|
|
493
|
+
console.log(`\x1b[32m✓ Upgraded figma-rust-mcp (${major === 'latest' ? 'latest' : `v${major}.x`}).\x1b[0m`);
|
|
485
494
|
setupPlugin();
|
|
486
495
|
if (isServiceInstalled()) {
|
|
487
496
|
console.log('\x1b[36mUpdating background service binary...\x1b[0m');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
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-rust-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"test": "./scripts/test-rust-mcp.sh",
|
|
21
21
|
"test:tokens": "node --test scripts/test-plugin-tokens.mjs && cargo test",
|
|
22
22
|
"dev": "cargo run",
|
|
23
|
+
"bench": "cargo build --release && node scripts/bench.mjs",
|
|
23
24
|
"test:sessions": "cargo build && node --test scripts/test-bridge-sessions.mjs"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
package/plugin/manifest.json
CHANGED
|
@@ -5,21 +5,33 @@
|
|
|
5
5
|
"main": "code.js",
|
|
6
6
|
"ui": "ui.html",
|
|
7
7
|
"documentAccess": "dynamic-page",
|
|
8
|
-
"editorType": [
|
|
8
|
+
"editorType": [
|
|
9
|
+
"figma",
|
|
10
|
+
"dev"
|
|
11
|
+
],
|
|
9
12
|
"networkAccess": {
|
|
10
13
|
"allowedDomains": [
|
|
11
|
-
"http://localhost:
|
|
12
|
-
"http://localhost:
|
|
13
|
-
"http://localhost:
|
|
14
|
-
"http://localhost:
|
|
15
|
-
"http://localhost:
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
14
|
+
"http://localhost:41730",
|
|
15
|
+
"http://localhost:41731",
|
|
16
|
+
"http://localhost:41732",
|
|
17
|
+
"http://localhost:41733",
|
|
18
|
+
"http://localhost:41734",
|
|
19
|
+
"http://localhost:41735",
|
|
20
|
+
"http://localhost:41736",
|
|
21
|
+
"http://localhost:41737",
|
|
22
|
+
"http://localhost:41738",
|
|
23
|
+
"http://localhost:41739",
|
|
24
|
+
"ws://localhost:41730",
|
|
25
|
+
"ws://localhost:41731",
|
|
26
|
+
"ws://localhost:41732",
|
|
27
|
+
"ws://localhost:41733",
|
|
28
|
+
"ws://localhost:41734",
|
|
29
|
+
"ws://localhost:41735",
|
|
30
|
+
"ws://localhost:41736",
|
|
31
|
+
"ws://localhost:41737",
|
|
32
|
+
"ws://localhost:41738",
|
|
33
|
+
"ws://localhost:41739"
|
|
21
34
|
],
|
|
22
|
-
"reasoning": "Thin loader connecting dynamically to the local figma-rust-mcp Rust engine on ports
|
|
35
|
+
"reasoning": "Thin loader connecting dynamically to the local figma-rust-mcp Rust engine on ports 41730-41739."
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
|
-
|
package/plugin/ui.html
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
<title>Figma Rust MCP Loader</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
|
-
--bg: #
|
|
10
|
-
--surface-base: #
|
|
11
|
-
--surface-card: #
|
|
12
|
-
--border-subtle:
|
|
13
|
-
--border-ghost:
|
|
14
|
-
--text-primary: #
|
|
15
|
-
--text-secondary: #
|
|
16
|
-
--text-muted: #
|
|
17
|
-
--ghost-lavender: #
|
|
18
|
-
--ghost-mint: #
|
|
19
|
-
--font-ui: -apple-system, BlinkMacSystemFont, 'Inter',
|
|
20
|
-
--font-mono: '
|
|
9
|
+
--bg: #fdf1dc;
|
|
10
|
+
--surface-base: #fffaf0;
|
|
11
|
+
--surface-card: #fffdf7;
|
|
12
|
+
--border-subtle: #1c1a17;
|
|
13
|
+
--border-ghost: #1c1a17;
|
|
14
|
+
--text-primary: #1c1a17;
|
|
15
|
+
--text-secondary: #4a453d;
|
|
16
|
+
--text-muted: #7d7567;
|
|
17
|
+
--ghost-lavender: #f4532c;
|
|
18
|
+
--ghost-mint: #2f8f3a;
|
|
19
|
+
--font-ui: ui-rounded, 'SF Pro Rounded', -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
|
|
20
|
+
--font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;
|
|
21
21
|
}
|
|
22
22
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
23
|
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg); color: var(--text-primary); font-family: var(--font-ui); font-size: 11px; }
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
height: 14px;
|
|
59
59
|
border-radius: 50%;
|
|
60
60
|
background: var(--ghost-lavender);
|
|
61
|
-
|
|
61
|
+
border: 2px solid #1c1a17;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
@keyframes pulse {
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
gap: 6px;
|
|
86
86
|
padding: 4px 10px;
|
|
87
87
|
background: var(--surface-card);
|
|
88
|
-
border:
|
|
88
|
+
border: 2px solid var(--border-subtle);
|
|
89
|
+
box-shadow: 2px 2px 0 #1c1a17;
|
|
89
90
|
border-radius: 9999px;
|
|
90
91
|
font-family: var(--font-mono);
|
|
91
92
|
font-size: 10px;
|
|
@@ -94,7 +95,8 @@
|
|
|
94
95
|
}
|
|
95
96
|
.hint-box {
|
|
96
97
|
background: var(--surface-base);
|
|
97
|
-
border:
|
|
98
|
+
border: 2px solid var(--border-subtle);
|
|
99
|
+
box-shadow: 2px 2px 0 #1c1a17;
|
|
98
100
|
border-radius: 8px;
|
|
99
101
|
padding: 10px 12px;
|
|
100
102
|
max-width: 280px;
|
|
@@ -108,7 +110,7 @@
|
|
|
108
110
|
color: var(--ghost-lavender);
|
|
109
111
|
display: block;
|
|
110
112
|
margin-top: 4px;
|
|
111
|
-
background: rgba(
|
|
113
|
+
background: rgba(28,26,23,0.06);
|
|
112
114
|
padding: 3px 6px;
|
|
113
115
|
border-radius: 4px;
|
|
114
116
|
}
|
|
@@ -123,9 +125,9 @@
|
|
|
123
125
|
<div class="radar-core"></div>
|
|
124
126
|
</div>
|
|
125
127
|
<div class="title">Connecting to figma-rust-mcp</div>
|
|
126
|
-
<div id="status" class="status-text">Scanning local ports (
|
|
128
|
+
<div id="status" class="status-text">Scanning local ports (41730-41739)...</div>
|
|
127
129
|
<div class="badge">
|
|
128
|
-
<span>●</span> <span id="port-label">localhost:
|
|
130
|
+
<span>●</span> <span id="port-label">localhost:41730</span>
|
|
129
131
|
</div>
|
|
130
132
|
<div class="hint-box">
|
|
131
133
|
Ensure the local Rust server is running:
|
|
@@ -134,7 +136,8 @@
|
|
|
134
136
|
</div>
|
|
135
137
|
|
|
136
138
|
<script>
|
|
137
|
-
|
|
139
|
+
// Must match DEFAULT_PORT / PORT_RANGE in src/bridge/server.rs and manifest.json.
|
|
140
|
+
const PORTS = Array.from({ length: 10 }, (_, i) => 41730 + i);
|
|
138
141
|
let attempts = 0;
|
|
139
142
|
|
|
140
143
|
async function checkPort(port) {
|
|
@@ -148,8 +151,9 @@
|
|
|
148
151
|
clearTimeout(timeoutId);
|
|
149
152
|
if (res.ok) {
|
|
150
153
|
const data = await res.json();
|
|
151
|
-
return { ok: true, data };
|
|
154
|
+
if (data && data.name === "figma-rust-mcp") return { ok: true, data };
|
|
152
155
|
}
|
|
156
|
+
return { ok: false, answered: true };
|
|
153
157
|
} catch (_) {
|
|
154
158
|
clearTimeout(timeoutId);
|
|
155
159
|
}
|
|
@@ -174,7 +178,8 @@
|
|
|
174
178
|
if (!uiRes.ok) throw new Error("Failed to load plugin UI: " + uiRes.status);
|
|
175
179
|
const uiHtml = await uiRes.text();
|
|
176
180
|
|
|
177
|
-
// 3. Inject full UI cleanly
|
|
181
|
+
// 3. Inject full UI cleanly; the runtime reads the port from here (globals survive document.open).
|
|
182
|
+
window.__FIGMA_MCP_PORT = port;
|
|
178
183
|
document.open();
|
|
179
184
|
document.write(uiHtml);
|
|
180
185
|
document.close();
|
|
@@ -190,17 +195,20 @@
|
|
|
190
195
|
const statusEl = document.getElementById("status");
|
|
191
196
|
const portEl = document.getElementById("port-label");
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
if (portEl) portEl.textContent = `localhost:${PORTS[0]}-${PORTS[PORTS.length - 1]}`;
|
|
199
|
+
const results = await Promise.all(PORTS.map(checkPort));
|
|
200
|
+
const hit = results.findIndex(r => r.ok);
|
|
201
|
+
if (hit >= 0) {
|
|
202
|
+
if (portEl) portEl.textContent = `localhost:${PORTS[hit]}`;
|
|
203
|
+
await bootstrapFromPort(PORTS[hit]);
|
|
204
|
+
return;
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
if (statusEl) {
|
|
203
|
-
|
|
208
|
+
const busy = PORTS.filter((_, i) => results[i].answered);
|
|
209
|
+
statusEl.textContent = busy.length
|
|
210
|
+
? `Port ${busy.join(", ")} answered but is not a compatible figma-rust-mcp. Run: npx -y figma-rust-mcp@latest --install-service (attempt #${attempts})`
|
|
211
|
+
: `Waiting for server (attempt #${attempts})...`;
|
|
204
212
|
}
|
|
205
213
|
setTimeout(findAndConnect, 1000);
|
|
206
214
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Benchmark: cold start measured from outside (spawn → /health answers), then the
|
|
2
|
+
// server's in-process report from POST /benchmark (same numbers as the plugin "Bench" tab).
|
|
3
|
+
// Usage: npm run bench or node scripts/bench.mjs [path/to/figma-rust-mcp]
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { statSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
const bin = process.argv[2] ?? `target/release/figma-rust-mcp${process.platform === "win32" ? ".exe" : ""}`;
|
|
8
|
+
const port = 40000 + Math.floor(Math.random() * 10000);
|
|
9
|
+
const base = `http://127.0.0.1:${port}`;
|
|
10
|
+
|
|
11
|
+
const t0 = performance.now();
|
|
12
|
+
const child = spawn(bin, ["--server", "--port", String(port)], { stdio: "ignore" });
|
|
13
|
+
let coldStartMs;
|
|
14
|
+
try {
|
|
15
|
+
while (coldStartMs === undefined) {
|
|
16
|
+
if (performance.now() - t0 > 10_000) throw new Error("server did not answer /health within 10s");
|
|
17
|
+
try {
|
|
18
|
+
if ((await fetch(`${base}/health`)).ok) coldStartMs = performance.now() - t0;
|
|
19
|
+
} catch {
|
|
20
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const r = await (await fetch(`${base}/benchmark`, { method: "POST" })).json();
|
|
24
|
+
const lat = (s) => (s ? `p50 ${s.p50Ms.toFixed(3)}ms · p95 ${s.p95Ms.toFixed(3)}ms (n=${s.n})` : "skipped");
|
|
25
|
+
const rows = {
|
|
26
|
+
version: `v${r.version}`,
|
|
27
|
+
"binary size": `${(statSync(bin).size / 1024 / 1024).toFixed(1)}MB`,
|
|
28
|
+
"cold start (spawn → /health)": `${coldStartMs.toFixed(1)}ms`,
|
|
29
|
+
"startup (main → listening)": r.startupMs != null ? `${r.startupMs.toFixed(2)}ms` : "—",
|
|
30
|
+
"RAM (RSS)": r.memoryMb != null ? `${r.memoryMb.toFixed(1)}MB` : "n/a on this OS",
|
|
31
|
+
"MCP tools/list (in-process)": lat(r.mcpToolsList),
|
|
32
|
+
"plugin round trip": r.pluginRoundTrip ? lat(r.pluginRoundTrip) : "skipped (fresh server has no Figma session)",
|
|
33
|
+
};
|
|
34
|
+
for (const [k, v] of Object.entries(rows)) console.log(`${k.padEnd(30)} ${v}`);
|
|
35
|
+
} finally {
|
|
36
|
+
child.kill();
|
|
37
|
+
}
|
|
@@ -53,6 +53,20 @@ test('compact tree and scan preserve mixed typography instead of using the first
|
|
|
53
53
|
assert.equal(r.resolveTextStyle(node).fontWeight, undefined);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test('scan_design expands instances by default and flags opaque ones as incomplete', async () => {
|
|
57
|
+
const text = { id: '1:3', name: 'Label', type: 'TEXT', characters: 'Hi', fontSize: 12, fontName: { family: 'Inter', style: 'Regular' }, visible: true };
|
|
58
|
+
const inst = { id: '1:2', name: 'Button', type: 'INSTANCE', visible: true, children: [text], getMainComponentAsync: async () => null };
|
|
59
|
+
const frame = { id: '1:1', name: 'Frame', type: 'FRAME', visible: true, children: [inst] };
|
|
60
|
+
const r = runtime({ root: { id: '0:0' }, currentPage: { id: '0:1' }, getNodeByIdAsync: async () => frame });
|
|
61
|
+
const full = plain(await r.handlers.scan_design({ id: '1:1' }));
|
|
62
|
+
assert.equal(full.totals.textNodes, 1);
|
|
63
|
+
assert.equal(full.complete, true);
|
|
64
|
+
const opaque = plain(await r.handlers.scan_design({ id: '1:1', expandInstances: false }));
|
|
65
|
+
assert.equal(opaque.totals.textNodes, 0);
|
|
66
|
+
assert.equal(opaque.totals.opaqueInstances, 1);
|
|
67
|
+
assert.equal(opaque.complete, false);
|
|
68
|
+
});
|
|
69
|
+
|
|
56
70
|
test('equivalent CSS syntaxes preserve alpha in reads and writes', () => {
|
|
57
71
|
const r = runtime();
|
|
58
72
|
for (const color of ['rgba(255, 0, 0, .5)', 'rgb(100% 0% 0% / 50%)', 'hsl(0 100% 50% / .5)', 'hsla(0, 100%, 50%, .5)']) {
|
|
@@ -204,8 +218,8 @@ test('agent setup detects installed clients and preserves existing JSONC and TOM
|
|
|
204
218
|
configureAgent(agent);
|
|
205
219
|
const config = parseJsonc(readFileSync(agent.configPath, 'utf8')).mcpServers['figma-rust-mcp'];
|
|
206
220
|
assert.ok(config);
|
|
207
|
-
if (id === 'antigravity') assert.equal(config.serverUrl, 'http://127.0.0.1:
|
|
208
|
-
else assert.equal(config.url, 'http://127.0.0.1:
|
|
221
|
+
if (id === 'antigravity') assert.equal(config.serverUrl, 'http://127.0.0.1:41730/sse');
|
|
222
|
+
else assert.equal(config.url, 'http://127.0.0.1:41730/mcp');
|
|
209
223
|
}
|
|
210
224
|
|
|
211
225
|
const cursor = agents.find(agent => agent.id === 'cursor');
|
|
@@ -215,21 +229,21 @@ test('agent setup detects installed clients and preserves existing JSONC and TOM
|
|
|
215
229
|
const cursorConfig = parseJsonc(cursorText);
|
|
216
230
|
assert.match(cursorText, /keep this note/);
|
|
217
231
|
assert.equal(cursorConfig.mcpServers.other.url, 'http://example.test');
|
|
218
|
-
assert.equal(cursorConfig.mcpServers['figma-rust-mcp'].url, 'http://127.0.0.1:
|
|
232
|
+
assert.equal(cursorConfig.mcpServers['figma-rust-mcp'].url, 'http://127.0.0.1:41730/mcp');
|
|
219
233
|
assert.equal(configureAgent(cursor), false);
|
|
220
234
|
|
|
221
235
|
const zed = agents.find(agent => agent.id === 'zed');
|
|
222
236
|
writeFileSync(zed.configPath, '{\n "theme": "One Dark",\n}\n');
|
|
223
237
|
configureAgent(zed);
|
|
224
238
|
assert.equal(parseJsonc(readFileSync(zed.configPath, 'utf8')).theme, 'One Dark');
|
|
225
|
-
assert.equal(parseJsonc(readFileSync(zed.configPath, 'utf8')).context_servers['figma-rust-mcp'].url, 'http://127.0.0.1:
|
|
239
|
+
assert.equal(parseJsonc(readFileSync(zed.configPath, 'utf8')).context_servers['figma-rust-mcp'].url, 'http://127.0.0.1:41730/mcp');
|
|
226
240
|
|
|
227
241
|
const codex = agents.find(agent => agent.id === 'codex');
|
|
228
242
|
writeFileSync(codex.configPath, '[mcp_servers.other]\nurl = "http://other.test"\n');
|
|
229
243
|
configureAgent(codex);
|
|
230
244
|
const codexText = readFileSync(codex.configPath, 'utf8');
|
|
231
245
|
assert.match(codexText, /\[mcp_servers\.other\]/);
|
|
232
|
-
assert.match(codexText, /\[mcp_servers\.figma-rust-mcp\]\nurl = "http:\/\/127\.0\.0\.1:
|
|
246
|
+
assert.match(codexText, /\[mcp_servers\.figma-rust-mcp\]\nurl = "http:\/\/127\.0\.0\.1:41730\/mcp"/);
|
|
233
247
|
assert.equal(configureAgent(codex), false);
|
|
234
248
|
} finally {
|
|
235
249
|
rmSync(home, { recursive: true, force: true });
|
|
@@ -525,10 +539,10 @@ test('old UI socket callbacks cannot clear or dispatch through a replacement soc
|
|
|
525
539
|
close() {} send() {}
|
|
526
540
|
}
|
|
527
541
|
const r = vm.createContext({
|
|
528
|
-
ws: null, wsConnected: false, sessionId: null, fileName: 'File', documentId: 'doc', BRIDGE: 'http://localhost:
|
|
542
|
+
ws: null, wsConnected: false, sessionId: null, fileName: 'File', documentId: 'doc', BRIDGE: 'http://localhost:41730',
|
|
529
543
|
WebSocket: Socket, setInterval: () => 1, clearInterval: () => {}, setTimeout: () => 1, clearTimeout: () => {},
|
|
530
544
|
sendRuntimeCapabilities: () => {}, consecutiveErrors: 0, everConnected: false, retryTimer: null, polling: true,
|
|
531
|
-
setStatus: () => {}, log: () => {}, currentPort:
|
|
545
|
+
setStatus: () => {}, log: () => {}, currentPort: 41730, READ_OPS: [],
|
|
532
546
|
startLongPoll: () => assert.fail('stale socket started poll'), dispatchToMain: req => dispatched.push(req),
|
|
533
547
|
});
|
|
534
548
|
vm.runInContext(source, r);
|