figma-rust-mcp 8.0.0 → 8.0.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/README.md +1 -1
- package/bin/agent-config.js +18 -9
- package/bin/figma-rust-mcp.js +180 -59
- package/package.json +1 -1
- package/scripts/test-plugin-tokens.mjs +22 -10
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
65
65
|
npx -y figma-rust-mcp@latest --init
|
|
66
66
|
```
|
|
67
67
|
The setup detects installed Codex, Claude Code, Antigravity, Cursor, Windsurf,
|
|
68
|
-
VS Code
|
|
68
|
+
VS Code, Copilot CLI, Gemini CLI, opencode, and Zed clients. Choose an action from the menu:
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
71
|
1) Quick setup — plugin + background service + detected agents
|
package/bin/agent-config.js
CHANGED
|
@@ -28,7 +28,10 @@ export function detectAgents({ home = os.homedir(), env = process.env, platform
|
|
|
28
28
|
{ id: 'antigravity', label: 'Google Antigravity', configPath: path.join(home, '.gemini', 'config', 'mcp_config.json'), installed: existsAny([path.join(home, '.antigravity-ide'), path.join(home, '.gemini', 'config', 'mcp_config.json'), platform === 'darwin' ? path.join(support, 'Antigravity') : null, platform === 'win32' ? path.join(local, 'Programs', 'Antigravity') : null, macApp('Antigravity')]) || hasCommand('antigravity', env, platform) },
|
|
29
29
|
{ id: 'cursor', label: 'Cursor', configPath: path.join(home, '.cursor', 'mcp.json'), installed: existsAny([path.join(home, '.cursor'), platform === 'darwin' ? path.join(support, 'Cursor') : null, platform === 'win32' ? path.join(local, 'Programs', 'Cursor') : null, macApp('Cursor')]) || hasCommand('cursor', env, platform) },
|
|
30
30
|
{ id: 'windsurf', label: 'Windsurf', configPath: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), installed: existsAny([path.join(home, '.codeium', 'windsurf'), platform === 'darwin' ? path.join(support, 'Windsurf') : null, platform === 'win32' ? path.join(local, 'Programs', 'Windsurf') : null, macApp('Windsurf')]) || hasCommand('windsurf', env, platform) },
|
|
31
|
-
{ id: 'vscode', label: 'VS Code
|
|
31
|
+
{ id: 'vscode', label: 'VS Code', configPath: path.join(platform === 'darwin' ? support : platform === 'win32' ? roaming : xdg, 'Code', 'User', 'mcp.json'), installed: existsAny([path.join(home, '.vscode'), platform === 'darwin' ? path.join(support, 'Code') : null, platform === 'linux' ? path.join(xdg, 'Code') : null, platform === 'win32' ? path.join(local, 'Programs', 'Microsoft VS Code') : null, platform === 'win32' ? path.join(roaming, 'Code') : null, macApp('Visual Studio Code')]) || hasCommand('code', env, platform) },
|
|
32
|
+
{ id: 'copilot', label: 'Copilot CLI', configPath: path.join(env.COPILOT_HOME || path.join(home, '.copilot'), 'mcp-config.json'), installed: existsAny([env.COPILOT_HOME, path.join(home, '.copilot')]) || hasCommand('copilot', env, platform) },
|
|
33
|
+
{ id: 'gemini', label: 'Gemini CLI', configPath: path.join(home, '.gemini', 'settings.json'), installed: hasCommand('gemini', env, platform) },
|
|
34
|
+
{ id: 'opencode', label: 'opencode', configPath: path.join(xdg, 'opencode', 'opencode.json'), installed: existsAny([path.join(xdg, 'opencode'), path.join(home, '.opencode')]) || hasCommand('opencode', env, platform) },
|
|
32
35
|
{ id: 'zed', label: 'Zed', configPath: platform === 'darwin' ? path.join(support, 'Zed', 'settings.json') : platform === 'win32' ? path.join(roaming, 'Zed', 'settings.json') : path.join(xdg, 'zed', 'settings.json'), installed: existsAny([path.join(xdg, 'zed'), platform === 'darwin' ? path.join(support, 'Zed') : null, platform === 'win32' ? path.join(roaming, 'Zed') : null, macApp('Zed')]) || hasCommand('zed', env, platform) },
|
|
33
36
|
];
|
|
34
37
|
return agents.filter(agent => agent.installed).map(({ installed, ...agent }) => agent);
|
|
@@ -40,9 +43,9 @@ function upsertJsonc(filePath, rootKey, value) {
|
|
|
40
43
|
const config = parse(source, errors, { allowTrailingComma: true, disallowComments: false });
|
|
41
44
|
if (errors.length) throw new Error(`Invalid JSON/JSONC in ${filePath}; left it unchanged.`);
|
|
42
45
|
const current = config?.[rootKey]?.[SERVER_NAME];
|
|
43
|
-
if (current && Object.entries(value).every(([key, entryValue]) => current[key] === entryValue)) return false;
|
|
46
|
+
if (current && Object.entries(value).every(([key, entryValue]) => JSON.stringify(current[key]) === JSON.stringify(entryValue))) return false;
|
|
44
47
|
const merged = current && typeof current === 'object' && !Array.isArray(current) ? { ...current, ...value } : value;
|
|
45
|
-
for (const key of ['command', 'args', 'serverUrl', 'url', 'type']) {
|
|
48
|
+
for (const key of ['command', 'args', 'serverUrl', 'httpUrl', 'url', 'type']) {
|
|
46
49
|
if (!Object.hasOwn(value, key) && Object.hasOwn(merged, key)) delete merged[key];
|
|
47
50
|
}
|
|
48
51
|
const edits = modify(source, [rootKey, SERVER_NAME], merged, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } });
|
|
@@ -76,14 +79,20 @@ function upsertCodex(filePath) {
|
|
|
76
79
|
return true;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
const ENTRIES = {
|
|
83
|
+
antigravity: ['mcpServers', { serverUrl: 'http://127.0.0.1:41730/sse' }],
|
|
84
|
+
claude: ['mcpServers', { type: 'http', url: HTTP_URL }],
|
|
85
|
+
windsurf: ['mcpServers', { serverUrl: HTTP_URL }],
|
|
86
|
+
vscode: ['servers', { type: 'http', url: HTTP_URL }],
|
|
87
|
+
copilot: ['mcpServers', { type: 'http', url: HTTP_URL, tools: ['*'] }],
|
|
88
|
+
gemini: ['mcpServers', { httpUrl: HTTP_URL }],
|
|
89
|
+
opencode: ['mcp', { type: 'remote', url: HTTP_URL, enabled: true }],
|
|
90
|
+
zed: ['context_servers', { url: HTTP_URL }],
|
|
91
|
+
};
|
|
92
|
+
|
|
79
93
|
export function configureAgent(agent) {
|
|
80
|
-
const entry = agent.id === 'antigravity'
|
|
81
|
-
? { serverUrl: 'http://127.0.0.1:41730/sse' }
|
|
82
|
-
: agent.id === 'claude' || agent.id === 'vscode'
|
|
83
|
-
? { type: 'http', url: HTTP_URL }
|
|
84
|
-
: { url: HTTP_URL };
|
|
85
94
|
if (agent.id === 'codex') return upsertCodex(agent.configPath);
|
|
86
|
-
const rootKey = agent.id
|
|
95
|
+
const [rootKey, entry] = ENTRIES[agent.id] || ['mcpServers', { url: HTTP_URL }];
|
|
87
96
|
return upsertJsonc(agent.configPath, rootKey, entry);
|
|
88
97
|
}
|
|
89
98
|
|
package/bin/figma-rust-mcp.js
CHANGED
|
@@ -40,6 +40,17 @@ try {
|
|
|
40
40
|
} catch (_) {}
|
|
41
41
|
if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
|
|
42
42
|
|
|
43
|
+
// ─── TERMINAL UI ──────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
46
|
+
const paint = code => text => useColor ? `\x1b[${code}m${text}\x1b[0m` : String(text);
|
|
47
|
+
const bold = paint('1'), dim = paint('2'), green = paint('32'), yellow = paint('33'), red = paint('31'), cyan = paint('36');
|
|
48
|
+
const tag = code => process.stderr.isTTY && !process.env.NO_COLOR ? `\x1b[${code}m[figma-rust-mcp]\x1b[0m` : '[figma-rust-mcp]';
|
|
49
|
+
const tilde = p => p.startsWith(os.homedir()) ? `~${p.slice(os.homedir().length)}` : p;
|
|
50
|
+
const ok = (msg, detail = '') => console.log(` ${green('✓')} ${msg}${detail ? ` ${dim(detail)}` : ''}`);
|
|
51
|
+
const note = msg => console.log(` ${dim(msg)}`);
|
|
52
|
+
const step = (n, total, title) => console.log(`\n${cyan(`[${n}/${total}]`)} ${bold(title)}`);
|
|
53
|
+
|
|
43
54
|
function getPlatformArch() {
|
|
44
55
|
const platform = process.platform;
|
|
45
56
|
const arch = process.arch;
|
|
@@ -83,6 +94,27 @@ function getCacheDir(version) {
|
|
|
83
94
|
return path.join(baseCache, 'figma-rust-mcp', `v${version}`);
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
// Progress bar on stderr (stdout carries MCP stdio traffic); silent when stderr is not a terminal.
|
|
98
|
+
function downloadProgress(total) {
|
|
99
|
+
const tty = process.stderr.isTTY;
|
|
100
|
+
const started = Date.now();
|
|
101
|
+
let received = 0, lastDraw = 0;
|
|
102
|
+
const mb = bytes => (bytes / 1048576).toFixed(1);
|
|
103
|
+
const draw = () => {
|
|
104
|
+
const speed = `${mb(received / Math.max((Date.now() - started) / 1000, 0.001))} MB/s`;
|
|
105
|
+
if (!total) return process.stderr.write(`\r\x1b[2K ↓ ${mb(received)} MB ${speed}`);
|
|
106
|
+
const ratio = Math.min(received / total, 1), width = 24, filled = Math.round(ratio * width);
|
|
107
|
+
process.stderr.write(`\r\x1b[2K ↓ \x1b[36m${'█'.repeat(filled)}\x1b[2m${'░'.repeat(width - filled)}\x1b[0m ${String(Math.round(ratio * 100)).padStart(3)}% ${mb(received)}/${mb(total)} MB ${speed}`);
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
add(bytes) {
|
|
111
|
+
received += bytes;
|
|
112
|
+
if (tty && Date.now() - lastDraw > 80) { lastDraw = Date.now(); draw(); }
|
|
113
|
+
},
|
|
114
|
+
done() { if (tty) { draw(); process.stderr.write('\n'); } },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
86
118
|
function downloadFile(url, destPath) {
|
|
87
119
|
return new Promise((resolve, reject) => {
|
|
88
120
|
const followRedirects = (currentUrl, redirectCount = 0) => {
|
|
@@ -104,9 +136,12 @@ function downloadFile(url, destPath) {
|
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
const fileStream = fs.createWriteStream(destPath);
|
|
139
|
+
const progress = downloadProgress(Number(res.headers['content-length']) || 0);
|
|
140
|
+
res.on('data', chunk => progress.add(chunk.length));
|
|
107
141
|
res.pipe(fileStream);
|
|
108
142
|
|
|
109
143
|
fileStream.on('finish', () => {
|
|
144
|
+
progress.done();
|
|
110
145
|
fileStream.close(() => resolve());
|
|
111
146
|
});
|
|
112
147
|
|
|
@@ -147,8 +182,12 @@ async function findOrDownloadBinary() {
|
|
|
147
182
|
const localRelease = path.resolve(__dirname, '../target/release/figma-rust-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
148
183
|
const localDebug = path.resolve(__dirname, '../target/debug/figma-rust-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
149
184
|
|
|
150
|
-
|
|
151
|
-
|
|
185
|
+
// A stale local build would silently downgrade the service; only use one built from this version.
|
|
186
|
+
const isCurrentBuild = bin => {
|
|
187
|
+
try { return fs.existsSync(bin) && execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000 }).trim().endsWith(` ${pkgVersion}`); }
|
|
188
|
+
catch (_) { return false; }
|
|
189
|
+
};
|
|
190
|
+
for (const bin of [localRelease, localDebug]) if (isCurrentBuild(bin)) return bin;
|
|
152
191
|
|
|
153
192
|
const info = getPlatformArch();
|
|
154
193
|
const cacheDir = getCacheDir(pkgVersion);
|
|
@@ -167,12 +206,12 @@ async function findOrDownloadBinary() {
|
|
|
167
206
|
const releaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/download/v${pkgVersion}/${info.assetName}`;
|
|
168
207
|
const latestUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest/download/${info.assetName}`;
|
|
169
208
|
|
|
170
|
-
process.stderr.write(
|
|
209
|
+
process.stderr.write(`${tag(36)} Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
|
|
171
210
|
|
|
172
211
|
try {
|
|
173
212
|
await downloadFile(releaseUrl, archivePath);
|
|
174
213
|
} catch (err) {
|
|
175
|
-
process.stderr.write(
|
|
214
|
+
process.stderr.write(`${tag(33)} Release v${pkgVersion} not found, trying latest release...\n`);
|
|
176
215
|
try {
|
|
177
216
|
await downloadFile(latestUrl, archivePath);
|
|
178
217
|
} catch (fallbackErr) {
|
|
@@ -201,7 +240,7 @@ async function findOrDownloadBinary() {
|
|
|
201
240
|
fs.chmodSync(cachedBin, 0o755);
|
|
202
241
|
}
|
|
203
242
|
|
|
204
|
-
process.stderr.write(
|
|
243
|
+
process.stderr.write(`${tag(32)} Downloaded to ${tilde(cacheDir)}\n`);
|
|
205
244
|
return cachedBin;
|
|
206
245
|
}
|
|
207
246
|
|
|
@@ -214,7 +253,15 @@ async function findOrDownloadBinary() {
|
|
|
214
253
|
* - Windows: Task Scheduler XML via schtasks
|
|
215
254
|
*/
|
|
216
255
|
async function installService(binPath) {
|
|
256
|
+
// An outdated CLI (e.g. an old global install run via plain `npx figma-rust-mcp`) must not replace a newer service.
|
|
257
|
+
const running = await serverVersion(Number(process.env.FIGMA_RUST_MCP_PORT) || 41730);
|
|
258
|
+
if (running && isNewer(running, pkgVersion)) {
|
|
259
|
+
console.log(` ${yellow('!')} Service runs v${running}, newer than this CLI (v${pkgVersion}); left it unchanged.`);
|
|
260
|
+
note('Use the latest CLI: npx -y figma-rust-mcp@latest --init');
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
217
263
|
removeLegacyService();
|
|
264
|
+
await stopRunningServer();
|
|
218
265
|
const platform = process.platform;
|
|
219
266
|
|
|
220
267
|
if (platform === 'darwin') {
|
|
@@ -253,15 +300,9 @@ async function installService(binPath) {
|
|
|
253
300
|
|
|
254
301
|
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
255
302
|
|
|
256
|
-
|
|
257
|
-
try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
258
|
-
execSync(`launchctl load -w "${plistPath}"`, { stdio: 'inherit' });
|
|
303
|
+
execSync(`launchctl load -w "${plistPath}"`, { stdio: 'pipe' });
|
|
259
304
|
|
|
260
|
-
|
|
261
|
-
console.log(` Plist: ${plistPath}`);
|
|
262
|
-
console.log(` Logs: ${logDir}/stdout.log`);
|
|
263
|
-
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
264
|
-
console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
|
|
305
|
+
await serviceInstalled(`Logs ${tilde(path.join(logDir, 'stdout.log'))}`);
|
|
265
306
|
|
|
266
307
|
} else if (platform === 'linux') {
|
|
267
308
|
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
@@ -284,13 +325,10 @@ WantedBy=default.target
|
|
|
284
325
|
`;
|
|
285
326
|
|
|
286
327
|
fs.writeFileSync(servicePath, unit, 'utf8');
|
|
287
|
-
execSync(`systemctl --user daemon-reload`, { stdio: '
|
|
288
|
-
execSync(`systemctl --user enable --now figma-rust-mcp`, { stdio: '
|
|
328
|
+
execSync(`systemctl --user daemon-reload`, { stdio: 'pipe' });
|
|
329
|
+
execSync(`systemctl --user enable --now figma-rust-mcp`, { stdio: 'pipe' });
|
|
289
330
|
|
|
290
|
-
|
|
291
|
-
console.log(` Service: ${servicePath}`);
|
|
292
|
-
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
293
|
-
console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
|
|
331
|
+
await serviceInstalled('Logs: journalctl --user -u figma-rust-mcp');
|
|
294
332
|
|
|
295
333
|
} else if (platform === 'win32') {
|
|
296
334
|
const taskName = 'FigmaRustMCPServer';
|
|
@@ -298,20 +336,91 @@ WantedBy=default.target
|
|
|
298
336
|
// The native launcher detaches the server and redirects output to a log file.
|
|
299
337
|
const taskCommand = `"${binPath}" --background`;
|
|
300
338
|
execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
|
|
301
|
-
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: '
|
|
339
|
+
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'pipe', windowsHide: true });
|
|
302
340
|
|
|
303
341
|
// Start it immediately too
|
|
304
342
|
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
|
305
343
|
|
|
306
|
-
|
|
307
|
-
console.log(` Task: ${taskName}`);
|
|
308
|
-
console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
|
|
309
|
-
console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
|
|
344
|
+
await serviceInstalled(`Task Scheduler: ${taskName}`);
|
|
310
345
|
|
|
311
346
|
} else {
|
|
312
347
|
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
313
348
|
process.exit(1);
|
|
314
349
|
}
|
|
350
|
+
pruneOldBinaries(binPath);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Delete cached binaries of versions older than the one the service now runs. A process still
|
|
355
|
+
* running an old binary keeps working on macOS/Linux; on Windows a locked file is skipped.
|
|
356
|
+
*/
|
|
357
|
+
function pruneOldBinaries(binPath) {
|
|
358
|
+
const root = path.dirname(getCacheDir(pkgVersion));
|
|
359
|
+
const current = path.basename(path.dirname(binPath)).match(/^v(\d+\.\d+\.\d+)$/)?.[1] || pkgVersion;
|
|
360
|
+
let entries;
|
|
361
|
+
try { entries = fs.readdirSync(root); } catch (_) { return; }
|
|
362
|
+
let freed = 0, removed = 0;
|
|
363
|
+
for (const name of entries) {
|
|
364
|
+
const version = name.match(/^v(\d+\.\d+\.\d+)$/)?.[1];
|
|
365
|
+
if (!version || !isNewer(current, version)) continue;
|
|
366
|
+
const dir = path.join(root, name);
|
|
367
|
+
try {
|
|
368
|
+
const size = fs.readdirSync(dir).reduce((sum, file) => sum + fs.statSync(path.join(dir, file)).size, 0);
|
|
369
|
+
fs.rmSync(dir, { recursive: true });
|
|
370
|
+
freed += size; removed++;
|
|
371
|
+
} catch (_) {}
|
|
372
|
+
}
|
|
373
|
+
if (removed) note(`Removed ${removed} old binar${removed === 1 ? 'y' : 'ies'} (${(freed / 1048576).toFixed(0)} MB freed)`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function serviceInstalled(where) {
|
|
377
|
+
const port = Number(process.env.FIGMA_RUST_MCP_PORT) || 41730;
|
|
378
|
+
const deadline = Date.now() + 10000;
|
|
379
|
+
while (!(await serverVersion(port)) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 300));
|
|
380
|
+
if (await serverVersion(port)) ok('Background service running', `http://127.0.0.1:${port}`);
|
|
381
|
+
else console.log(` ${yellow('!')} Service installed but not answering on port ${port} yet`);
|
|
382
|
+
note(`Starts on login · ${where}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Version of the figma-rust-mcp answering on `port`, or null. */
|
|
386
|
+
function serverVersion(port) {
|
|
387
|
+
return new Promise(resolve => {
|
|
388
|
+
const req = http.get({ host: '127.0.0.1', port, path: '/plugin/version', timeout: 500 }, res => {
|
|
389
|
+
let body = '';
|
|
390
|
+
res.on('data', chunk => { body += chunk; });
|
|
391
|
+
res.on('end', () => { try { const v = JSON.parse(body); resolve(v.name === 'figma-rust-mcp' ? v.version || '0.0.0' : null); } catch (_) { resolve(null); } });
|
|
392
|
+
});
|
|
393
|
+
req.on('error', () => resolve(null));
|
|
394
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Stop the installed service and any other running server before installing the new one.
|
|
400
|
+
* A still-running old version keeps port 41730, so the new one would move to 41731 while
|
|
401
|
+
* agents stay pointed at the old server.
|
|
402
|
+
*/
|
|
403
|
+
async function stopRunningServer() {
|
|
404
|
+
const quiet = cmd => { try { execSync(cmd, { stdio: 'pipe', windowsHide: true }); } catch (_) {} };
|
|
405
|
+
if (process.platform === 'darwin') {
|
|
406
|
+
quiet(`launchctl unload "${path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-rust-mcp.server.plist')}"`);
|
|
407
|
+
quiet('pkill -x figma-rust-mcp');
|
|
408
|
+
} else if (process.platform === 'linux') {
|
|
409
|
+
quiet('systemctl --user stop figma-rust-mcp');
|
|
410
|
+
quiet('pkill -x figma-rust-mcp');
|
|
411
|
+
} else if (process.platform === 'win32') {
|
|
412
|
+
quiet('schtasks /End /TN "FigmaRustMCPServer"');
|
|
413
|
+
quiet('taskkill /F /T /IM figma-rust-mcp.exe');
|
|
414
|
+
}
|
|
415
|
+
const port = Number(process.env.FIGMA_RUST_MCP_PORT) || 41730;
|
|
416
|
+
const deadline = Date.now() + 10000;
|
|
417
|
+
while (await serverVersion(port)) {
|
|
418
|
+
if (Date.now() > deadline) {
|
|
419
|
+
console.log(` ${yellow('!')} An old figma-rust-mcp still answers on port ${port}; stop it, then rerun setup.`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
423
|
+
}
|
|
315
424
|
}
|
|
316
425
|
|
|
317
426
|
/**
|
|
@@ -331,7 +440,7 @@ async function uninstallService() {
|
|
|
331
440
|
}
|
|
332
441
|
removeLegacyService();
|
|
333
442
|
|
|
334
|
-
|
|
443
|
+
ok('Background service removed');
|
|
335
444
|
|
|
336
445
|
} else if (platform === 'linux') {
|
|
337
446
|
try { execSync(`systemctl --user disable --now figma-rust-mcp`, { stdio: 'inherit' }); } catch (_) {}
|
|
@@ -340,14 +449,14 @@ async function uninstallService() {
|
|
|
340
449
|
try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
|
|
341
450
|
removeLegacyService();
|
|
342
451
|
|
|
343
|
-
|
|
452
|
+
ok('Background service removed');
|
|
344
453
|
|
|
345
454
|
} else if (platform === 'win32') {
|
|
346
455
|
const taskName = 'FigmaRustMCPServer';
|
|
347
456
|
try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
348
457
|
removeLegacyService();
|
|
349
458
|
|
|
350
|
-
|
|
459
|
+
ok('Background service removed');
|
|
351
460
|
|
|
352
461
|
} else {
|
|
353
462
|
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
@@ -415,13 +524,9 @@ function setupPlugin(customDir) {
|
|
|
415
524
|
}
|
|
416
525
|
|
|
417
526
|
const manifestPath = path.join(pluginDir, 'manifest.json');
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
console.log(` 1. Open Figma Desktop`);
|
|
422
|
-
console.log(` 2. Go to Plugins → Development → Import plugin from manifest...`);
|
|
423
|
-
console.log(` 3. Select: ${manifestPath}`);
|
|
424
|
-
console.log(` 4. Done! All future updates load dynamically from figma-rust-mcp without file re-imports.\n`);
|
|
527
|
+
ok('Figma plugin installed', tilde(manifestPath));
|
|
528
|
+
note('First time only: Figma Desktop → Plugins → Development → Import plugin from manifest…');
|
|
529
|
+
note('Later updates load automatically; no re-import needed.');
|
|
425
530
|
return manifestPath;
|
|
426
531
|
}
|
|
427
532
|
|
|
@@ -486,19 +591,17 @@ function checkForUpdatesAsync() {
|
|
|
486
591
|
}
|
|
487
592
|
|
|
488
593
|
async function upgrade() {
|
|
489
|
-
|
|
594
|
+
const major = pkgVersion.split('.')[0];
|
|
595
|
+
console.log(`${tag(36)} Upgrading figma-rust-mcp v${major}.x…`);
|
|
490
596
|
try {
|
|
491
|
-
const major = pkgVersion ? pkgVersion.split('.')[0] : 'latest';
|
|
492
597
|
execSync(`npm install -g figma-rust-mcp@${major}`, { stdio: 'inherit' });
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const binPath = await findOrDownloadBinary();
|
|
498
|
-
await installService(binPath);
|
|
499
|
-
}
|
|
598
|
+
// This process still runs the old code and version; let the new package refresh the plugin and service.
|
|
599
|
+
const args = ['-y', `figma-rust-mcp@${major}`, isServiceInstalled() ? '--install-service' : '--setup-plugin'];
|
|
600
|
+
execFileSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', args, { stdio: 'inherit', shell: process.platform === 'win32' });
|
|
601
|
+
ok(`Upgraded figma-rust-mcp (v${major}.x)`);
|
|
500
602
|
} catch (err) {
|
|
501
|
-
console.log('
|
|
603
|
+
console.log(` ${yellow('!')} Upgrade failed: ${err.message}`);
|
|
604
|
+
note('Run the latest version directly: npx -y figma-rust-mcp@latest --init');
|
|
502
605
|
}
|
|
503
606
|
}
|
|
504
607
|
|
|
@@ -510,25 +613,42 @@ async function setupMenu() {
|
|
|
510
613
|
|
|
511
614
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
512
615
|
const agents = detectAgents();
|
|
616
|
+
const items = [
|
|
617
|
+
['1', 'Quick setup', 'plugin + background service + agents', true],
|
|
618
|
+
['2', 'Figma plugin', 'install or update only'],
|
|
619
|
+
['3', 'Run server', 'in this terminal'],
|
|
620
|
+
['4', 'Agents', `configure detected agents (${agents.length} found)`],
|
|
621
|
+
['5', 'Service status', 'is the background service installed?'],
|
|
622
|
+
['6', 'Upgrade', 'update package and refresh setup'],
|
|
623
|
+
['7', 'Remove service', 'stop and uninstall the background service'],
|
|
624
|
+
['0', 'Exit', ''],
|
|
625
|
+
];
|
|
513
626
|
try {
|
|
514
|
-
console.log(`\n
|
|
515
|
-
|
|
627
|
+
console.log(`\n ${bold('Figma Rust MCP')} ${dim(`v${pkgVersion}`)}\n`);
|
|
628
|
+
for (const [key, label, hint, recommended] of items) {
|
|
629
|
+
console.log(` ${cyan(key)} ${label.padEnd(16)}${dim(hint)}${recommended ? ` ${green('(recommended)')}` : ''}`);
|
|
630
|
+
}
|
|
631
|
+
const choice = (await rl.question(`\n${cyan('›')} Choose [1]: `)).trim() || '1';
|
|
516
632
|
switch (choice) {
|
|
517
633
|
case '1': {
|
|
634
|
+
step(1, 3, 'Figma plugin');
|
|
518
635
|
setupPlugin();
|
|
636
|
+
step(2, 3, 'Background service');
|
|
519
637
|
const binPath = await findOrDownloadBinary();
|
|
520
638
|
await installService(binPath);
|
|
639
|
+
step(3, 3, 'AI agents');
|
|
521
640
|
await configureDetectedAgents(rl, agents);
|
|
641
|
+
console.log(`\n${green('✓')} ${bold('Setup complete.')} Restart your agents, then run the plugin in Figma.\n`);
|
|
522
642
|
break;
|
|
523
643
|
}
|
|
524
644
|
case '2': setupPlugin(); break;
|
|
525
645
|
case '3': await runBinary(['--server']); break;
|
|
526
|
-
case '4': await configureDetectedAgents(rl, agents); break;
|
|
527
|
-
case '5':
|
|
646
|
+
case '4': await configureDetectedAgents(rl, agents); note('Restart the configured agents to connect.'); break;
|
|
647
|
+
case '5': isServiceInstalled() ? ok('Background service is installed') : console.log(` ${yellow('!')} Background service is not installed`); break;
|
|
528
648
|
case '6': await upgrade(); break;
|
|
529
649
|
case '7': await uninstallService(); break;
|
|
530
650
|
case '0': break;
|
|
531
|
-
default: console.log('Unknown option. Run --init to try again
|
|
651
|
+
default: console.log(` ${yellow('!')} Unknown option. Run --init to try again.`);
|
|
532
652
|
}
|
|
533
653
|
} finally {
|
|
534
654
|
rl.close();
|
|
@@ -537,33 +657,34 @@ async function setupMenu() {
|
|
|
537
657
|
|
|
538
658
|
async function configureDetectedAgents(rl, agents) {
|
|
539
659
|
if (!agents.length) {
|
|
540
|
-
console.log('No supported AI
|
|
660
|
+
console.log(` ${yellow('!')} No supported AI agents detected. Install one and rerun --init.`);
|
|
541
661
|
return;
|
|
542
662
|
}
|
|
543
663
|
|
|
544
|
-
|
|
545
|
-
agents.forEach((agent, index) => console.log(` ${index + 1}
|
|
546
|
-
const answer = (await rl.question('Configure which
|
|
547
|
-
if (answer === '')
|
|
664
|
+
const width = Math.max(...agents.map(agent => agent.label.length));
|
|
665
|
+
agents.forEach((agent, index) => console.log(` ${cyan(index + 1)} ${agent.label.padEnd(width + 2)}${dim(tilde(agent.configPath))}`));
|
|
666
|
+
const answer = (await rl.question(`\n${cyan('›')} Configure which? ${dim('numbers like 1,3 · a = all · n = skip')} [a]: `)).trim().toLowerCase() || 'a';
|
|
667
|
+
if (answer === 'n') {
|
|
668
|
+
note('Skipped; MCP configuration was not changed.');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
548
671
|
const selected = answer === 'a'
|
|
549
672
|
? agents
|
|
550
673
|
: [...new Set(answer.split(',').map(value => Number(value.trim()) - 1))]
|
|
551
674
|
.filter(index => Number.isInteger(index) && index >= 0 && index < agents.length)
|
|
552
675
|
.map(index => agents[index]);
|
|
553
676
|
if (!selected.length) {
|
|
554
|
-
console.log('No valid
|
|
677
|
+
console.log(` ${yellow('!')} No valid selection; MCP configuration was not changed.`);
|
|
555
678
|
return;
|
|
556
679
|
}
|
|
557
680
|
|
|
558
681
|
for (const agent of selected) {
|
|
559
682
|
try {
|
|
560
|
-
|
|
561
|
-
console.log(` ${changed ? '✓ Configured' : '✓ Already configured'} ${agent.label}`);
|
|
683
|
+
ok(agent.label.padEnd(width + 2), configureAgent(agent) ? 'configured' : 'already configured');
|
|
562
684
|
} catch (err) {
|
|
563
|
-
console.
|
|
685
|
+
console.log(` ${red('✗')} ${agent.label.padEnd(width + 2)}${dim(err.message)}`);
|
|
564
686
|
}
|
|
565
687
|
}
|
|
566
|
-
console.log('Restart or reload the selected agent to connect it to Figma Rust MCP.');
|
|
567
688
|
}
|
|
568
689
|
|
|
569
690
|
function runBinary(args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.2",
|
|
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",
|
|
@@ -289,21 +289,33 @@ test('setup command prints scriptable choices when no terminal is attached', ()
|
|
|
289
289
|
test('agent setup detects installed clients and preserves existing JSONC and TOML config', () => {
|
|
290
290
|
const home = mkdtempSync(path.join(os.tmpdir(), 'figma-rust-mcp-'));
|
|
291
291
|
try {
|
|
292
|
-
const
|
|
293
|
-
|
|
292
|
+
const bin = path.join(home, 'bin');
|
|
293
|
+
mkdirSync(bin);
|
|
294
|
+
writeFileSync(path.join(bin, 'gemini'), '');
|
|
295
|
+
const env = { PATH: bin };
|
|
296
|
+
for (const dir of ['.codex', '.claude', '.antigravity-ide', '.cursor', '.codeium/windsurf', '.config/Code', '.copilot', '.config/opencode', '.config/zed']) {
|
|
294
297
|
mkdirSync(path.join(home, dir), { recursive: true });
|
|
295
298
|
}
|
|
296
299
|
const agents = detectAgents({ home, env, platform: 'linux' });
|
|
297
|
-
assert.deepEqual(agents.map(agent => agent.id), ['codex', 'claude', 'antigravity', 'cursor', 'windsurf', 'vscode', 'zed']);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
assert.deepEqual(agents.map(agent => agent.id), ['codex', 'claude', 'antigravity', 'cursor', 'windsurf', 'vscode', 'copilot', 'gemini', 'opencode', 'zed']);
|
|
301
|
+
|
|
302
|
+
const expected = {
|
|
303
|
+
claude: ['mcpServers', 'url', 'http://127.0.0.1:41730/mcp'],
|
|
304
|
+
antigravity: ['mcpServers', 'serverUrl', 'http://127.0.0.1:41730/sse'],
|
|
305
|
+
windsurf: ['mcpServers', 'serverUrl', 'http://127.0.0.1:41730/mcp'],
|
|
306
|
+
vscode: ['servers', 'url', 'http://127.0.0.1:41730/mcp'],
|
|
307
|
+
copilot: ['mcpServers', 'url', 'http://127.0.0.1:41730/mcp'],
|
|
308
|
+
gemini: ['mcpServers', 'httpUrl', 'http://127.0.0.1:41730/mcp'],
|
|
309
|
+
opencode: ['mcp', 'url', 'http://127.0.0.1:41730/mcp'],
|
|
310
|
+
};
|
|
311
|
+
for (const [id, [rootKey, key, value]] of Object.entries(expected)) {
|
|
300
312
|
const agent = agents.find(candidate => candidate.id === id);
|
|
301
|
-
configureAgent(agent);
|
|
302
|
-
const config = parseJsonc(readFileSync(agent.configPath, 'utf8'))
|
|
303
|
-
assert.
|
|
304
|
-
|
|
305
|
-
else assert.equal(config.url, 'http://127.0.0.1:41730/mcp');
|
|
313
|
+
assert.equal(configureAgent(agent), true);
|
|
314
|
+
const config = parseJsonc(readFileSync(agent.configPath, 'utf8'))[rootKey]['figma-rust-mcp'];
|
|
315
|
+
assert.equal(config[key], value);
|
|
316
|
+
assert.equal(configureAgent(agent), false);
|
|
306
317
|
}
|
|
318
|
+
assert.equal(agents.find(agent => agent.id === 'vscode').configPath, path.join(home, '.config', 'Code', 'User', 'mcp.json'));
|
|
307
319
|
|
|
308
320
|
const cursor = agents.find(agent => agent.id === 'cursor');
|
|
309
321
|
writeFileSync(cursor.configPath, '{\n // keep this note\n "mcpServers": { "other": { "url": "http://example.test" } }\n}\n');
|