figma-rust-mcp 2.8.2
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/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/figma-mcp.js +445 -0
- package/package.json +60 -0
- package/plugin/code.js +5257 -0
- package/plugin/icon16.png +0 -0
- package/plugin/icon32.png +0 -0
- package/plugin/manifest.json +24 -0
- package/plugin/ui.html +998 -0
- package/scripts/build-plugin.js +34 -0
- package/scripts/test-rust-mcp.sh +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BuiHung1612
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# figma-mcp
|
|
2
|
+
|
|
3
|
+
High-performance, bidirectional Model Context Protocol (MCP) server written in **Rust** connecting AI coding assistants directly to **Figma Desktop**.
|
|
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## β‘ Highlights
|
|
10
|
+
|
|
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).
|
|
15
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ποΈ Architecture
|
|
21
|
+
|
|
22
|
+
```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
|
+
βββββββββββββββββββββ
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## π Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. One-Line Launch with NPX (No Rust toolchain needed)
|
|
45
|
+
|
|
46
|
+
You can launch `figma-rust-mcp` on any Mac, Linux, or Windows machine with zero setup:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx figma-rust-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This automatically downloads the prebuilt native binary for your architecture and starts the server.
|
|
53
|
+
|
|
54
|
+
Or if configuring in **Cursor / Claude Desktop / Windsurf / Antigravity MCP Settings**:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"figma-rust-mcp": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "figma-rust-mcp", "--stdio"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 2. Or Build from Source (Rust)
|
|
70
|
+
|
|
71
|
+
Prerequisites: [Rust toolchain](https://rustup.rs/) (`cargo >= 1.80`).
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/BuiHung1612/figma-mcp.git
|
|
75
|
+
cd figma-mcp
|
|
76
|
+
cargo build --release
|
|
77
|
+
./target/release/figma-mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 3. Install the Figma Plugin
|
|
83
|
+
|
|
84
|
+
1. Open **Figma Desktop**.
|
|
85
|
+
2. Go to **Plugins** β **Development** β **Import plugin from manifest...**
|
|
86
|
+
3. Select the file: `plugin/manifest.json` from this repository.
|
|
87
|
+
4. Run the plugin (**Plugins** β **Development** β **Figma MCP Bridge**).
|
|
88
|
+
5. The plugin UI will show a green dot (**Connected**).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### 4. Configure Your MCP Client
|
|
93
|
+
|
|
94
|
+
#### Google Antigravity (SSE Transport - Recommended)
|
|
95
|
+
Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.json`:
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"figma-mcp": {
|
|
100
|
+
"serverUrl": "http://127.0.0.1:38451/sse"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Claude Code / Cursor / Windsurf / VS Code
|
|
107
|
+
You can connect via SSE or stdio:
|
|
108
|
+
|
|
109
|
+
**Option A: SSE Transport (Recommended)**
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"figma-mcp": {
|
|
114
|
+
"url": "http://127.0.0.1:38451/sse"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Option B: Stdio Subprocess (via NPX)**
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"figma-rust-mcp": {
|
|
125
|
+
"command": "npx",
|
|
126
|
+
"args": ["-y", "figma-rust-mcp", "--stdio"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## π οΈ MCP Tools Reference
|
|
135
|
+
|
|
136
|
+
`figma-mcp` exposes 7 first-class MCP tools:
|
|
137
|
+
|
|
138
|
+
### `figma_get_selection`
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
### `figma_inspect_node`
|
|
142
|
+
Inspects a specific node by ID or name, returning complete design context, CSS, auto-layout rules, and typography.
|
|
143
|
+
|
|
144
|
+
### `figma_export_asset`
|
|
145
|
+
Exports any Figma node/layer directly into PNG, JPG, or SVG.
|
|
146
|
+
- `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!
|
|
147
|
+
|
|
148
|
+
### `figma_status`
|
|
149
|
+
Checks live bridge connection status, connected Figma tabs, active file name, and latency statistics.
|
|
150
|
+
|
|
151
|
+
### `figma_write`
|
|
152
|
+
Executes JavaScript draw commands inside the sandboxed VM to build or modify designs on the Figma canvas.
|
|
153
|
+
- Supports Auto-layout (`layoutMode`, `itemSpacing`, `padding`), typography, fills, strokes, corner radius, drop shadows, and component creation.
|
|
154
|
+
- Supports icon loading: `figma.loadIcon("ionicons", "heart", { size: 24, fill: "#ff4757" })`.
|
|
155
|
+
- Supports image insertion: `figma.loadImage(url, { width: 300, height: 200 })`.
|
|
156
|
+
|
|
157
|
+
### `figma_read`
|
|
158
|
+
Universal reader for advanced queries: `get_design`, `scan_design`, `screenshot`, `export_svg`, `get_variables`, `get_styles`, `search_nodes`. Supports `outputPath` for file exports.
|
|
159
|
+
|
|
160
|
+
### `figma_docs`
|
|
161
|
+
Fetches built-in documentation, design rules, layout guidelines, and code examples for `figma_write`.
|
|
162
|
+
- `section`: `"all"` | `"rules"` | `"layout"` | `"api"` | `"tokens"` | `"icons"`
|
|
163
|
+
|
|
164
|
+
### `figma_rules`
|
|
165
|
+
Audits the current Figma document and generates consistency rules and token definitions to guide AI generation.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## π» Development & Testing
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Run server directly in debug mode
|
|
173
|
+
cargo run
|
|
174
|
+
|
|
175
|
+
# Build release binary
|
|
176
|
+
cargo build --release
|
|
177
|
+
|
|
178
|
+
# Run automated MCP test suite
|
|
179
|
+
./scripts/test-rust-mcp.sh
|
|
180
|
+
|
|
181
|
+
# Rebuild Figma plugin bundle (if plugin-src/ is modified)
|
|
182
|
+
node scripts/build-plugin.js
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## π License
|
|
188
|
+
|
|
189
|
+
MIT Β© [BuiHung1612](https://github.com/BuiHung1612) β Free to use, modify, and distribute. See [LICENSE](LICENSE) for details.
|
package/bin/figma-mcp.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Figma MCP - NPX Binary Runner
|
|
5
|
+
* Automatically detects platform/arch, downloads prebuilt Rust binary from GitHub Releases,
|
|
6
|
+
* caches it locally, and executes it transparently.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn, execSync } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import https from 'node:https';
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// Read package.json version
|
|
21
|
+
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
22
|
+
let pkgVersion = '2.8.2';
|
|
23
|
+
let repoOwner = 'BuiHung1612';
|
|
24
|
+
let repoName = 'figma-mcp';
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(pkgPath)) {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
29
|
+
if (pkg.version) pkgVersion = pkg.version;
|
|
30
|
+
if (pkg.repository && typeof pkg.repository.url === 'string') {
|
|
31
|
+
const m = pkg.repository.url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
32
|
+
if (m) {
|
|
33
|
+
repoOwner = m[1];
|
|
34
|
+
repoName = m[2];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {}
|
|
39
|
+
|
|
40
|
+
function getPlatformArch() {
|
|
41
|
+
const platform = process.platform;
|
|
42
|
+
const arch = process.arch;
|
|
43
|
+
|
|
44
|
+
let osName = '';
|
|
45
|
+
let archName = '';
|
|
46
|
+
let ext = 'tar.gz';
|
|
47
|
+
let binName = 'figma-mcp';
|
|
48
|
+
|
|
49
|
+
if (platform === 'darwin') {
|
|
50
|
+
osName = 'macos';
|
|
51
|
+
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
52
|
+
} else if (platform === 'linux') {
|
|
53
|
+
osName = 'linux';
|
|
54
|
+
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
55
|
+
} else if (platform === 'win32') {
|
|
56
|
+
osName = 'windows';
|
|
57
|
+
archName = 'x86_64';
|
|
58
|
+
ext = 'zip';
|
|
59
|
+
binName = 'figma-mcp.exe';
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Unsupported operating system: ${platform}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const assetName = `figma-mcp-${osName}-${archName}.${ext}`;
|
|
65
|
+
return { osName, archName, ext, binName, assetName };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getCacheDir(version) {
|
|
69
|
+
const platform = process.platform;
|
|
70
|
+
let baseCache = '';
|
|
71
|
+
|
|
72
|
+
if (platform === 'win32') {
|
|
73
|
+
baseCache = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
74
|
+
} else if (platform === 'darwin') {
|
|
75
|
+
baseCache = path.join(os.homedir(), 'Library', 'Caches');
|
|
76
|
+
} else {
|
|
77
|
+
baseCache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return path.join(baseCache, 'figma-mcp', `v${version}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function downloadFile(url, destPath) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const followRedirects = (currentUrl, redirectCount = 0) => {
|
|
86
|
+
if (redirectCount > 10) {
|
|
87
|
+
reject(new Error('Too many redirects while downloading binary'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const client = currentUrl.startsWith('https') ? https : http;
|
|
92
|
+
client.get(currentUrl, { headers: { 'User-Agent': 'figma-mcp-npx' } }, (res) => {
|
|
93
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
94
|
+
followRedirects(res.headers.location, redirectCount + 1);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (res.statusCode !== 200) {
|
|
99
|
+
reject(new Error(`Failed to download binary: HTTP ${res.statusCode} from ${currentUrl}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
104
|
+
res.pipe(fileStream);
|
|
105
|
+
|
|
106
|
+
fileStream.on('finish', () => {
|
|
107
|
+
fileStream.close(() => resolve());
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
fileStream.on('error', (err) => {
|
|
111
|
+
fs.unlink(destPath, () => {});
|
|
112
|
+
reject(err);
|
|
113
|
+
});
|
|
114
|
+
}).on('error', (err) => {
|
|
115
|
+
reject(err);
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
followRedirects(url);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractArchive(archivePath, targetDir, ext) {
|
|
124
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
125
|
+
|
|
126
|
+
if (ext === 'zip') {
|
|
127
|
+
try {
|
|
128
|
+
execSync(`tar -xf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
129
|
+
return;
|
|
130
|
+
} catch (_) {
|
|
131
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${targetDir}' -Force"`, { stdio: 'pipe' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function findOrDownloadBinary() {
|
|
140
|
+
if (process.env.FIGMA_MCP_BIN && fs.existsSync(process.env.FIGMA_MCP_BIN)) {
|
|
141
|
+
return process.env.FIGMA_MCP_BIN;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const localRelease = path.resolve(__dirname, '../target/release/figma-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
145
|
+
const localDebug = path.resolve(__dirname, '../target/debug/figma-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
146
|
+
|
|
147
|
+
if (fs.existsSync(localRelease)) return localRelease;
|
|
148
|
+
if (fs.existsSync(localDebug)) return localDebug;
|
|
149
|
+
|
|
150
|
+
const info = getPlatformArch();
|
|
151
|
+
const cacheDir = getCacheDir(pkgVersion);
|
|
152
|
+
const cachedBin = path.join(cacheDir, info.binName);
|
|
153
|
+
|
|
154
|
+
if (fs.existsSync(cachedBin)) {
|
|
155
|
+
if (process.platform !== 'win32') {
|
|
156
|
+
try { fs.chmodSync(cachedBin, 0o755); } catch (_) {}
|
|
157
|
+
}
|
|
158
|
+
return cachedBin;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
162
|
+
const archivePath = path.join(cacheDir, info.assetName);
|
|
163
|
+
|
|
164
|
+
const releaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/download/v${pkgVersion}/${info.assetName}`;
|
|
165
|
+
const latestUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest/download/${info.assetName}`;
|
|
166
|
+
|
|
167
|
+
process.stderr.write(`\x1b[36m[figma-mcp]\x1b[0m Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await downloadFile(releaseUrl, archivePath);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
process.stderr.write(`\x1b[33m[figma-mcp]\x1b[0m Release v${pkgVersion} not found, trying latest release...\n`);
|
|
173
|
+
try {
|
|
174
|
+
await downloadFile(latestUrl, archivePath);
|
|
175
|
+
} catch (fallbackErr) {
|
|
176
|
+
throw new Error(`Failed to download figma-mcp prebuilt binary:\n - ${err.message}\n - ${fallbackErr.message}\n\nPlease verify your internet connection or build from source: cargo build --release`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
extractArchive(archivePath, cacheDir, info.ext);
|
|
182
|
+
} finally {
|
|
183
|
+
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(cachedBin)) {
|
|
187
|
+
const files = fs.readdirSync(cacheDir, { recursive: true });
|
|
188
|
+
const found = files.find(f => path.basename(f) === info.binName);
|
|
189
|
+
if (found) {
|
|
190
|
+
const foundPath = path.join(cacheDir, found);
|
|
191
|
+
fs.renameSync(foundPath, cachedBin);
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`Binary ${info.binName} not found inside downloaded archive`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (process.platform !== 'win32') {
|
|
198
|
+
fs.chmodSync(cachedBin, 0o755);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
process.stderr.write(`\x1b[32m[figma-mcp]\x1b[0m Ready! (cached in ${cacheDir})\n\n`);
|
|
202
|
+
return cachedBin;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// βββ SERVICE MANAGEMENT βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Install figma-mcp as a background service that auto-starts on login.
|
|
209
|
+
* - macOS: LaunchAgent plist (~/.config/figma-mcp/launchd.plist β ~/Library/LaunchAgents/)
|
|
210
|
+
* - Linux: systemd user service (~/.config/systemd/user/figma-mcp.service)
|
|
211
|
+
* - Windows: Task Scheduler XML via schtasks
|
|
212
|
+
*/
|
|
213
|
+
async function installService(binPath) {
|
|
214
|
+
const platform = process.platform;
|
|
215
|
+
const nodeExec = process.execPath;
|
|
216
|
+
const scriptPath = path.resolve(__filename);
|
|
217
|
+
|
|
218
|
+
if (platform === 'darwin') {
|
|
219
|
+
const plistLabel = 'io.github.figma-mcp.server';
|
|
220
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
221
|
+
const plistPath = path.join(plistDir, `${plistLabel}.plist`);
|
|
222
|
+
|
|
223
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
224
|
+
|
|
225
|
+
const logDir = path.join(os.homedir(), 'Library', 'Logs', 'figma-mcp');
|
|
226
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
229
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
230
|
+
<plist version="1.0">
|
|
231
|
+
<dict>
|
|
232
|
+
<key>Label</key>
|
|
233
|
+
<string>${plistLabel}</string>
|
|
234
|
+
<key>ProgramArguments</key>
|
|
235
|
+
<array>
|
|
236
|
+
<string>${binPath}</string>
|
|
237
|
+
</array>
|
|
238
|
+
<key>RunAtLoad</key>
|
|
239
|
+
<true/>
|
|
240
|
+
<key>KeepAlive</key>
|
|
241
|
+
<true/>
|
|
242
|
+
<key>StandardOutPath</key>
|
|
243
|
+
<string>${path.join(logDir, 'stdout.log')}</string>
|
|
244
|
+
<key>StandardErrorPath</key>
|
|
245
|
+
<string>${path.join(logDir, 'stderr.log')}</string>
|
|
246
|
+
<key>ThrottleInterval</key>
|
|
247
|
+
<integer>5</integer>
|
|
248
|
+
</dict>
|
|
249
|
+
</plist>`;
|
|
250
|
+
|
|
251
|
+
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
252
|
+
|
|
253
|
+
// Unload first (ignore error if not loaded)
|
|
254
|
+
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
255
|
+
execSync(`launchctl load -w "${plistPath}"`, { stdio: 'inherit' });
|
|
256
|
+
|
|
257
|
+
console.log(`\x1b[32mβ figma-mcp service installed!\x1b[0m`);
|
|
258
|
+
console.log(` Plist: ${plistPath}`);
|
|
259
|
+
console.log(` Logs: ${logDir}/stdout.log`);
|
|
260
|
+
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
261
|
+
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
262
|
+
|
|
263
|
+
} else if (platform === 'linux') {
|
|
264
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
265
|
+
const servicePath = path.join(serviceDir, 'figma-mcp.service');
|
|
266
|
+
|
|
267
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
268
|
+
|
|
269
|
+
const unit = `[Unit]
|
|
270
|
+
Description=Figma MCP Bridge Server
|
|
271
|
+
After=network.target
|
|
272
|
+
|
|
273
|
+
[Service]
|
|
274
|
+
Type=simple
|
|
275
|
+
ExecStart=${binPath}
|
|
276
|
+
Restart=on-failure
|
|
277
|
+
RestartSec=5
|
|
278
|
+
|
|
279
|
+
[Install]
|
|
280
|
+
WantedBy=default.target
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
fs.writeFileSync(servicePath, unit, 'utf8');
|
|
284
|
+
execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' });
|
|
285
|
+
execSync(`systemctl --user enable --now figma-mcp`, { stdio: 'inherit' });
|
|
286
|
+
|
|
287
|
+
console.log(`\x1b[32mβ figma-mcp systemd user service installed!\x1b[0m`);
|
|
288
|
+
console.log(` Service: ${servicePath}`);
|
|
289
|
+
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
290
|
+
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
291
|
+
|
|
292
|
+
} else if (platform === 'win32') {
|
|
293
|
+
const taskName = 'FigmaMCPServer';
|
|
294
|
+
// Use schtasks to create a task that runs at login
|
|
295
|
+
const cmd = `schtasks /Create /F /TN "${taskName}" /TR "${binPath}" /SC ONLOGON /RL HIGHEST`;
|
|
296
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
297
|
+
|
|
298
|
+
// Start it immediately too
|
|
299
|
+
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
|
300
|
+
|
|
301
|
+
console.log(`\x1b[32mβ figma-mcp Windows Task installed!\x1b[0m`);
|
|
302
|
+
console.log(` Task: ${taskName}`);
|
|
303
|
+
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
304
|
+
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
305
|
+
|
|
306
|
+
} else {
|
|
307
|
+
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Uninstall the background service.
|
|
314
|
+
*/
|
|
315
|
+
async function uninstallService() {
|
|
316
|
+
const platform = process.platform;
|
|
317
|
+
|
|
318
|
+
if (platform === 'darwin') {
|
|
319
|
+
const plistLabel = 'io.github.figma-mcp.server';
|
|
320
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${plistLabel}.plist`);
|
|
321
|
+
|
|
322
|
+
if (!fs.existsSync(plistPath)) {
|
|
323
|
+
console.log(`\x1b[33mService not found at ${plistPath}\x1b[0m`);
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try { execSync(`launchctl unload -w "${plistPath}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
328
|
+
fs.unlinkSync(plistPath);
|
|
329
|
+
|
|
330
|
+
console.log(`\x1b[32mβ figma-mcp service uninstalled.\x1b[0m`);
|
|
331
|
+
|
|
332
|
+
} else if (platform === 'linux') {
|
|
333
|
+
try { execSync(`systemctl --user disable --now figma-mcp`, { stdio: 'inherit' }); } catch (_) {}
|
|
334
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
335
|
+
if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
|
|
336
|
+
try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
|
|
337
|
+
|
|
338
|
+
console.log(`\x1b[32mβ figma-mcp service uninstalled.\x1b[0m`);
|
|
339
|
+
|
|
340
|
+
} else if (platform === 'win32') {
|
|
341
|
+
const taskName = 'FigmaMCPServer';
|
|
342
|
+
try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
343
|
+
|
|
344
|
+
console.log(`\x1b[32mβ figma-mcp Windows Task removed.\x1b[0m`);
|
|
345
|
+
|
|
346
|
+
} else {
|
|
347
|
+
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check if the service is currently installed.
|
|
354
|
+
*/
|
|
355
|
+
function isServiceInstalled() {
|
|
356
|
+
const platform = process.platform;
|
|
357
|
+
if (platform === 'darwin') {
|
|
358
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-mcp.server.plist');
|
|
359
|
+
return fs.existsSync(plistPath);
|
|
360
|
+
} else if (platform === 'linux') {
|
|
361
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
362
|
+
return fs.existsSync(servicePath);
|
|
363
|
+
} else if (platform === 'win32') {
|
|
364
|
+
try {
|
|
365
|
+
execSync('schtasks /Query /TN "FigmaMCPServer"', { stdio: 'pipe' });
|
|
366
|
+
return true;
|
|
367
|
+
} catch (_) { return false; }
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// βββ MAIN βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
373
|
+
|
|
374
|
+
async function main() {
|
|
375
|
+
const args = process.argv.slice(2);
|
|
376
|
+
|
|
377
|
+
if (args.includes('--install-service')) {
|
|
378
|
+
try {
|
|
379
|
+
const binPath = await findOrDownloadBinary();
|
|
380
|
+
await installService(binPath);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (args.includes('--uninstall-service')) {
|
|
389
|
+
try {
|
|
390
|
+
await uninstallService();
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (args.includes('--service-status')) {
|
|
399
|
+
const installed = isServiceInstalled();
|
|
400
|
+
if (installed) {
|
|
401
|
+
console.log('\x1b[32mβ figma-mcp service is installed.\x1b[0m');
|
|
402
|
+
console.log(' To uninstall: npx figma-mcp --uninstall-service');
|
|
403
|
+
} else {
|
|
404
|
+
console.log('\x1b[33mβ figma-mcp service is NOT installed.\x1b[0m');
|
|
405
|
+
console.log(' To install: npx figma-mcp --install-service');
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const binPath = await findOrDownloadBinary();
|
|
412
|
+
|
|
413
|
+
const child = spawn(binPath, args, {
|
|
414
|
+
stdio: 'inherit',
|
|
415
|
+
env: process.env,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
child.on('error', (err) => {
|
|
419
|
+
console.error(`[figma-mcp] Failed to start binary: ${err.message}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
child.on('exit', (code, signal) => {
|
|
424
|
+
if (signal) {
|
|
425
|
+
process.kill(process.pid, signal);
|
|
426
|
+
} else {
|
|
427
|
+
process.exit(code ?? 0);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const forwardSignal = (sig) => {
|
|
432
|
+
if (child && !child.killed) {
|
|
433
|
+
child.kill(sig);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
process.on('SIGINT', () => forwardSignal('SIGINT'));
|
|
438
|
+
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
main();
|