@mp3wizard/figma-console-mcp 1.15.3 ā 1.15.4
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 +0 -22
- package/dist/cloudflare/core/cloud-websocket-relay.js +2 -3
- package/dist/cloudflare/core/config.js +1 -1
- package/dist/cloudflare/core/figma-api.js +6 -2
- package/dist/cloudflare/core/figma-desktop-connector.js +1 -2
- package/dist/cloudflare/core/port-discovery.js +73 -2
- package/dist/cloudflare/core/websocket-server.js +151 -21
- package/dist/cloudflare/core/write-tools.js +10 -2
- package/dist/cloudflare/index.js +701 -746
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +6 -2
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +2 -8
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +37 -22
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/ui.html +1156 -173
- package/package.json +84 -85
package/README.md
CHANGED
|
@@ -758,28 +758,6 @@ The architecture supports adding new apps with minimal boilerplate ā each app
|
|
|
758
758
|
|
|
759
759
|
---
|
|
760
760
|
|
|
761
|
-
## š Security
|
|
762
|
-
|
|
763
|
-
This repository has undergone **two security reviews** ā most recently on 2026-03-18 (v1.14.0).
|
|
764
|
-
|
|
765
|
-
### What Was Reviewed
|
|
766
|
-
|
|
767
|
-
- Authentication & token handling
|
|
768
|
-
- Input validation and sanitization
|
|
769
|
-
- Rate limiting and abuse prevention
|
|
770
|
-
- Error message exposure
|
|
771
|
-
- Dependency vulnerabilities
|
|
772
|
-
- Secrets management
|
|
773
|
-
|
|
774
|
-
### Review Outcome
|
|
775
|
-
|
|
776
|
-
All identified security findings have been addressed and patched in this fork. No sensitive credentials are exposed, and error messages have been sanitized to prevent information leakage.
|
|
777
|
-
|
|
778
|
-
š **Latest report:** [SECURITY-REVIEW-2026-03-18.md](Security%20review%20report/SECURITY-REVIEW-2026-03-18.md)
|
|
779
|
-
š **Previous report:** [SECURITY-REVIEW-2026-03-16.md](Security%20review%20report/SECURITY-REVIEW-2026-03-16.md)
|
|
780
|
-
|
|
781
|
-
---
|
|
782
|
-
|
|
783
761
|
## š» Development
|
|
784
762
|
|
|
785
763
|
```bash
|
|
@@ -164,7 +164,6 @@ export class PluginRelayDO extends DurableObject {
|
|
|
164
164
|
}
|
|
165
165
|
const body = await request.json();
|
|
166
166
|
const { method, params = {}, timeoutMs = 15000 } = body;
|
|
167
|
-
const safeTimeout = Math.min(Math.max(timeoutMs, 1000), 60000); // clamp 1sā60s
|
|
168
167
|
const id = `relay_${++this.requestIdCounter}_${Date.now()}`;
|
|
169
168
|
// Send command to plugin
|
|
170
169
|
try {
|
|
@@ -178,9 +177,9 @@ export class PluginRelayDO extends DurableObject {
|
|
|
178
177
|
const timeoutId = setTimeout(() => {
|
|
179
178
|
if (this.pendingRequests.has(id)) {
|
|
180
179
|
this.pendingRequests.delete(id);
|
|
181
|
-
resolve(new Response(JSON.stringify({ error: `Command ${method} timed out after ${
|
|
180
|
+
resolve(new Response(JSON.stringify({ error: `Command ${method} timed out after ${timeoutMs}ms` }), { status: 504, headers: { 'Content-Type': 'application/json' } }));
|
|
182
181
|
}
|
|
183
|
-
},
|
|
182
|
+
}, timeoutMs);
|
|
184
183
|
this.pendingRequests.set(id, { resolve, reject: () => { }, timeoutId });
|
|
185
184
|
});
|
|
186
185
|
}
|
|
@@ -30,7 +30,7 @@ const DEFAULT_CONFIG = {
|
|
|
30
30
|
args: [
|
|
31
31
|
'--disable-blink-features=AutomationControlled',
|
|
32
32
|
'--disable-dev-shm-usage',
|
|
33
|
-
|
|
33
|
+
'--no-sandbox', // Note: Only use in trusted environments
|
|
34
34
|
],
|
|
35
35
|
},
|
|
36
36
|
console: {
|
|
@@ -94,12 +94,16 @@ export class FigmaAPI {
|
|
|
94
94
|
// OAuth tokens start with 'figu_' and require Authorization: Bearer header
|
|
95
95
|
// Personal Access Tokens use X-Figma-Token header
|
|
96
96
|
const isOAuthToken = this.accessToken.startsWith('figu_');
|
|
97
|
-
|
|
97
|
+
// Debug logging to verify token is being used
|
|
98
|
+
const tokenPreview = this.accessToken ? `${this.accessToken.substring(0, 10)}...` : 'NO TOKEN';
|
|
99
|
+
logger.info({
|
|
98
100
|
url,
|
|
101
|
+
tokenPreview,
|
|
99
102
|
hasToken: !!this.accessToken,
|
|
103
|
+
tokenLength: this.accessToken?.length,
|
|
100
104
|
isOAuthToken,
|
|
101
105
|
authMethod: isOAuthToken ? 'Bearer' : 'X-Figma-Token'
|
|
102
|
-
}, 'Making Figma API request');
|
|
106
|
+
}, 'Making Figma API request with token');
|
|
103
107
|
const headers = {
|
|
104
108
|
'Content-Type': 'application/json',
|
|
105
109
|
...(options.headers || {}),
|
|
@@ -80,7 +80,7 @@ export function advertisePort(port, host = 'localhost') {
|
|
|
80
80
|
};
|
|
81
81
|
const filePath = getPortFilePath(port);
|
|
82
82
|
try {
|
|
83
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2)
|
|
83
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
84
84
|
logger.info({ port, filePath }, 'Port advertised');
|
|
85
85
|
}
|
|
86
86
|
catch (error) {
|
|
@@ -103,7 +103,7 @@ export function refreshPortAdvertisement(port) {
|
|
|
103
103
|
if (data.pid !== process.pid)
|
|
104
104
|
return;
|
|
105
105
|
data.lastSeen = new Date().toISOString();
|
|
106
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2)
|
|
106
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
107
107
|
}
|
|
108
108
|
catch {
|
|
109
109
|
// Best-effort ā heartbeat failures are non-fatal
|
|
@@ -265,6 +265,77 @@ export function cleanupStalePortFiles() {
|
|
|
265
265
|
}
|
|
266
266
|
return cleaned;
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Deep scan for orphaned MCP server processes that hold ports but have no port files.
|
|
270
|
+
* These are processes left behind by Claude Desktop when tabs close without proper cleanup.
|
|
271
|
+
*
|
|
272
|
+
* Uses lsof (macOS/Linux) to find PIDs listening on each port in the range,
|
|
273
|
+
* then verifies they're figma-console-mcp before terminating.
|
|
274
|
+
*
|
|
275
|
+
* Call AFTER cleanupStalePortFiles() ā that handles the port-file-based cleanup first,
|
|
276
|
+
* then this catches any remaining ghosts.
|
|
277
|
+
*/
|
|
278
|
+
export function cleanupOrphanedProcesses(preferredPort = DEFAULT_WS_PORT) {
|
|
279
|
+
// Only supported on macOS/Linux (lsof)
|
|
280
|
+
if (process.platform === 'win32')
|
|
281
|
+
return 0;
|
|
282
|
+
let cleaned = 0;
|
|
283
|
+
const myPid = process.pid;
|
|
284
|
+
const ports = getPortRange(preferredPort);
|
|
285
|
+
// Collect PIDs that have valid port files (known-good servers)
|
|
286
|
+
const knownPids = new Set();
|
|
287
|
+
for (const port of ports) {
|
|
288
|
+
const data = readPortFile(port);
|
|
289
|
+
if (data)
|
|
290
|
+
knownPids.add(data.pid);
|
|
291
|
+
}
|
|
292
|
+
knownPids.add(myPid); // Never kill ourselves
|
|
293
|
+
for (const port of ports) {
|
|
294
|
+
try {
|
|
295
|
+
// Find PIDs listening on this port via lsof
|
|
296
|
+
const { execSync } = require('child_process');
|
|
297
|
+
const output = execSync(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null`, {
|
|
298
|
+
encoding: 'utf-8',
|
|
299
|
+
timeout: 3000,
|
|
300
|
+
}).trim();
|
|
301
|
+
if (!output)
|
|
302
|
+
continue;
|
|
303
|
+
const pids = output.split('\n').map(Number).filter(Boolean);
|
|
304
|
+
for (const pid of pids) {
|
|
305
|
+
if (knownPids.has(pid))
|
|
306
|
+
continue; // Skip known-good servers
|
|
307
|
+
// Verify this is actually a figma-console-mcp process before killing
|
|
308
|
+
try {
|
|
309
|
+
const cmdline = execSync(`ps -p ${pid} -o command= 2>/dev/null`, {
|
|
310
|
+
encoding: 'utf-8',
|
|
311
|
+
timeout: 2000,
|
|
312
|
+
}).trim();
|
|
313
|
+
if (cmdline.includes('figma-console-mcp') || cmdline.includes('figma_console_mcp') || cmdline.includes('local.js')) {
|
|
314
|
+
logger.info({ port, pid, command: cmdline.substring(0, 120) }, 'Terminating orphaned MCP server (no port file, holding port)');
|
|
315
|
+
terminateProcess(pid);
|
|
316
|
+
cleaned++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Can't read process info ā skip to be safe
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// lsof failed for this port ā skip
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (cleaned > 0) {
|
|
329
|
+
// Give terminated processes a moment to release their ports
|
|
330
|
+
try {
|
|
331
|
+
const { execSync } = require('child_process');
|
|
332
|
+
execSync('sleep 0.5', { timeout: 2000 });
|
|
333
|
+
}
|
|
334
|
+
catch { /* non-critical */ }
|
|
335
|
+
logger.info({ cleaned }, `Cleaned up ${cleaned} orphaned MCP server process(es)`);
|
|
336
|
+
}
|
|
337
|
+
return cleaned;
|
|
338
|
+
}
|
|
268
339
|
/**
|
|
269
340
|
* Register process exit handlers to clean up port advertisement file.
|
|
270
341
|
* Should be called once after the port is successfully bound.
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { WebSocketServer as WSServer, WebSocket } from 'ws';
|
|
16
16
|
import { EventEmitter } from 'events';
|
|
17
|
-
import {
|
|
17
|
+
import { createServer as createHttpServer } from 'http';
|
|
18
|
+
import { readFileSync, existsSync } from 'fs';
|
|
18
19
|
import { join } from 'path';
|
|
19
20
|
import { createChildLogger } from './logger.js';
|
|
20
21
|
// Read version from package.json
|
|
@@ -27,11 +28,37 @@ try {
|
|
|
27
28
|
catch {
|
|
28
29
|
// Non-critical ā version will show as 0.0.0
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Load the full plugin UI HTML content that gets served to the bootloader.
|
|
33
|
+
* Falls back to a minimal error page if the file isn't found.
|
|
34
|
+
*/
|
|
35
|
+
function loadPluginUIContent() {
|
|
36
|
+
const candidates = [
|
|
37
|
+
// ESM runtime: dist/core/ ā ../../figma-desktop-bridge/
|
|
38
|
+
typeof __dirname !== 'undefined'
|
|
39
|
+
? join(__dirname, '..', '..', 'figma-desktop-bridge', 'ui-full.html')
|
|
40
|
+
: join(process.cwd(), 'figma-desktop-bridge', 'ui-full.html'),
|
|
41
|
+
// Direct from project root
|
|
42
|
+
join(process.cwd(), 'figma-desktop-bridge', 'ui-full.html'),
|
|
43
|
+
];
|
|
44
|
+
for (const path of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
if (existsSync(path)) {
|
|
47
|
+
return readFileSync(path, 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// try next candidate
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return '<html><body><p>Plugin UI not found. Please reinstall figma-console-mcp.</p></body></html>';
|
|
55
|
+
}
|
|
30
56
|
const logger = createChildLogger({ component: 'websocket-server' });
|
|
31
57
|
export class FigmaWebSocketServer extends EventEmitter {
|
|
32
58
|
constructor(options) {
|
|
33
59
|
super();
|
|
34
60
|
this.wss = null;
|
|
61
|
+
this.httpServer = null;
|
|
35
62
|
/** Named clients indexed by fileKey ā each represents a connected Figma file */
|
|
36
63
|
this.clients = new Map();
|
|
37
64
|
/** Clients awaiting FILE_INFO identification, mapped to their pending timeout */
|
|
@@ -44,21 +71,75 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
44
71
|
this._startedAt = Date.now();
|
|
45
72
|
this.consoleBufferSize = 1000;
|
|
46
73
|
this.documentChangeBufferSize = 200;
|
|
74
|
+
/** Cached plugin UI HTML content ā loaded once and served to bootloader requests */
|
|
75
|
+
this._pluginUIContent = null;
|
|
47
76
|
this.options = options;
|
|
48
77
|
this._startedAt = Date.now();
|
|
49
78
|
}
|
|
50
79
|
/**
|
|
51
|
-
*
|
|
80
|
+
* Handle HTTP requests on the same port as WebSocket.
|
|
81
|
+
* Serves plugin UI content for the bootloader and health checks.
|
|
82
|
+
*/
|
|
83
|
+
handleHttpRequest(req, res) {
|
|
84
|
+
// CORS headers for Figma plugin iframe (sandboxed, origin: null)
|
|
85
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
86
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
87
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
88
|
+
if (req.method === 'OPTIONS') {
|
|
89
|
+
res.writeHead(204);
|
|
90
|
+
res.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const url = req.url || '/';
|
|
94
|
+
// Plugin UI endpoint ā bootloader redirects here
|
|
95
|
+
if (url === '/plugin/ui' || url === '/plugin/ui/') {
|
|
96
|
+
if (!this._pluginUIContent) {
|
|
97
|
+
this._pluginUIContent = loadPluginUIContent();
|
|
98
|
+
}
|
|
99
|
+
res.writeHead(200, {
|
|
100
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
101
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
102
|
+
});
|
|
103
|
+
res.end(this._pluginUIContent);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Health/version endpoint
|
|
107
|
+
if (url === '/health' || url === '/') {
|
|
108
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
109
|
+
res.end(JSON.stringify({
|
|
110
|
+
status: 'ok',
|
|
111
|
+
version: SERVER_VERSION,
|
|
112
|
+
clients: this.clients.size,
|
|
113
|
+
uptime: Math.floor((Date.now() - this._startedAt) / 1000),
|
|
114
|
+
}));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// 404 for anything else
|
|
118
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
119
|
+
res.end('Not Found');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start the HTTP + WebSocket server.
|
|
123
|
+
* HTTP serves the plugin UI content; WebSocket handles plugin communication.
|
|
52
124
|
*/
|
|
53
125
|
async start() {
|
|
54
126
|
if (this._isStarted)
|
|
55
127
|
return;
|
|
56
128
|
return new Promise((resolve, reject) => {
|
|
129
|
+
let rejected = false;
|
|
130
|
+
const rejectOnce = (error) => {
|
|
131
|
+
if (!rejected) {
|
|
132
|
+
rejected = true;
|
|
133
|
+
reject(error);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
57
136
|
try {
|
|
137
|
+
// Create HTTP server first ā handles plugin UI requests
|
|
138
|
+
this.httpServer = createHttpServer((req, res) => this.handleHttpRequest(req, res));
|
|
139
|
+
// Attach WebSocket server to the HTTP server (shares the same port)
|
|
58
140
|
this.wss = new WSServer({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
maxPayload: 25 * 1024 * 1024, // 25MB ā sufficient for screenshots; set FIGMA_WS_MAX_PAYLOAD_MB env var to override
|
|
141
|
+
server: this.httpServer,
|
|
142
|
+
maxPayload: 100 * 1024 * 1024, // 100MB ā screenshots and large component data can be big
|
|
62
143
|
verifyClient: (info, callback) => {
|
|
63
144
|
// Mitigate Cross-Site WebSocket Hijacking (CSWSH):
|
|
64
145
|
// Reject connections from unexpected browser origins.
|
|
@@ -76,18 +157,38 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
76
157
|
}
|
|
77
158
|
},
|
|
78
159
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
84
|
-
this.wss.on('error', (error) => {
|
|
160
|
+
// Error handler for startup failures (EADDRINUSE, etc.)
|
|
161
|
+
// Must be on BOTH httpServer and wss ā the WSS re-emits HTTP server errors
|
|
162
|
+
// and throws if no listener is attached.
|
|
163
|
+
const onStartupError = (error) => {
|
|
85
164
|
if (!this._isStarted) {
|
|
86
|
-
|
|
165
|
+
try {
|
|
166
|
+
if (this.wss) {
|
|
167
|
+
this.wss.close();
|
|
168
|
+
this.wss = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch { /* ignore */ }
|
|
172
|
+
try {
|
|
173
|
+
if (this.httpServer) {
|
|
174
|
+
this.httpServer.close();
|
|
175
|
+
this.httpServer = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch { /* ignore */ }
|
|
179
|
+
rejectOnce(error);
|
|
87
180
|
}
|
|
88
181
|
else {
|
|
89
|
-
logger.error({ error }, 'WebSocket server error');
|
|
182
|
+
logger.error({ error }, 'HTTP/WebSocket server error');
|
|
90
183
|
}
|
|
184
|
+
};
|
|
185
|
+
this.httpServer.on('error', onStartupError);
|
|
186
|
+
this.wss.on('error', onStartupError);
|
|
187
|
+
// Start listening on the HTTP server (which also handles WS upgrades)
|
|
188
|
+
this.httpServer.listen(this.options.port, this.options.host || 'localhost', () => {
|
|
189
|
+
this._isStarted = true;
|
|
190
|
+
logger.info({ port: this.options.port, host: this.options.host || 'localhost' }, 'WebSocket bridge server started (with HTTP plugin UI endpoint)');
|
|
191
|
+
resolve();
|
|
91
192
|
});
|
|
92
193
|
this.wss.on('connection', (ws) => {
|
|
93
194
|
// Add to pending until FILE_INFO identifies the file
|
|
@@ -146,7 +247,7 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
146
247
|
});
|
|
147
248
|
}
|
|
148
249
|
catch (error) {
|
|
149
|
-
|
|
250
|
+
rejectOnce(error);
|
|
150
251
|
}
|
|
151
252
|
});
|
|
152
253
|
}
|
|
@@ -177,6 +278,22 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
177
278
|
}
|
|
178
279
|
return;
|
|
179
280
|
}
|
|
281
|
+
// Bootloader request: send the full plugin UI HTML
|
|
282
|
+
if (message.type === 'GET_PLUGIN_UI') {
|
|
283
|
+
if (!this._pluginUIContent) {
|
|
284
|
+
this._pluginUIContent = loadPluginUIContent();
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
ws.send(JSON.stringify({
|
|
288
|
+
type: 'PLUGIN_UI_CONTENT',
|
|
289
|
+
html: this._pluginUIContent,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Non-critical ā bootloader will show error
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
180
297
|
// Unsolicited data from plugin (FILE_INFO, events, forwarded data)
|
|
181
298
|
if (message.type) {
|
|
182
299
|
// FILE_INFO promotes pending clients to named clients
|
|
@@ -434,11 +551,19 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
434
551
|
* Returns the actual port ā critical when using port 0 for OS-assigned ports.
|
|
435
552
|
*/
|
|
436
553
|
address() {
|
|
554
|
+
// Use the HTTP server's address (which is the actual listening socket)
|
|
555
|
+
if (this.httpServer) {
|
|
556
|
+
const addr = this.httpServer.address();
|
|
557
|
+
if (typeof addr === 'string' || !addr)
|
|
558
|
+
return null;
|
|
559
|
+
return addr;
|
|
560
|
+
}
|
|
561
|
+
// Fallback for backward compat
|
|
437
562
|
if (!this.wss)
|
|
438
563
|
return null;
|
|
439
564
|
const addr = this.wss.address();
|
|
440
565
|
if (typeof addr === 'string')
|
|
441
|
-
return null;
|
|
566
|
+
return null;
|
|
442
567
|
return addr;
|
|
443
568
|
}
|
|
444
569
|
// ============================================================================
|
|
@@ -632,15 +757,20 @@ export class FigmaWebSocketServer extends EventEmitter {
|
|
|
632
757
|
}
|
|
633
758
|
this.clients.clear();
|
|
634
759
|
this._activeFileKey = null;
|
|
760
|
+
// Close WS server first (handles WebSocket connections)
|
|
635
761
|
if (this.wss) {
|
|
636
|
-
|
|
637
|
-
this.wss.close(() =>
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
762
|
+
await new Promise((resolve) => {
|
|
763
|
+
this.wss.close(() => resolve());
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// Then close HTTP server (releases the port)
|
|
767
|
+
if (this.httpServer) {
|
|
768
|
+
await new Promise((resolve) => {
|
|
769
|
+
this.httpServer.close(() => resolve());
|
|
642
770
|
});
|
|
771
|
+
this.httpServer = null;
|
|
643
772
|
}
|
|
644
773
|
this._isStarted = false;
|
|
774
|
+
logger.info('WebSocket bridge server stopped');
|
|
645
775
|
}
|
|
646
776
|
}
|
|
@@ -17,7 +17,15 @@ export function registerWriteTools(server, getDesktopConnector) {
|
|
|
17
17
|
|
|
18
18
|
**VALIDATION:** After creating/modifying visuals: screenshot with figma_capture_screenshot, check alignment/spacing/proportions, iterate up to 3x.
|
|
19
19
|
|
|
20
|
-
**PLACEMENT:** Always create components inside a Section or Frame, never on blank canvas. Use parent.insertChild(0, bg) for z-ordering backgrounds behind content
|
|
20
|
+
**PLACEMENT:** Always create components inside a Section or Frame, never on blank canvas. Use parent.insertChild(0, bg) for z-ordering backgrounds behind content.
|
|
21
|
+
|
|
22
|
+
**HOUSEKEEPING (MANDATORY):**
|
|
23
|
+
Before creating: screenshot the target page to see existing content and find clear space.
|
|
24
|
+
When creating: place inside a named Section, positioned BELOW or AWAY from existing content. Never overlap.
|
|
25
|
+
After creating: screenshot to verify clean placement and no overlaps.
|
|
26
|
+
On failure/retry: DELETE any partial artifacts (empty frames, orphaned layers, blank pages) before retrying. Use node.remove() to clean up.
|
|
27
|
+
Pages: NEVER create a new page if one with that name already exists ā use the existing one. If you created a blank page during a failed attempt, delete it.
|
|
28
|
+
Layers: If your code creates helper frames, placeholder nodes, or intermediate layers that aren't part of the final result, remove them.`, {
|
|
21
29
|
code: z
|
|
22
30
|
.string()
|
|
23
31
|
.describe("JavaScript code to execute. Has access to the 'figma' global object. " +
|
|
@@ -1476,7 +1484,7 @@ After instantiating components, use figma_take_screenshot to verify the result l
|
|
|
1476
1484
|
}
|
|
1477
1485
|
});
|
|
1478
1486
|
// Tool: Create Child Node
|
|
1479
|
-
server.tool("figma_create_child", "Create a new child node inside a parent container.
|
|
1487
|
+
server.tool("figma_create_child", "Create a new child node inside a parent container. Always place inside an existing Section or Frame ā never on a bare page. If no suitable parent exists, create a Section first. Clean up any empty or orphaned nodes if the operation fails.", {
|
|
1480
1488
|
parentId: z.string().describe("The parent node ID"),
|
|
1481
1489
|
nodeType: z
|
|
1482
1490
|
.enum(["RECTANGLE", "ELLIPSE", "FRAME", "TEXT", "LINE"])
|