figma-rust-mcp 3.2.9 → 3.2.12
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 -21
- package/README.md +409 -403
- package/bin/figma-mcp.js +591 -591
- package/package.json +62 -62
- package/plugin/code.js +52 -52
- package/plugin/manifest.json +25 -25
- package/plugin/ui.html +211 -211
- package/scripts/build-plugin.js +52 -52
- package/scripts/test-bridge-sessions.mjs +155 -155
- package/scripts/test-plugin-tokens.mjs +429 -350
- package/scripts/test-rust-mcp.sh +38 -38
package/bin/figma-mcp.js
CHANGED
|
@@ -1,591 +1,591 @@
|
|
|
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, execFileSync } 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 = null;
|
|
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
|
-
if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
|
|
40
|
-
|
|
41
|
-
function getPlatformArch() {
|
|
42
|
-
const platform = process.platform;
|
|
43
|
-
const arch = process.arch;
|
|
44
|
-
|
|
45
|
-
let osName = '';
|
|
46
|
-
let archName = '';
|
|
47
|
-
let ext = 'tar.gz';
|
|
48
|
-
let binName = 'figma-mcp';
|
|
49
|
-
|
|
50
|
-
if (platform === 'darwin') {
|
|
51
|
-
osName = 'macos';
|
|
52
|
-
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
53
|
-
} else if (platform === 'linux') {
|
|
54
|
-
osName = 'linux';
|
|
55
|
-
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
56
|
-
} else if (platform === 'win32') {
|
|
57
|
-
osName = 'windows';
|
|
58
|
-
archName = 'x86_64';
|
|
59
|
-
ext = 'zip';
|
|
60
|
-
binName = 'figma-mcp.exe';
|
|
61
|
-
} else {
|
|
62
|
-
throw new Error(`Unsupported operating system: ${platform}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const assetName = `figma-mcp-${osName}-${archName}.${ext}`;
|
|
66
|
-
return { osName, archName, ext, binName, assetName };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getCacheDir(version) {
|
|
70
|
-
const platform = process.platform;
|
|
71
|
-
let baseCache = '';
|
|
72
|
-
|
|
73
|
-
if (platform === 'win32') {
|
|
74
|
-
baseCache = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
75
|
-
} else if (platform === 'darwin') {
|
|
76
|
-
baseCache = path.join(os.homedir(), 'Library', 'Caches');
|
|
77
|
-
} else {
|
|
78
|
-
baseCache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return path.join(baseCache, 'figma-mcp', `v${version}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function downloadFile(url, destPath) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
const followRedirects = (currentUrl, redirectCount = 0) => {
|
|
87
|
-
if (redirectCount > 10) {
|
|
88
|
-
reject(new Error('Too many redirects while downloading binary'));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const client = currentUrl.startsWith('https') ? https : http;
|
|
93
|
-
client.get(currentUrl, { headers: { 'User-Agent': 'figma-mcp-npx' } }, (res) => {
|
|
94
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
95
|
-
followRedirects(res.headers.location, redirectCount + 1);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (res.statusCode !== 200) {
|
|
100
|
-
reject(new Error(`Failed to download binary: HTTP ${res.statusCode} from ${currentUrl}`));
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const fileStream = fs.createWriteStream(destPath);
|
|
105
|
-
res.pipe(fileStream);
|
|
106
|
-
|
|
107
|
-
fileStream.on('finish', () => {
|
|
108
|
-
fileStream.close(() => resolve());
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
fileStream.on('error', (err) => {
|
|
112
|
-
fs.unlink(destPath, () => {});
|
|
113
|
-
reject(err);
|
|
114
|
-
});
|
|
115
|
-
}).on('error', (err) => {
|
|
116
|
-
reject(err);
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
followRedirects(url);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function extractArchive(archivePath, targetDir, ext) {
|
|
125
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
126
|
-
|
|
127
|
-
if (ext === 'zip') {
|
|
128
|
-
try {
|
|
129
|
-
execSync(`tar -xf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
130
|
-
return;
|
|
131
|
-
} catch (_) {
|
|
132
|
-
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${targetDir}' -Force"`, { stdio: 'pipe' });
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function findOrDownloadBinary() {
|
|
141
|
-
if (process.env.FIGMA_MCP_BIN && fs.existsSync(process.env.FIGMA_MCP_BIN)) {
|
|
142
|
-
return process.env.FIGMA_MCP_BIN;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const localRelease = path.resolve(__dirname, '../target/release/figma-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
146
|
-
const localDebug = path.resolve(__dirname, '../target/debug/figma-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
147
|
-
|
|
148
|
-
if (fs.existsSync(localRelease)) return localRelease;
|
|
149
|
-
if (fs.existsSync(localDebug)) return localDebug;
|
|
150
|
-
|
|
151
|
-
const info = getPlatformArch();
|
|
152
|
-
const cacheDir = getCacheDir(pkgVersion);
|
|
153
|
-
const cachedBin = path.join(cacheDir, info.binName);
|
|
154
|
-
|
|
155
|
-
if (fs.existsSync(cachedBin)) {
|
|
156
|
-
if (process.platform !== 'win32') {
|
|
157
|
-
try { fs.chmodSync(cachedBin, 0o755); } catch (_) {}
|
|
158
|
-
}
|
|
159
|
-
return cachedBin;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
163
|
-
const archivePath = path.join(cacheDir, info.assetName);
|
|
164
|
-
|
|
165
|
-
const releaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/download/v${pkgVersion}/${info.assetName}`;
|
|
166
|
-
const latestUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest/download/${info.assetName}`;
|
|
167
|
-
|
|
168
|
-
process.stderr.write(`\x1b[36m[figma-mcp]\x1b[0m Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
await downloadFile(releaseUrl, archivePath);
|
|
172
|
-
} catch (err) {
|
|
173
|
-
process.stderr.write(`\x1b[33m[figma-mcp]\x1b[0m Release v${pkgVersion} not found, trying latest release...\n`);
|
|
174
|
-
try {
|
|
175
|
-
await downloadFile(latestUrl, archivePath);
|
|
176
|
-
} catch (fallbackErr) {
|
|
177
|
-
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`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
extractArchive(archivePath, cacheDir, info.ext);
|
|
183
|
-
} finally {
|
|
184
|
-
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!fs.existsSync(cachedBin)) {
|
|
188
|
-
const files = fs.readdirSync(cacheDir, { recursive: true });
|
|
189
|
-
const found = files.find(f => path.basename(f) === info.binName);
|
|
190
|
-
if (found) {
|
|
191
|
-
const foundPath = path.join(cacheDir, found);
|
|
192
|
-
fs.renameSync(foundPath, cachedBin);
|
|
193
|
-
} else {
|
|
194
|
-
throw new Error(`Binary ${info.binName} not found inside downloaded archive`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (process.platform !== 'win32') {
|
|
199
|
-
fs.chmodSync(cachedBin, 0o755);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
process.stderr.write(`\x1b[32m[figma-mcp]\x1b[0m Ready! (cached in ${cacheDir})\n\n`);
|
|
203
|
-
return cachedBin;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ─── SERVICE MANAGEMENT ───────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Install figma-mcp as a background service that auto-starts on login.
|
|
210
|
-
* - macOS: LaunchAgent plist (~/.config/figma-mcp/launchd.plist → ~/Library/LaunchAgents/)
|
|
211
|
-
* - Linux: systemd user service (~/.config/systemd/user/figma-mcp.service)
|
|
212
|
-
* - Windows: Task Scheduler XML via schtasks
|
|
213
|
-
*/
|
|
214
|
-
async function installService(binPath) {
|
|
215
|
-
const platform = process.platform;
|
|
216
|
-
const nodeExec = process.execPath;
|
|
217
|
-
const scriptPath = path.resolve(__filename);
|
|
218
|
-
|
|
219
|
-
if (platform === 'darwin') {
|
|
220
|
-
const plistLabel = 'io.github.figma-mcp.server';
|
|
221
|
-
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
222
|
-
const plistPath = path.join(plistDir, `${plistLabel}.plist`);
|
|
223
|
-
|
|
224
|
-
fs.mkdirSync(plistDir, { recursive: true });
|
|
225
|
-
|
|
226
|
-
const logDir = path.join(os.homedir(), 'Library', 'Logs', 'figma-mcp');
|
|
227
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
228
|
-
|
|
229
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
230
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
231
|
-
<plist version="1.0">
|
|
232
|
-
<dict>
|
|
233
|
-
<key>Label</key>
|
|
234
|
-
<string>${plistLabel}</string>
|
|
235
|
-
<key>ProgramArguments</key>
|
|
236
|
-
<array>
|
|
237
|
-
<string>${binPath}</string>
|
|
238
|
-
<string>--server</string>
|
|
239
|
-
</array>
|
|
240
|
-
<key>RunAtLoad</key>
|
|
241
|
-
<true/>
|
|
242
|
-
<key>KeepAlive</key>
|
|
243
|
-
<true/>
|
|
244
|
-
<key>StandardOutPath</key>
|
|
245
|
-
<string>${path.join(logDir, 'stdout.log')}</string>
|
|
246
|
-
<key>StandardErrorPath</key>
|
|
247
|
-
<string>${path.join(logDir, 'stderr.log')}</string>
|
|
248
|
-
<key>ThrottleInterval</key>
|
|
249
|
-
<integer>5</integer>
|
|
250
|
-
</dict>
|
|
251
|
-
</plist>`;
|
|
252
|
-
|
|
253
|
-
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
254
|
-
|
|
255
|
-
// Unload first (ignore error if not loaded)
|
|
256
|
-
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
257
|
-
execSync(`launchctl load -w "${plistPath}"`, { stdio: 'inherit' });
|
|
258
|
-
|
|
259
|
-
console.log(`\x1b[32m✓ figma-mcp service installed!\x1b[0m`);
|
|
260
|
-
console.log(` Plist: ${plistPath}`);
|
|
261
|
-
console.log(` Logs: ${logDir}/stdout.log`);
|
|
262
|
-
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
263
|
-
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
264
|
-
|
|
265
|
-
} else if (platform === 'linux') {
|
|
266
|
-
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
267
|
-
const servicePath = path.join(serviceDir, 'figma-mcp.service');
|
|
268
|
-
|
|
269
|
-
fs.mkdirSync(serviceDir, { recursive: true });
|
|
270
|
-
|
|
271
|
-
const unit = `[Unit]
|
|
272
|
-
Description=Figma MCP Bridge Server
|
|
273
|
-
After=network.target
|
|
274
|
-
|
|
275
|
-
[Service]
|
|
276
|
-
Type=simple
|
|
277
|
-
ExecStart=${binPath} --server
|
|
278
|
-
Restart=on-failure
|
|
279
|
-
RestartSec=5
|
|
280
|
-
|
|
281
|
-
[Install]
|
|
282
|
-
WantedBy=default.target
|
|
283
|
-
`;
|
|
284
|
-
|
|
285
|
-
fs.writeFileSync(servicePath, unit, 'utf8');
|
|
286
|
-
execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' });
|
|
287
|
-
execSync(`systemctl --user enable --now figma-mcp`, { stdio: 'inherit' });
|
|
288
|
-
|
|
289
|
-
console.log(`\x1b[32m✓ figma-mcp systemd user service installed!\x1b[0m`);
|
|
290
|
-
console.log(` Service: ${servicePath}`);
|
|
291
|
-
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
292
|
-
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
293
|
-
|
|
294
|
-
} else if (platform === 'win32') {
|
|
295
|
-
const taskName = 'FigmaMCPServer';
|
|
296
|
-
// Use schtasks to create a task that runs at login
|
|
297
|
-
// The native launcher detaches the server and redirects output to a log file.
|
|
298
|
-
const taskCommand = `"${binPath}" --background`;
|
|
299
|
-
execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
|
|
300
|
-
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'inherit', windowsHide: true });
|
|
301
|
-
|
|
302
|
-
// Start it immediately too
|
|
303
|
-
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
|
304
|
-
|
|
305
|
-
console.log(`\x1b[32m✓ figma-rust-mcp Windows Task installed!\x1b[0m`);
|
|
306
|
-
console.log(` Task: ${taskName}`);
|
|
307
|
-
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
308
|
-
console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
|
|
309
|
-
|
|
310
|
-
} else {
|
|
311
|
-
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Uninstall the background service.
|
|
318
|
-
*/
|
|
319
|
-
async function uninstallService() {
|
|
320
|
-
const platform = process.platform;
|
|
321
|
-
|
|
322
|
-
if (platform === 'darwin') {
|
|
323
|
-
const plistLabel = 'io.github.figma-mcp.server';
|
|
324
|
-
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${plistLabel}.plist`);
|
|
325
|
-
|
|
326
|
-
if (!fs.existsSync(plistPath)) {
|
|
327
|
-
console.log(`\x1b[33mService not found at ${plistPath}\x1b[0m`);
|
|
328
|
-
process.exit(0);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
try { execSync(`launchctl unload -w "${plistPath}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
332
|
-
try { execSync(`pkill -f figma-mcp 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
333
|
-
fs.unlinkSync(plistPath);
|
|
334
|
-
|
|
335
|
-
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
336
|
-
|
|
337
|
-
} else if (platform === 'linux') {
|
|
338
|
-
try { execSync(`systemctl --user disable --now figma-mcp`, { stdio: 'inherit' }); } catch (_) {}
|
|
339
|
-
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
340
|
-
if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
|
|
341
|
-
try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
|
|
342
|
-
|
|
343
|
-
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
344
|
-
|
|
345
|
-
} else if (platform === 'win32') {
|
|
346
|
-
const taskName = 'FigmaMCPServer';
|
|
347
|
-
try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
348
|
-
|
|
349
|
-
console.log(`\x1b[32m✓ figma-rust-mcp Windows Task removed.\x1b[0m`);
|
|
350
|
-
|
|
351
|
-
} else {
|
|
352
|
-
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Check if the service is currently installed.
|
|
359
|
-
*/
|
|
360
|
-
function isServiceInstalled() {
|
|
361
|
-
const platform = process.platform;
|
|
362
|
-
if (platform === 'darwin') {
|
|
363
|
-
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-mcp.server.plist');
|
|
364
|
-
return fs.existsSync(plistPath);
|
|
365
|
-
} else if (platform === 'linux') {
|
|
366
|
-
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
367
|
-
return fs.existsSync(servicePath);
|
|
368
|
-
} else if (platform === 'win32') {
|
|
369
|
-
try {
|
|
370
|
-
execSync('schtasks /Query /TN "FigmaMCPServer"', { stdio: 'pipe' });
|
|
371
|
-
return true;
|
|
372
|
-
} catch (_) { return false; }
|
|
373
|
-
}
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function setupPlugin(customDir) {
|
|
378
|
-
const pluginDir = customDir || path.join(os.homedir(), '.figma-mcp', 'plugin');
|
|
379
|
-
const sourcePluginDir = path.resolve(__dirname, '../plugin');
|
|
380
|
-
|
|
381
|
-
fs.mkdirSync(pluginDir, { recursive: true });
|
|
382
|
-
|
|
383
|
-
const files = ['manifest.json', 'code.js', 'ui.html', 'icon16.png', 'icon32.png'];
|
|
384
|
-
for (const f of files) {
|
|
385
|
-
const src = path.join(sourcePluginDir, f);
|
|
386
|
-
const dest = path.join(pluginDir, f);
|
|
387
|
-
if (fs.existsSync(src)) {
|
|
388
|
-
fs.copyFileSync(src, dest);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const manifestPath = path.join(pluginDir, 'manifest.json');
|
|
393
|
-
console.log(`\n\x1b[32m✓ Figma MCP Dynamic Thin Plugin installed to:\x1b[0m`);
|
|
394
|
-
console.log(` \x1b[36m${manifestPath}\x1b[0m\n`);
|
|
395
|
-
console.log(`\x1b[1mTo connect Figma (Do this ONCE forever):\x1b[0m`);
|
|
396
|
-
console.log(` 1. Open Figma Desktop`);
|
|
397
|
-
console.log(` 2. Go to Plugins → Development → Import plugin from manifest...`);
|
|
398
|
-
console.log(` 3. Select: ${manifestPath}`);
|
|
399
|
-
console.log(` 4. Done! All future updates load dynamically from figma-mcp without file re-imports.\n`);
|
|
400
|
-
return manifestPath;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function isNewer(latest, current) {
|
|
404
|
-
const pLatest = latest.split('.').map(n => parseInt(n, 10) || 0);
|
|
405
|
-
const pCurrent = current.split('.').map(n => parseInt(n, 10) || 0);
|
|
406
|
-
for (let i = 0; i < 3; i++) {
|
|
407
|
-
if ((pLatest[i] || 0) > (pCurrent[i] || 0)) return true;
|
|
408
|
-
if ((pLatest[i] || 0) < (pCurrent[i] || 0)) return false;
|
|
409
|
-
}
|
|
410
|
-
return false;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function checkForUpdatesAsync() {
|
|
414
|
-
const updateStateFile = path.join(os.homedir(), '.figma-mcp', 'update-check.json');
|
|
415
|
-
try {
|
|
416
|
-
if (fs.existsSync(updateStateFile)) {
|
|
417
|
-
const state = JSON.parse(fs.readFileSync(updateStateFile, 'utf8'));
|
|
418
|
-
const now = Date.now();
|
|
419
|
-
if (state.lastChecked && (now - state.lastChecked < 6 * 3600 * 1000)) {
|
|
420
|
-
if (state.latestVersion && isNewer(state.latestVersion, pkgVersion)) {
|
|
421
|
-
process.stderr.write(`\n\x1b[36m⚡ Update available: v${pkgVersion} → v${state.latestVersion} (Run: npx figma-rust-mcp --upgrade)\x1b[0m\n\n`);
|
|
422
|
-
}
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
} catch (_) {}
|
|
427
|
-
|
|
428
|
-
const req = https.get('https://registry.npmjs.org/figma-rust-mcp/latest', {
|
|
429
|
-
headers: { 'User-Agent': 'figma-mcp-updater' },
|
|
430
|
-
timeout: 2000
|
|
431
|
-
}, (res) => {
|
|
432
|
-
let data = '';
|
|
433
|
-
res.on('data', chunk => { data += chunk; });
|
|
434
|
-
res.on('end', () => {
|
|
435
|
-
try {
|
|
436
|
-
if (res.statusCode === 200) {
|
|
437
|
-
const json = JSON.parse(data);
|
|
438
|
-
const latest = json.version;
|
|
439
|
-
fs.mkdirSync(path.join(os.homedir(), '.figma-mcp'), { recursive: true });
|
|
440
|
-
fs.writeFileSync(updateStateFile, JSON.stringify({
|
|
441
|
-
lastChecked: Date.now(),
|
|
442
|
-
latestVersion: latest
|
|
443
|
-
}, null, 2));
|
|
444
|
-
if (latest && isNewer(latest, pkgVersion)) {
|
|
445
|
-
process.stderr.write(`\n\x1b[36m⚡ Update available: v${pkgVersion} → v${latest}\x1b[0m\n\x1b[36m Run: npx figma-rust-mcp --upgrade\x1b[0m\n\n`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
} catch (_) {}
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
req.on('error', () => {});
|
|
452
|
-
req.on('timeout', () => req.destroy());
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async function upgrade() {
|
|
456
|
-
console.log('\x1b[36m[figma-mcp]\x1b[0m Checking for updates and upgrading figma-rust-mcp...');
|
|
457
|
-
try {
|
|
458
|
-
execSync('npm install -g figma-rust-mcp@latest', { stdio: 'inherit' });
|
|
459
|
-
console.log('\x1b[32m✓ Upgraded figma-rust-mcp to latest version.\x1b[0m');
|
|
460
|
-
setupPlugin();
|
|
461
|
-
if (isServiceInstalled()) {
|
|
462
|
-
console.log('\x1b[36mUpdating background service binary...\x1b[0m');
|
|
463
|
-
const binPath = await findOrDownloadBinary();
|
|
464
|
-
await installService(binPath);
|
|
465
|
-
}
|
|
466
|
-
} catch (err) {
|
|
467
|
-
console.log('\x1b[33mTip: Run directly with latest version:\x1b[0m\n npx -y figma-rust-mcp@latest\n');
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function setupAlias() {
|
|
472
|
-
const platform = process.platform;
|
|
473
|
-
if (platform === 'win32') {
|
|
474
|
-
console.log('\x1b[33mOn Windows, install globally for direct command access:\x1b[0m\n npm install -g figma-rust-mcp\n');
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const shellRc = process.env.SHELL && process.env.SHELL.includes('zsh')
|
|
479
|
-
? path.join(os.homedir(), '.zshrc')
|
|
480
|
-
: path.join(os.homedir(), '.bashrc');
|
|
481
|
-
|
|
482
|
-
const aliasLine = "alias figma-mcp='npx figma-rust-mcp'";
|
|
483
|
-
try {
|
|
484
|
-
let content = fs.existsSync(shellRc) ? fs.readFileSync(shellRc, 'utf8') : '';
|
|
485
|
-
if (!content.includes('alias figma-mcp=')) {
|
|
486
|
-
fs.appendFileSync(shellRc, `\n# Figma MCP alias\n${aliasLine}\n`);
|
|
487
|
-
console.log(`\x1b[32m✓ Added alias to ${shellRc}:\x1b[0m`);
|
|
488
|
-
console.log(` ${aliasLine}`);
|
|
489
|
-
console.log(`\nRun \x1b[36msource ${shellRc}\x1b[0m to start using \x1b[1mfigma-mcp\x1b[0m command directly!`);
|
|
490
|
-
} else {
|
|
491
|
-
console.log(`\x1b[32m✓ Alias already exists in ${shellRc}\x1b[0m`);
|
|
492
|
-
}
|
|
493
|
-
} catch (err) {
|
|
494
|
-
console.error(`Failed to configure alias: ${err.message}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// ─── MAIN ─────────────────────────────────────────────────────────────────────
|
|
499
|
-
|
|
500
|
-
async function main() {
|
|
501
|
-
const args = process.argv.slice(2);
|
|
502
|
-
|
|
503
|
-
if (args.includes('--upgrade') || args.includes('--update')) {
|
|
504
|
-
await upgrade();
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (args.includes('--alias')) {
|
|
509
|
-
setupAlias();
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (args.includes('--setup-plugin') || args.includes('--export-plugin') || args.includes('--setup')) {
|
|
514
|
-
const customDir = args.find(a => !a.startsWith('-') && a !== '--setup-plugin' && a !== '--export-plugin' && a !== '--setup');
|
|
515
|
-
setupPlugin(customDir);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (args.includes('--install-service')) {
|
|
520
|
-
try {
|
|
521
|
-
setupPlugin();
|
|
522
|
-
const binPath = await findOrDownloadBinary();
|
|
523
|
-
await installService(binPath);
|
|
524
|
-
} catch (err) {
|
|
525
|
-
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
526
|
-
process.exit(1);
|
|
527
|
-
}
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (args.includes('--uninstall-service')) {
|
|
532
|
-
try {
|
|
533
|
-
await uninstallService();
|
|
534
|
-
} catch (err) {
|
|
535
|
-
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
536
|
-
process.exit(1);
|
|
537
|
-
}
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (args.includes('--service-status')) {
|
|
542
|
-
const installed = isServiceInstalled();
|
|
543
|
-
if (installed) {
|
|
544
|
-
console.log('\x1b[32m✓ figma-rust-mcp service is installed.\x1b[0m');
|
|
545
|
-
console.log(' To uninstall: npx figma-rust-mcp --uninstall-service');
|
|
546
|
-
} else {
|
|
547
|
-
console.log('\x1b[33m✗ figma-rust-mcp service is NOT installed.\x1b[0m');
|
|
548
|
-
console.log(' To install: npx figma-rust-mcp --install-service');
|
|
549
|
-
}
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Non-blocking auto update check
|
|
554
|
-
checkForUpdatesAsync();
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
const binPath = await findOrDownloadBinary();
|
|
558
|
-
|
|
559
|
-
const child = spawn(binPath, args, {
|
|
560
|
-
stdio: 'inherit',
|
|
561
|
-
env: process.env,
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
child.on('error', (err) => {
|
|
565
|
-
console.error(`[figma-mcp] Failed to start binary: ${err.message}`);
|
|
566
|
-
process.exit(1);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
child.on('exit', (code, signal) => {
|
|
570
|
-
if (signal) {
|
|
571
|
-
process.kill(process.pid, signal);
|
|
572
|
-
} else {
|
|
573
|
-
process.exit(code ?? 0);
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const forwardSignal = (sig) => {
|
|
578
|
-
if (child && !child.killed) {
|
|
579
|
-
child.kill(sig);
|
|
580
|
-
}
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
process.on('SIGINT', () => forwardSignal('SIGINT'));
|
|
584
|
-
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
585
|
-
} catch (err) {
|
|
586
|
-
console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
|
|
587
|
-
process.exit(1);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
main();
|
|
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, execFileSync } 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 = null;
|
|
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
|
+
if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
|
|
40
|
+
|
|
41
|
+
function getPlatformArch() {
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
const arch = process.arch;
|
|
44
|
+
|
|
45
|
+
let osName = '';
|
|
46
|
+
let archName = '';
|
|
47
|
+
let ext = 'tar.gz';
|
|
48
|
+
let binName = 'figma-mcp';
|
|
49
|
+
|
|
50
|
+
if (platform === 'darwin') {
|
|
51
|
+
osName = 'macos';
|
|
52
|
+
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
53
|
+
} else if (platform === 'linux') {
|
|
54
|
+
osName = 'linux';
|
|
55
|
+
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
56
|
+
} else if (platform === 'win32') {
|
|
57
|
+
osName = 'windows';
|
|
58
|
+
archName = 'x86_64';
|
|
59
|
+
ext = 'zip';
|
|
60
|
+
binName = 'figma-mcp.exe';
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error(`Unsupported operating system: ${platform}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const assetName = `figma-mcp-${osName}-${archName}.${ext}`;
|
|
66
|
+
return { osName, archName, ext, binName, assetName };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getCacheDir(version) {
|
|
70
|
+
const platform = process.platform;
|
|
71
|
+
let baseCache = '';
|
|
72
|
+
|
|
73
|
+
if (platform === 'win32') {
|
|
74
|
+
baseCache = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
75
|
+
} else if (platform === 'darwin') {
|
|
76
|
+
baseCache = path.join(os.homedir(), 'Library', 'Caches');
|
|
77
|
+
} else {
|
|
78
|
+
baseCache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return path.join(baseCache, 'figma-mcp', `v${version}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function downloadFile(url, destPath) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const followRedirects = (currentUrl, redirectCount = 0) => {
|
|
87
|
+
if (redirectCount > 10) {
|
|
88
|
+
reject(new Error('Too many redirects while downloading binary'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const client = currentUrl.startsWith('https') ? https : http;
|
|
93
|
+
client.get(currentUrl, { headers: { 'User-Agent': 'figma-mcp-npx' } }, (res) => {
|
|
94
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
95
|
+
followRedirects(res.headers.location, redirectCount + 1);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (res.statusCode !== 200) {
|
|
100
|
+
reject(new Error(`Failed to download binary: HTTP ${res.statusCode} from ${currentUrl}`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
105
|
+
res.pipe(fileStream);
|
|
106
|
+
|
|
107
|
+
fileStream.on('finish', () => {
|
|
108
|
+
fileStream.close(() => resolve());
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
fileStream.on('error', (err) => {
|
|
112
|
+
fs.unlink(destPath, () => {});
|
|
113
|
+
reject(err);
|
|
114
|
+
});
|
|
115
|
+
}).on('error', (err) => {
|
|
116
|
+
reject(err);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
followRedirects(url);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractArchive(archivePath, targetDir, ext) {
|
|
125
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
126
|
+
|
|
127
|
+
if (ext === 'zip') {
|
|
128
|
+
try {
|
|
129
|
+
execSync(`tar -xf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
130
|
+
return;
|
|
131
|
+
} catch (_) {
|
|
132
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${targetDir}' -Force"`, { stdio: 'pipe' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function findOrDownloadBinary() {
|
|
141
|
+
if (process.env.FIGMA_MCP_BIN && fs.existsSync(process.env.FIGMA_MCP_BIN)) {
|
|
142
|
+
return process.env.FIGMA_MCP_BIN;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const localRelease = path.resolve(__dirname, '../target/release/figma-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
146
|
+
const localDebug = path.resolve(__dirname, '../target/debug/figma-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
147
|
+
|
|
148
|
+
if (fs.existsSync(localRelease)) return localRelease;
|
|
149
|
+
if (fs.existsSync(localDebug)) return localDebug;
|
|
150
|
+
|
|
151
|
+
const info = getPlatformArch();
|
|
152
|
+
const cacheDir = getCacheDir(pkgVersion);
|
|
153
|
+
const cachedBin = path.join(cacheDir, info.binName);
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(cachedBin)) {
|
|
156
|
+
if (process.platform !== 'win32') {
|
|
157
|
+
try { fs.chmodSync(cachedBin, 0o755); } catch (_) {}
|
|
158
|
+
}
|
|
159
|
+
return cachedBin;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
163
|
+
const archivePath = path.join(cacheDir, info.assetName);
|
|
164
|
+
|
|
165
|
+
const releaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/download/v${pkgVersion}/${info.assetName}`;
|
|
166
|
+
const latestUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest/download/${info.assetName}`;
|
|
167
|
+
|
|
168
|
+
process.stderr.write(`\x1b[36m[figma-mcp]\x1b[0m Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await downloadFile(releaseUrl, archivePath);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
process.stderr.write(`\x1b[33m[figma-mcp]\x1b[0m Release v${pkgVersion} not found, trying latest release...\n`);
|
|
174
|
+
try {
|
|
175
|
+
await downloadFile(latestUrl, archivePath);
|
|
176
|
+
} catch (fallbackErr) {
|
|
177
|
+
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`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
extractArchive(archivePath, cacheDir, info.ext);
|
|
183
|
+
} finally {
|
|
184
|
+
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!fs.existsSync(cachedBin)) {
|
|
188
|
+
const files = fs.readdirSync(cacheDir, { recursive: true });
|
|
189
|
+
const found = files.find(f => path.basename(f) === info.binName);
|
|
190
|
+
if (found) {
|
|
191
|
+
const foundPath = path.join(cacheDir, found);
|
|
192
|
+
fs.renameSync(foundPath, cachedBin);
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error(`Binary ${info.binName} not found inside downloaded archive`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (process.platform !== 'win32') {
|
|
199
|
+
fs.chmodSync(cachedBin, 0o755);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
process.stderr.write(`\x1b[32m[figma-mcp]\x1b[0m Ready! (cached in ${cacheDir})\n\n`);
|
|
203
|
+
return cachedBin;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── SERVICE MANAGEMENT ───────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Install figma-mcp as a background service that auto-starts on login.
|
|
210
|
+
* - macOS: LaunchAgent plist (~/.config/figma-mcp/launchd.plist → ~/Library/LaunchAgents/)
|
|
211
|
+
* - Linux: systemd user service (~/.config/systemd/user/figma-mcp.service)
|
|
212
|
+
* - Windows: Task Scheduler XML via schtasks
|
|
213
|
+
*/
|
|
214
|
+
async function installService(binPath) {
|
|
215
|
+
const platform = process.platform;
|
|
216
|
+
const nodeExec = process.execPath;
|
|
217
|
+
const scriptPath = path.resolve(__filename);
|
|
218
|
+
|
|
219
|
+
if (platform === 'darwin') {
|
|
220
|
+
const plistLabel = 'io.github.figma-mcp.server';
|
|
221
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
222
|
+
const plistPath = path.join(plistDir, `${plistLabel}.plist`);
|
|
223
|
+
|
|
224
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
225
|
+
|
|
226
|
+
const logDir = path.join(os.homedir(), 'Library', 'Logs', 'figma-mcp');
|
|
227
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
228
|
+
|
|
229
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
230
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
231
|
+
<plist version="1.0">
|
|
232
|
+
<dict>
|
|
233
|
+
<key>Label</key>
|
|
234
|
+
<string>${plistLabel}</string>
|
|
235
|
+
<key>ProgramArguments</key>
|
|
236
|
+
<array>
|
|
237
|
+
<string>${binPath}</string>
|
|
238
|
+
<string>--server</string>
|
|
239
|
+
</array>
|
|
240
|
+
<key>RunAtLoad</key>
|
|
241
|
+
<true/>
|
|
242
|
+
<key>KeepAlive</key>
|
|
243
|
+
<true/>
|
|
244
|
+
<key>StandardOutPath</key>
|
|
245
|
+
<string>${path.join(logDir, 'stdout.log')}</string>
|
|
246
|
+
<key>StandardErrorPath</key>
|
|
247
|
+
<string>${path.join(logDir, 'stderr.log')}</string>
|
|
248
|
+
<key>ThrottleInterval</key>
|
|
249
|
+
<integer>5</integer>
|
|
250
|
+
</dict>
|
|
251
|
+
</plist>`;
|
|
252
|
+
|
|
253
|
+
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
254
|
+
|
|
255
|
+
// Unload first (ignore error if not loaded)
|
|
256
|
+
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
257
|
+
execSync(`launchctl load -w "${plistPath}"`, { stdio: 'inherit' });
|
|
258
|
+
|
|
259
|
+
console.log(`\x1b[32m✓ figma-mcp service installed!\x1b[0m`);
|
|
260
|
+
console.log(` Plist: ${plistPath}`);
|
|
261
|
+
console.log(` Logs: ${logDir}/stdout.log`);
|
|
262
|
+
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
263
|
+
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
264
|
+
|
|
265
|
+
} else if (platform === 'linux') {
|
|
266
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
267
|
+
const servicePath = path.join(serviceDir, 'figma-mcp.service');
|
|
268
|
+
|
|
269
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
const unit = `[Unit]
|
|
272
|
+
Description=Figma MCP Bridge Server
|
|
273
|
+
After=network.target
|
|
274
|
+
|
|
275
|
+
[Service]
|
|
276
|
+
Type=simple
|
|
277
|
+
ExecStart=${binPath} --server
|
|
278
|
+
Restart=on-failure
|
|
279
|
+
RestartSec=5
|
|
280
|
+
|
|
281
|
+
[Install]
|
|
282
|
+
WantedBy=default.target
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
fs.writeFileSync(servicePath, unit, 'utf8');
|
|
286
|
+
execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' });
|
|
287
|
+
execSync(`systemctl --user enable --now figma-mcp`, { stdio: 'inherit' });
|
|
288
|
+
|
|
289
|
+
console.log(`\x1b[32m✓ figma-mcp systemd user service installed!\x1b[0m`);
|
|
290
|
+
console.log(` Service: ${servicePath}`);
|
|
291
|
+
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
292
|
+
console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
|
|
293
|
+
|
|
294
|
+
} else if (platform === 'win32') {
|
|
295
|
+
const taskName = 'FigmaMCPServer';
|
|
296
|
+
// Use schtasks to create a task that runs at login
|
|
297
|
+
// The native launcher detaches the server and redirects output to a log file.
|
|
298
|
+
const taskCommand = `"${binPath}" --background`;
|
|
299
|
+
execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
|
|
300
|
+
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'inherit', windowsHide: true });
|
|
301
|
+
|
|
302
|
+
// Start it immediately too
|
|
303
|
+
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
|
304
|
+
|
|
305
|
+
console.log(`\x1b[32m✓ figma-rust-mcp Windows Task installed!\x1b[0m`);
|
|
306
|
+
console.log(` Task: ${taskName}`);
|
|
307
|
+
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
308
|
+
console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
|
|
309
|
+
|
|
310
|
+
} else {
|
|
311
|
+
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Uninstall the background service.
|
|
318
|
+
*/
|
|
319
|
+
async function uninstallService() {
|
|
320
|
+
const platform = process.platform;
|
|
321
|
+
|
|
322
|
+
if (platform === 'darwin') {
|
|
323
|
+
const plistLabel = 'io.github.figma-mcp.server';
|
|
324
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${plistLabel}.plist`);
|
|
325
|
+
|
|
326
|
+
if (!fs.existsSync(plistPath)) {
|
|
327
|
+
console.log(`\x1b[33mService not found at ${plistPath}\x1b[0m`);
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try { execSync(`launchctl unload -w "${plistPath}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
332
|
+
try { execSync(`pkill -f figma-mcp 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
333
|
+
fs.unlinkSync(plistPath);
|
|
334
|
+
|
|
335
|
+
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
336
|
+
|
|
337
|
+
} else if (platform === 'linux') {
|
|
338
|
+
try { execSync(`systemctl --user disable --now figma-mcp`, { stdio: 'inherit' }); } catch (_) {}
|
|
339
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
340
|
+
if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
|
|
341
|
+
try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
|
|
342
|
+
|
|
343
|
+
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
344
|
+
|
|
345
|
+
} else if (platform === 'win32') {
|
|
346
|
+
const taskName = 'FigmaMCPServer';
|
|
347
|
+
try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
348
|
+
|
|
349
|
+
console.log(`\x1b[32m✓ figma-rust-mcp Windows Task removed.\x1b[0m`);
|
|
350
|
+
|
|
351
|
+
} else {
|
|
352
|
+
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Check if the service is currently installed.
|
|
359
|
+
*/
|
|
360
|
+
function isServiceInstalled() {
|
|
361
|
+
const platform = process.platform;
|
|
362
|
+
if (platform === 'darwin') {
|
|
363
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-mcp.server.plist');
|
|
364
|
+
return fs.existsSync(plistPath);
|
|
365
|
+
} else if (platform === 'linux') {
|
|
366
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
367
|
+
return fs.existsSync(servicePath);
|
|
368
|
+
} else if (platform === 'win32') {
|
|
369
|
+
try {
|
|
370
|
+
execSync('schtasks /Query /TN "FigmaMCPServer"', { stdio: 'pipe' });
|
|
371
|
+
return true;
|
|
372
|
+
} catch (_) { return false; }
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function setupPlugin(customDir) {
|
|
378
|
+
const pluginDir = customDir || path.join(os.homedir(), '.figma-mcp', 'plugin');
|
|
379
|
+
const sourcePluginDir = path.resolve(__dirname, '../plugin');
|
|
380
|
+
|
|
381
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
382
|
+
|
|
383
|
+
const files = ['manifest.json', 'code.js', 'ui.html', 'icon16.png', 'icon32.png'];
|
|
384
|
+
for (const f of files) {
|
|
385
|
+
const src = path.join(sourcePluginDir, f);
|
|
386
|
+
const dest = path.join(pluginDir, f);
|
|
387
|
+
if (fs.existsSync(src)) {
|
|
388
|
+
fs.copyFileSync(src, dest);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const manifestPath = path.join(pluginDir, 'manifest.json');
|
|
393
|
+
console.log(`\n\x1b[32m✓ Figma MCP Dynamic Thin Plugin installed to:\x1b[0m`);
|
|
394
|
+
console.log(` \x1b[36m${manifestPath}\x1b[0m\n`);
|
|
395
|
+
console.log(`\x1b[1mTo connect Figma (Do this ONCE forever):\x1b[0m`);
|
|
396
|
+
console.log(` 1. Open Figma Desktop`);
|
|
397
|
+
console.log(` 2. Go to Plugins → Development → Import plugin from manifest...`);
|
|
398
|
+
console.log(` 3. Select: ${manifestPath}`);
|
|
399
|
+
console.log(` 4. Done! All future updates load dynamically from figma-mcp without file re-imports.\n`);
|
|
400
|
+
return manifestPath;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isNewer(latest, current) {
|
|
404
|
+
const pLatest = latest.split('.').map(n => parseInt(n, 10) || 0);
|
|
405
|
+
const pCurrent = current.split('.').map(n => parseInt(n, 10) || 0);
|
|
406
|
+
for (let i = 0; i < 3; i++) {
|
|
407
|
+
if ((pLatest[i] || 0) > (pCurrent[i] || 0)) return true;
|
|
408
|
+
if ((pLatest[i] || 0) < (pCurrent[i] || 0)) return false;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function checkForUpdatesAsync() {
|
|
414
|
+
const updateStateFile = path.join(os.homedir(), '.figma-mcp', 'update-check.json');
|
|
415
|
+
try {
|
|
416
|
+
if (fs.existsSync(updateStateFile)) {
|
|
417
|
+
const state = JSON.parse(fs.readFileSync(updateStateFile, 'utf8'));
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
if (state.lastChecked && (now - state.lastChecked < 6 * 3600 * 1000)) {
|
|
420
|
+
if (state.latestVersion && isNewer(state.latestVersion, pkgVersion)) {
|
|
421
|
+
process.stderr.write(`\n\x1b[36m⚡ Update available: v${pkgVersion} → v${state.latestVersion} (Run: npx figma-rust-mcp --upgrade)\x1b[0m\n\n`);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (_) {}
|
|
427
|
+
|
|
428
|
+
const req = https.get('https://registry.npmjs.org/figma-rust-mcp/latest', {
|
|
429
|
+
headers: { 'User-Agent': 'figma-mcp-updater' },
|
|
430
|
+
timeout: 2000
|
|
431
|
+
}, (res) => {
|
|
432
|
+
let data = '';
|
|
433
|
+
res.on('data', chunk => { data += chunk; });
|
|
434
|
+
res.on('end', () => {
|
|
435
|
+
try {
|
|
436
|
+
if (res.statusCode === 200) {
|
|
437
|
+
const json = JSON.parse(data);
|
|
438
|
+
const latest = json.version;
|
|
439
|
+
fs.mkdirSync(path.join(os.homedir(), '.figma-mcp'), { recursive: true });
|
|
440
|
+
fs.writeFileSync(updateStateFile, JSON.stringify({
|
|
441
|
+
lastChecked: Date.now(),
|
|
442
|
+
latestVersion: latest
|
|
443
|
+
}, null, 2));
|
|
444
|
+
if (latest && isNewer(latest, pkgVersion)) {
|
|
445
|
+
process.stderr.write(`\n\x1b[36m⚡ Update available: v${pkgVersion} → v${latest}\x1b[0m\n\x1b[36m Run: npx figma-rust-mcp --upgrade\x1b[0m\n\n`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch (_) {}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
req.on('error', () => {});
|
|
452
|
+
req.on('timeout', () => req.destroy());
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function upgrade() {
|
|
456
|
+
console.log('\x1b[36m[figma-mcp]\x1b[0m Checking for updates and upgrading figma-rust-mcp...');
|
|
457
|
+
try {
|
|
458
|
+
execSync('npm install -g figma-rust-mcp@latest', { stdio: 'inherit' });
|
|
459
|
+
console.log('\x1b[32m✓ Upgraded figma-rust-mcp to latest version.\x1b[0m');
|
|
460
|
+
setupPlugin();
|
|
461
|
+
if (isServiceInstalled()) {
|
|
462
|
+
console.log('\x1b[36mUpdating background service binary...\x1b[0m');
|
|
463
|
+
const binPath = await findOrDownloadBinary();
|
|
464
|
+
await installService(binPath);
|
|
465
|
+
}
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.log('\x1b[33mTip: Run directly with latest version:\x1b[0m\n npx -y figma-rust-mcp@latest\n');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function setupAlias() {
|
|
472
|
+
const platform = process.platform;
|
|
473
|
+
if (platform === 'win32') {
|
|
474
|
+
console.log('\x1b[33mOn Windows, install globally for direct command access:\x1b[0m\n npm install -g figma-rust-mcp\n');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const shellRc = process.env.SHELL && process.env.SHELL.includes('zsh')
|
|
479
|
+
? path.join(os.homedir(), '.zshrc')
|
|
480
|
+
: path.join(os.homedir(), '.bashrc');
|
|
481
|
+
|
|
482
|
+
const aliasLine = "alias figma-mcp='npx figma-rust-mcp'";
|
|
483
|
+
try {
|
|
484
|
+
let content = fs.existsSync(shellRc) ? fs.readFileSync(shellRc, 'utf8') : '';
|
|
485
|
+
if (!content.includes('alias figma-mcp=')) {
|
|
486
|
+
fs.appendFileSync(shellRc, `\n# Figma MCP alias\n${aliasLine}\n`);
|
|
487
|
+
console.log(`\x1b[32m✓ Added alias to ${shellRc}:\x1b[0m`);
|
|
488
|
+
console.log(` ${aliasLine}`);
|
|
489
|
+
console.log(`\nRun \x1b[36msource ${shellRc}\x1b[0m to start using \x1b[1mfigma-mcp\x1b[0m command directly!`);
|
|
490
|
+
} else {
|
|
491
|
+
console.log(`\x1b[32m✓ Alias already exists in ${shellRc}\x1b[0m`);
|
|
492
|
+
}
|
|
493
|
+
} catch (err) {
|
|
494
|
+
console.error(`Failed to configure alias: ${err.message}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─── MAIN ─────────────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
async function main() {
|
|
501
|
+
const args = process.argv.slice(2);
|
|
502
|
+
|
|
503
|
+
if (args.includes('--upgrade') || args.includes('--update')) {
|
|
504
|
+
await upgrade();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (args.includes('--alias')) {
|
|
509
|
+
setupAlias();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (args.includes('--setup-plugin') || args.includes('--export-plugin') || args.includes('--setup')) {
|
|
514
|
+
const customDir = args.find(a => !a.startsWith('-') && a !== '--setup-plugin' && a !== '--export-plugin' && a !== '--setup');
|
|
515
|
+
setupPlugin(customDir);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (args.includes('--install-service')) {
|
|
520
|
+
try {
|
|
521
|
+
setupPlugin();
|
|
522
|
+
const binPath = await findOrDownloadBinary();
|
|
523
|
+
await installService(binPath);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (args.includes('--uninstall-service')) {
|
|
532
|
+
try {
|
|
533
|
+
await uninstallService();
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (args.includes('--service-status')) {
|
|
542
|
+
const installed = isServiceInstalled();
|
|
543
|
+
if (installed) {
|
|
544
|
+
console.log('\x1b[32m✓ figma-rust-mcp service is installed.\x1b[0m');
|
|
545
|
+
console.log(' To uninstall: npx figma-rust-mcp --uninstall-service');
|
|
546
|
+
} else {
|
|
547
|
+
console.log('\x1b[33m✗ figma-rust-mcp service is NOT installed.\x1b[0m');
|
|
548
|
+
console.log(' To install: npx figma-rust-mcp --install-service');
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Non-blocking auto update check
|
|
554
|
+
checkForUpdatesAsync();
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const binPath = await findOrDownloadBinary();
|
|
558
|
+
|
|
559
|
+
const child = spawn(binPath, args, {
|
|
560
|
+
stdio: 'inherit',
|
|
561
|
+
env: process.env,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
child.on('error', (err) => {
|
|
565
|
+
console.error(`[figma-mcp] Failed to start binary: ${err.message}`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
child.on('exit', (code, signal) => {
|
|
570
|
+
if (signal) {
|
|
571
|
+
process.kill(process.pid, signal);
|
|
572
|
+
} else {
|
|
573
|
+
process.exit(code ?? 0);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const forwardSignal = (sig) => {
|
|
578
|
+
if (child && !child.killed) {
|
|
579
|
+
child.kill(sig);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
process.on('SIGINT', () => forwardSignal('SIGINT'));
|
|
584
|
+
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
585
|
+
} catch (err) {
|
|
586
|
+
console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
main();
|