figma-console-mcp 1.15.3 → 1.15.5

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.
@@ -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 { readFileSync } from 'fs';
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,20 +71,74 @@ 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
- * Start the WebSocket server
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
- port: this.options.port,
60
- host: this.options.host || 'localhost',
141
+ server: this.httpServer,
61
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):
@@ -76,18 +157,38 @@ export class FigmaWebSocketServer extends EventEmitter {
76
157
  }
77
158
  },
78
159
  });
79
- this.wss.on('listening', () => {
80
- this._isStarted = true;
81
- logger.info({ port: this.options.port, host: this.options.host || 'localhost' }, 'WebSocket bridge server started');
82
- resolve();
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
- reject(error);
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
- reject(error);
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; // Unix socket path, not applicable
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
- return new Promise((resolve) => {
637
- this.wss.close(() => {
638
- this._isStarted = false;
639
- logger.info('WebSocket bridge server stopped');
640
- resolve();
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. Useful for adding shapes, text, or frames to existing structures.", {
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"])
@@ -38,7 +38,7 @@ export class FigmaConsoleMCPv3 extends McpAgent {
38
38
  super(...arguments);
39
39
  this.server = new McpServer({
40
40
  name: "Figma Console MCP",
41
- version: "1.13.0",
41
+ version: "1.15.5",
42
42
  });
43
43
  this.browserManager = null;
44
44
  this.consoleMonitor = null;
@@ -950,7 +950,7 @@ export default {
950
950
  });
951
951
  const statelessServer = new McpServer({
952
952
  name: "Figma Console MCP",
953
- version: "1.13.0",
953
+ version: "1.15.5",
954
954
  });
955
955
  // ================================================================
956
956
  // Cloud Write Relay — Pairing Tool (stateless /mcp path)
@@ -1579,7 +1579,7 @@ export default {
1579
1579
  return new Response(JSON.stringify({
1580
1580
  status: "healthy",
1581
1581
  service: "Figma Console MCP",
1582
- version: "1.13.0",
1582
+ version: "1.15.5",
1583
1583
  endpoints: {
1584
1584
  mcp: ["/sse", "/mcp"],
1585
1585
  oauth_mcp_spec: ["/.well-known/oauth-authorization-server", "/authorize", "/token", "/oauth/register"],
@@ -1625,13 +1625,13 @@ export default {
1625
1625
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1626
1626
  <title>Figma Console MCP - The Most Comprehensive MCP Server for Figma</title>
1627
1627
  <link rel="icon" type="image/svg+xml" href="https://docs.figma-console-mcp.southleft.com/favicon.svg">
1628
- <meta name="description" content="Turn your Figma design system into a living API. 59+ tools give AI assistants deep access to design tokens, component specs, variables, and programmatic design creation.">
1628
+ <meta name="description" content="Turn your Figma design system into a living API. 63+ tools give AI assistants deep access to design tokens, component specs, variables, and programmatic design creation.">
1629
1629
 
1630
1630
  <!-- Open Graph -->
1631
1631
  <meta property="og:type" content="website">
1632
1632
  <meta property="og:url" content="https://figma-console-mcp.southleft.com">
1633
1633
  <meta property="og:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
1634
- <meta property="og:description" content="The most comprehensive MCP server for Figma. 59+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1634
+ <meta property="og:description" content="The most comprehensive MCP server for Figma. 63+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1635
1635
  <meta property="og:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
1636
1636
  <meta property="og:image:width" content="1200">
1637
1637
  <meta property="og:image:height" content="630">
@@ -1639,7 +1639,7 @@ export default {
1639
1639
  <!-- Twitter -->
1640
1640
  <meta name="twitter:card" content="summary_large_image">
1641
1641
  <meta name="twitter:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
1642
- <meta name="twitter:description" content="The most comprehensive MCP server for Figma. 59+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1642
+ <meta name="twitter:description" content="The most comprehensive MCP server for Figma. 63+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1643
1643
  <meta name="twitter:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
1644
1644
 
1645
1645
  <meta name="theme-color" content="#0D9488">
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;GAYG;AAiFH;;;GAGG;AACH,cAAM,oBAAoB;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,cAAc,CAA+C;IACrE,uGAAuG;IACvG,OAAO,CAAC,YAAY,CAAuB;IAC3C,6DAA6D;IAC7D,OAAO,CAAC,eAAe,CAA2B;IAClD,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB,CAA+C;IACvE,OAAO,CAAC,MAAM,CAAe;IAI7B,OAAO,CAAC,cAAc,CAMR;;IA2Ed;;OAEG;YACW,WAAW;IA0BzB;;;OAGG;YACW,mBAAmB;IA6CjC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAiBzB;;OAEG;YACW,iBAAiB;IAqB/B,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAAuB;IAE/C;;;OAGG;IACH,OAAO,CAAC,aAAa;IAgBrB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;YACW,iBAAiB;IA+I/B;;OAEG;IACH,OAAO,CAAC,aAAa;IA09KrB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgK5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgC/B;AA4ED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../src/local.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;GAYG;AAiFH;;;GAGG;AACH,cAAM,oBAAoB;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,cAAc,CAA+C;IACrE,uGAAuG;IACvG,OAAO,CAAC,YAAY,CAAuB;IAC3C,6DAA6D;IAC7D,OAAO,CAAC,eAAe,CAA2B;IAClD,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB,CAA+C;IACvE,OAAO,CAAC,MAAM,CAAe;IAI7B,OAAO,CAAC,cAAc,CAMR;;IA2Ed;;OAEG;YACW,WAAW;IA0BzB;;;OAGG;YACW,mBAAmB;IA6CjC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAiBzB;;OAEG;YACW,iBAAiB;IAqB/B,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAAuB;IAE/C;;;OAGG;IACH,OAAO,CAAC,aAAa;IAgBrB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;YACW,iBAAiB;IA+I/B;;OAEG;IACH,OAAO,CAAC,aAAa;IAk/KrB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgK5B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgC/B;AA4ED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
package/dist/local.js CHANGED
@@ -1122,7 +1122,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1122
1122
  : this.wsStartupError?.code === "EADDRINUSE"
1123
1123
  ? `❌ All WebSocket ports ${this.wsPreferredPort}-${this.wsPreferredPort + 9} are in use`
1124
1124
  : this.wsActualPort !== null && this.wsActualPort !== this.wsPreferredPort
1125
- ? `❌ WebSocket server running on port ${this.wsActualPort} (fallback) but no plugin connected. Re-import the Desktop Bridge plugin in Figma to enable multi-port scanning.`
1125
+ ? `❌ WebSocket server running on port ${this.wsActualPort} (fallback) but no plugin connected. Restart the Desktop Bridge plugin in Figma to reconnect.`
1126
1126
  : "❌ No connection to Figma Desktop",
1127
1127
  setupInstructions: !setupValid
1128
1128
  ? this.wsStartupError?.code === "EADDRINUSE"
@@ -1138,7 +1138,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1138
1138
  ? this.wsStartupError?.code === "EADDRINUSE"
1139
1139
  ? `All WebSocket ports in range ${this.wsPreferredPort}-${this.wsPreferredPort + 9} are in use — most likely multiple Claude Desktop tabs or terminal sessions are running the Figma Console MCP server. Ask the user to close some sessions and restart.`
1140
1140
  : this.wsActualPort !== null && this.wsActualPort !== this.wsPreferredPort
1141
- ? `Server is running on fallback port ${this.wsActualPort} (port ${this.wsPreferredPort} was taken by another instance). The Desktop Bridge plugin is not connected — most likely because the plugin has old code that only scans port ${this.wsPreferredPort}. TELL THE USER: Re-import the Desktop Bridge plugin in Figma (Plugins → Development → Import plugin from manifest) to update it with multi-port scanning support. This is a one-time step.${this.getPluginPath() ? ' The manifest file is at: ' + this.getPluginPath() : ''}`
1141
+ ? `Server is running on fallback port ${this.wsActualPort} (port ${this.wsPreferredPort} was taken by another instance). The Desktop Bridge plugin is not connected. TELL THE USER: Close and reopen the Desktop Bridge plugin in Figma to reconnect. The plugin's bootloader will automatically scan all ports in the range.`
1142
1142
  : `No connection to Figma Desktop. Open the Desktop Bridge plugin in Figma to connect.${this.getPluginPath() ? ' Plugin manifest: ' + this.getPluginPath() : ''}`
1143
1143
  : activeTransport === "websocket"
1144
1144
  ? `Connected via WebSocket Bridge to "${currentFileName || "unknown file"}" on port ${this.wsActualPort}. All design tools and console monitoring tools are available. Console logs are captured from the plugin sandbox (code.js). IMPORTANT: Always verify the file name before destructive operations when multiple files have the plugin open.`
@@ -3114,6 +3114,11 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
3114
3114
  **For local components:** Pass BOTH componentKey AND nodeId together. Most local/unpublished components require nodeId.
3115
3115
  **For library components:** Pass just the componentKey from figma_get_library_components or figma_search_components (with libraryFileKey). The component will be imported from the published library automatically.
3116
3116
 
3117
+ **CRITICAL: Use VARIANT keys, not COMPONENT_SET keys!**
3118
+ When importing from a published library, use the key of a specific variant (type: "COMPONENT"), NOT the parent component set key (type: "COMPONENT_SET"). Component set keys will fail with importComponentByKeyAsync. In figma_get_library_components results, look inside the "variants" array for individual variant keys.
3119
+
3120
+ **Font loading:** Library components may use fonts not loaded in the current file. If instantiation fails with a font error, load the required fonts first via figma_execute before retrying (e.g., \`await figma.loadFontAsync({ family: "Geist", style: "Regular" })\`).
3121
+
3117
3122
  **IMPORTANT: Always re-search before instantiating!**
3118
3123
  NodeIds are session-specific and may be stale from previous conversations. ALWAYS search for components at the start of each design session to get current, valid identifiers.
3119
3124
 
@@ -3202,8 +3207,12 @@ After instantiating components, use figma_take_screenshot to verify the result l
3202
3207
 
3203
3208
  **WORKFLOW:**
3204
3209
  1. Call this tool with the library file's URL or file key
3205
- 2. Browse the returned components (with keys, names, variants)
3206
- 3. Use figma_instantiate_component with the componentKey to place them in the current file
3210
+ 2. Browse the returned components — results include COMPONENT_SET (with variants array) and standalone COMPONENT types
3211
+ 3. Use figma_instantiate_component with a VARIANT key (from the variants array inside a COMPONENT_SET result, NOT the component set key itself)
3212
+
3213
+ **SEARCH NOTE:** The query filter matches both component names AND descriptions. If you get unexpected results (e.g., "Accordion" when searching "Button"), verify the result name matches what you need — it may have matched on a description mention.
3214
+
3215
+ **MULTI-FILE TIP:** If you need to find a specific component and REST API search returns too many results, you can switch to the library file via figma_navigate, use figma_execute to find the exact component and its variant key, then switch back.
3207
3216
 
3208
3217
  **NOTE:** Requires FIGMA_ACCESS_TOKEN to be set (uses the Figma REST API to read the library file).`, {
3209
3218
  libraryFileUrl: z
@@ -3272,13 +3281,19 @@ After instantiating components, use figma_take_screenshot to verify the result l
3272
3281
  // Use REST API to get published components from the library file
3273
3282
  const api = await this.getFigmaAPI();
3274
3283
  // Fetch both components and component sets in parallel
3284
+ // Surface errors instead of swallowing them — token/scope issues need to be visible
3285
+ const apiErrors = [];
3275
3286
  const [componentsResponse, componentSetsResponse] = await Promise.all([
3276
3287
  api.getComponents(fileKey).catch((err) => {
3288
+ const msg = err.message || String(err);
3277
3289
  logger.warn({ error: err }, "Failed to fetch components from library");
3290
+ apiErrors.push(`Components API: ${msg}`);
3278
3291
  return { meta: { components: [] } };
3279
3292
  }),
3280
3293
  api.getComponentSets(fileKey).catch((err) => {
3294
+ const msg = err.message || String(err);
3281
3295
  logger.warn({ error: err }, "Failed to fetch component sets from library");
3296
+ apiErrors.push(`Component Sets API: ${msg}`);
3282
3297
  return { meta: { component_sets: [] } };
3283
3298
  }),
3284
3299
  ]);
@@ -3374,9 +3389,13 @@ After instantiating components, use figma_take_screenshot to verify the result l
3374
3389
  {
3375
3390
  type: "text",
3376
3391
  text: JSON.stringify({
3377
- success: true,
3392
+ success: apiErrors.length === 0,
3378
3393
  libraryFileKey: fileKey,
3379
3394
  query: query || "(all)",
3395
+ ...(apiErrors.length > 0 && {
3396
+ apiErrors,
3397
+ hint: "REST API errors occurred. Check that FIGMA_ACCESS_TOKEN is valid and has file_content:read scope. If the token is correct, the library components may not be published to a team library.",
3398
+ }),
3380
3399
  summary: {
3381
3400
  totalComponentSets: componentSets.length,
3382
3401
  totalStandaloneComponents: standaloneComponents.length,
@@ -3390,11 +3409,17 @@ After instantiating components, use figma_take_screenshot to verify the result l
3390
3409
  hasMore,
3391
3410
  },
3392
3411
  usage: {
3393
- instantiate: `To use a component: call figma_instantiate_component with the componentKey from results above.`,
3394
- example: paginatedResults[0]
3395
- ? `figma_instantiate_component({ componentKey: "${paginatedResults[0].key}" })`
3396
- : undefined,
3397
- note: "Components will be imported from the published library into the current file automatically.",
3412
+ instantiate: `To use a component: call figma_instantiate_component with a VARIANT key (not the component set key). For COMPONENT_SET results, pick a variant from the "variants" array.`,
3413
+ example: (() => {
3414
+ const first = paginatedResults[0];
3415
+ if (!first)
3416
+ return undefined;
3417
+ if (first.type === "COMPONENT_SET" && first.variants?.length > 0) {
3418
+ return `figma_instantiate_component({ componentKey: "${first.variants[0].key}" }) — using first variant of "${first.name}"`;
3419
+ }
3420
+ return `figma_instantiate_component({ componentKey: "${first.key}" })`;
3421
+ })(),
3422
+ note: "IMPORTANT: Use variant keys (type COMPONENT), not component set keys (type COMPONENT_SET). Component set keys will fail. Also pre-load any custom fonts the component uses via figma_execute before instantiating.",
3398
3423
  },
3399
3424
  }),
3400
3425
  },