@mp3wizard/figma-console-mcp 1.20.1 → 1.21.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.
Files changed (36) hide show
  1. package/README.md +9 -9
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +3 -0
  3. package/dist/cloudflare/core/figjam-tools.js +91 -11
  4. package/dist/cloudflare/core/figma-api.js +1 -10
  5. package/dist/cloudflare/core/figma-desktop-connector.js +1 -0
  6. package/dist/cloudflare/core/websocket-connector.js +3 -0
  7. package/dist/cloudflare/core/websocket-server.js +61 -3
  8. package/dist/cloudflare/index.js +8 -10
  9. package/dist/core/figjam-tools.d.ts.map +1 -1
  10. package/dist/core/figjam-tools.js +91 -11
  11. package/dist/core/figjam-tools.js.map +1 -1
  12. package/dist/core/figma-api.d.ts.map +1 -1
  13. package/dist/core/figma-api.js +1 -10
  14. package/dist/core/figma-api.js.map +1 -1
  15. package/dist/core/figma-connector.d.ts +16 -0
  16. package/dist/core/figma-connector.d.ts.map +1 -1
  17. package/dist/core/figma-desktop-connector.d.ts +1 -0
  18. package/dist/core/figma-desktop-connector.d.ts.map +1 -1
  19. package/dist/core/figma-desktop-connector.js +1 -0
  20. package/dist/core/figma-desktop-connector.js.map +1 -1
  21. package/dist/core/websocket-connector.d.ts +16 -0
  22. package/dist/core/websocket-connector.d.ts.map +1 -1
  23. package/dist/core/websocket-connector.js +3 -0
  24. package/dist/core/websocket-connector.js.map +1 -1
  25. package/dist/core/websocket-server.d.ts +18 -1
  26. package/dist/core/websocket-server.d.ts.map +1 -1
  27. package/dist/core/websocket-server.js +61 -3
  28. package/dist/core/websocket-server.js.map +1 -1
  29. package/dist/local.d.ts +13 -0
  30. package/dist/local.d.ts.map +1 -1
  31. package/dist/local.js +129 -19
  32. package/dist/local.js.map +1 -1
  33. package/figma-desktop-bridge/code.js +94 -7
  34. package/figma-desktop-bridge/ui-full.html +24 -0
  35. package/figma-desktop-bridge/ui.html +8 -2
  36. package/package.json +103 -2
package/README.md CHANGED
@@ -54,9 +54,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
54
54
  | Real-time monitoring (console, selection) | ✅ | ❌ | ❌ |
55
55
  | Desktop Bridge plugin | ✅ | ✅ | ❌ |
56
56
  | Requires Node.js | Yes | **No** | No |
57
- | **Total tools available** | **91+** | **43** | **22** |
57
+ | **Total tools available** | **92+** | **43** | **22** |
58
58
 
59
- > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 91+ tools with real-time monitoring.
59
+ > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 92+ tools with real-time monitoring.
60
60
 
61
61
  ---
62
62
 
@@ -64,7 +64,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
64
64
 
65
65
  **Best for:** Designers who want full AI-assisted design capabilities.
66
66
 
67
- **What you get:** All 91+ tools including design creation, variable management, and component instantiation.
67
+ **What you get:** All 92+ tools including design creation, variable management, and component instantiation.
68
68
 
69
69
  #### Prerequisites
70
70
 
@@ -159,7 +159,7 @@ Create a simple frame with a blue background
159
159
 
160
160
  **Best for:** Developers who want to modify source code or contribute to the project.
161
161
 
162
- **What you get:** Same 91+ tools as NPX, plus full source code access.
162
+ **What you get:** Same 92+ tools as NPX, plus full source code access.
163
163
 
164
164
  #### Quick Setup
165
165
 
@@ -248,7 +248,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
248
248
 
249
249
  **Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs — no Node.js required.
250
250
 
251
- **What you get:** 81 tools including full write access — design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
251
+ **What you get:** 82 tools including full write access — design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
252
252
 
253
253
  #### Prerequisites
254
254
 
@@ -305,7 +305,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
305
305
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
306
306
  |---------|-------------------|------------|-----------|------------|
307
307
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
308
- | **Total tools** | **91+** | **43** | **91+** | **22** (read-only) |
308
+ | **Total tools** | **92+** | **43** | **92+** | **22** (read-only) |
309
309
  | **Design creation** | ✅ | ✅ | ✅ | ❌ |
310
310
  | **Variable management** | ✅ | ✅ | ✅ | ❌ |
311
311
  | **Component instantiation** | ✅ | ✅ | ✅ | ❌ |
@@ -320,7 +320,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
320
320
  | **Automatic updates** | ✅ (`@latest`) | ✅ | Manual (`git pull`) | ✅ |
321
321
  | **Source code access** | ❌ | ❌ | ✅ | ❌ |
322
322
 
323
- > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 91+ tools.
323
+ > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 92+ tools.
324
324
 
325
325
  **📖 [Complete Feature Comparison](docs/mode-comparison.md)**
326
326
 
@@ -654,7 +654,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
654
654
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
655
655
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
656
656
  - The plugin scans all ports in the range and connects to every active server it finds
657
- - All 91+ tools work through the WebSocket transport
657
+ - All 92+ tools work through the WebSocket transport
658
658
 
659
659
  **Multiple files:** The WebSocket server supports multiple simultaneous plugin connections — one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs).
660
660
 
@@ -791,7 +791,7 @@ The architecture supports adding new apps with minimal boilerplate — each app
791
791
 
792
792
  ## 🛤️ Roadmap
793
793
 
794
- **Current Status:** v1.17.0 (Stable) - Production-ready with FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, 91+ tools, Comments API, and MCP Apps
794
+ **Current Status:** v1.17.0 (Stable) - Production-ready with FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, 92+ tools, Comments API, and MCP Apps
795
795
 
796
796
  **Recent Releases:**
797
797
  - [x] **v1.17.0** - Figma Slides Support: 15 new tools for managing presentations — slides, transitions, content, reordering, and navigation. Inspired by Toni Haidamous (PR #11).
@@ -276,6 +276,9 @@ export class CloudWebSocketConnector {
276
276
  async createShapeWithText(params) {
277
277
  return this.sendCommand('CREATE_SHAPE_WITH_TEXT', params);
278
278
  }
279
+ async createSection(params) {
280
+ return this.sendCommand('CREATE_SECTION', params);
281
+ }
279
282
  async createTable(params) {
280
283
  return this.sendCommand('CREATE_TABLE', params, 30000);
281
284
  }
@@ -138,7 +138,9 @@ export function registerFigJamTools(server, getDesktopConnector) {
138
138
  // ============================================================================
139
139
  server.tool("figjam_create_connector", `Connect two nodes with a connector line in FigJam. Use to create flowcharts, diagrams, and relationship maps.
140
140
 
141
- Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from creation results.`, {
141
+ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from creation results.
142
+
143
+ **Magnet positions:** AUTO (default), TOP, BOTTOM, LEFT, RIGHT — controls where the connector attaches to each node.`, {
142
144
  startNodeId: z.string().describe("Node ID of the start element"),
143
145
  endNodeId: z.string().describe("Node ID of the end element"),
144
146
  label: z
@@ -146,13 +148,25 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
146
148
  .max(MAX_TEXT_LENGTH)
147
149
  .optional()
148
150
  .describe("Optional text label on the connector"),
149
- }, async ({ startNodeId, endNodeId, label }) => {
151
+ startMagnet: z
152
+ .enum(["AUTO", "TOP", "BOTTOM", "LEFT", "RIGHT"])
153
+ .optional()
154
+ .default("AUTO")
155
+ .describe("Magnet position on the start node"),
156
+ endMagnet: z
157
+ .enum(["AUTO", "TOP", "BOTTOM", "LEFT", "RIGHT"])
158
+ .optional()
159
+ .default("AUTO")
160
+ .describe("Magnet position on the end node"),
161
+ }, async ({ startNodeId, endNodeId, label, startMagnet, endMagnet }) => {
150
162
  try {
151
163
  const connector = await getDesktopConnector();
152
164
  const result = await connector.createConnector({
153
165
  startNodeId,
154
166
  endNodeId,
155
167
  label,
168
+ startMagnet,
169
+ endMagnet,
156
170
  });
157
171
  return {
158
172
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -177,7 +191,7 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
177
191
  // ============================================================================
178
192
  // SHAPE WITH TEXT TOOL
179
193
  // ============================================================================
180
- server.tool("figjam_create_shape_with_text", `Create a labeled shape on a FigJam board. Use for flowchart nodes, process diagrams, and visual organization.
194
+ server.tool("figjam_create_shape_with_text", `Create a labeled shape on a FigJam board with optional size, colors, and font control. Use for flowchart nodes, process diagrams, and visual organization.
181
195
 
182
196
  **Shape types:** ROUNDED_RECTANGLE (default), DIAMOND, ELLIPSE, TRIANGLE_UP, TRIANGLE_DOWN, PARALLELOGRAM_RIGHT, PARALLELOGRAM_LEFT, ENG_DATABASE, ENG_QUEUE, ENG_FILE, ENG_FOLDER`, {
183
197
  text: z
@@ -191,7 +205,13 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
191
205
  .describe("Shape type"),
192
206
  x: z.number().optional().describe("X position on canvas"),
193
207
  y: z.number().optional().describe("Y position on canvas"),
194
- }, async ({ text, shapeType, x, y }) => {
208
+ width: z.number().min(1).max(10000).optional().describe("Width in pixels"),
209
+ height: z.number().min(1).max(10000).optional().describe("Height in pixels"),
210
+ fillColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Fill color as hex (e.g., '#E1F5EE')"),
211
+ strokeColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Stroke/border color as hex"),
212
+ fontSize: z.number().min(1).max(200).optional().describe("Text font size in pixels"),
213
+ strokeDashPattern: z.string().optional().describe("Dash pattern as comma-separated numbers (e.g., '10,5' for dashed)"),
214
+ }, async ({ text, shapeType, x, y, width, height, fillColor, strokeColor, fontSize, strokeDashPattern }) => {
195
215
  try {
196
216
  const connector = await getDesktopConnector();
197
217
  const result = await connector.createShapeWithText({
@@ -199,6 +219,12 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
199
219
  shapeType,
200
220
  x,
201
221
  y,
222
+ width,
223
+ height,
224
+ fillColor,
225
+ strokeColor,
226
+ fontSize,
227
+ strokeDashPattern,
202
228
  });
203
229
  return {
204
230
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -221,6 +247,42 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
221
247
  }
222
248
  });
223
249
  // ============================================================================
250
+ // SECTION TOOL
251
+ // ============================================================================
252
+ server.tool("figjam_create_section", `Create a section on a FigJam board. Sections are containers that can hold other elements. Use for grouping related content.\n\n**Note:** After creating a section, place elements inside it by setting their x/y coordinates within the section's bounds, then use figma_execute to call section.appendChild(node) to parent them.`, {
253
+ name: z.string().max(500).optional().describe("Section name/title"),
254
+ x: z.number().optional().describe("X position on canvas"),
255
+ y: z.number().optional().describe("Y position on canvas"),
256
+ width: z.number().min(1).max(20000).optional().default(1000).describe("Section width in pixels"),
257
+ height: z.number().min(1).max(20000).optional().default(800).describe("Section height in pixels"),
258
+ fillColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Fill color as hex"),
259
+ }, async ({ name, x, y, width, height, fillColor }) => {
260
+ try {
261
+ const connector = await getDesktopConnector();
262
+ const result = await connector.createSection({
263
+ name, x, y, width, height, fillColor,
264
+ });
265
+ return {
266
+ content: [{ type: "text", text: JSON.stringify(result) }],
267
+ };
268
+ }
269
+ catch (error) {
270
+ logger.error({ error }, "figjam_create_section failed");
271
+ return {
272
+ content: [
273
+ {
274
+ type: "text",
275
+ text: JSON.stringify({
276
+ error: error instanceof Error ? error.message : String(error),
277
+ hint: "This tool only works in FigJam files.",
278
+ }),
279
+ },
280
+ ],
281
+ isError: true,
282
+ };
283
+ }
284
+ });
285
+ // ============================================================================
224
286
  // TABLE TOOL
225
287
  // ============================================================================
226
288
  server.tool("figjam_create_table", `Create a table on a FigJam board with optional cell data. Use for structured data display, comparison matrices, and organized information.
@@ -278,9 +340,24 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
278
340
  server.tool("figjam_create_code_block", `Create a code block on a FigJam board. Use for sharing code snippets, config examples, or technical documentation in collaborative boards.`, {
279
341
  code: z.string().max(MAX_CODE_LENGTH).describe("The code content"),
280
342
  language: z
281
- .string()
343
+ .enum([
344
+ "TYPESCRIPT",
345
+ "JAVASCRIPT",
346
+ "HTML",
347
+ "CSS",
348
+ "JSON",
349
+ "PYTHON",
350
+ "RUBY",
351
+ "COFFEESCRIPT",
352
+ "SWIFT",
353
+ "KOTLIN",
354
+ "DART",
355
+ "BASH",
356
+ "SQL",
357
+ "PLAIN_TEXT",
358
+ ])
282
359
  .optional()
283
- .describe("Programming language (e.g., 'JAVASCRIPT', 'PYTHON', 'TYPESCRIPT', 'JSON', 'HTML', 'CSS')"),
360
+ .describe("Programming language for syntax highlighting. Must be one of the supported Figma CodeLanguage values."),
284
361
  x: z.number().optional().describe("X position on canvas"),
285
362
  y: z.number().optional().describe("Y position on canvas"),
286
363
  }, async ({ code, language, x, y }) => {
@@ -339,17 +416,20 @@ Nodes must exist on the board (stickies, shapes, etc.). Use their node IDs from
339
416
  const connector = await getDesktopConnector();
340
417
  // Compute grid columns safely on the server side — no string interpolation
341
418
  const gridCols = columns || Math.ceil(Math.sqrt(nodeIds.length));
342
- // Pass all parameters as a JSON object to avoid code injection.
343
- // The plugin code reads from the params object, not interpolated strings.
419
+ // SECURITY CONTRACT: All user-controlled values (nodeIds, layout, spacing,
420
+ // gridCols) MUST be passed through this JSON round-trip never interpolated
421
+ // directly into the code template string. JSON.stringify produces a
422
+ // properly-escaped JS string literal that handles all control characters,
423
+ // including \u2028/\u2029 line terminators that break naive string escaping.
424
+ // Any future addition of parameters MUST follow this same pattern.
344
425
  const paramsJson = JSON.stringify({
345
426
  nodeIds,
346
427
  layout,
347
428
  spacing,
348
429
  gridCols,
349
430
  });
350
- // Use JSON.stringify to produce a properly-escaped double-quoted JS string literal.
351
- // This handles all control characters including \u2028/\u2029 that manual
352
- // single-quote escaping would miss.
431
+ // Double-JSON-encode: paramsJson is a string; wrapping it in JSON.stringify
432
+ // again embeds it as a quoted, fully-escaped JS literal inside the code template.
353
433
  const code = `
354
434
  const params = JSON.parse(${JSON.stringify(paramsJson)});
355
435
  const nodes = [];
@@ -97,16 +97,7 @@ export class FigmaAPI {
97
97
  // OAuth tokens start with 'figu_' and require Authorization: Bearer header
98
98
  // Personal Access Tokens use X-Figma-Token header
99
99
  const isOAuthToken = this.accessToken.startsWith('figu_');
100
- // Debug logging to verify token is being used
101
- const tokenPreview = this.accessToken ? `${this.accessToken.substring(0, 10)}...` : 'NO TOKEN';
102
- logger.info({
103
- url,
104
- tokenPreview,
105
- hasToken: !!this.accessToken,
106
- tokenLength: this.accessToken?.length,
107
- isOAuthToken,
108
- authMethod: isOAuthToken ? 'Bearer' : 'X-Figma-Token'
109
- }, 'Making Figma API request with token');
100
+ logger.debug({ url, authMethod: isOAuthToken ? 'Bearer' : 'X-Figma-Token' }, 'Making Figma API request');
110
101
  const headers = {
111
102
  'Content-Type': 'application/json',
112
103
  ...(options.headers || {}),
@@ -1267,6 +1267,7 @@ export class FigmaDesktopConnector {
1267
1267
  async createStickies() { throw new Error('FigJam operations require WebSocket transport'); }
1268
1268
  async createConnector() { throw new Error('FigJam operations require WebSocket transport'); }
1269
1269
  async createShapeWithText() { throw new Error('FigJam operations require WebSocket transport'); }
1270
+ async createSection() { throw new Error('FigJam operations require WebSocket transport'); }
1270
1271
  async createTable() { throw new Error('FigJam operations require WebSocket transport'); }
1271
1272
  async createCodeBlock() { throw new Error('FigJam operations require WebSocket transport'); }
1272
1273
  async getBoardContents() { throw new Error('FigJam operations require WebSocket transport'); }
@@ -280,6 +280,9 @@ export class WebSocketConnector {
280
280
  async createShapeWithText(params) {
281
281
  return this.wsServer.sendCommand('CREATE_SHAPE_WITH_TEXT', params);
282
282
  }
283
+ async createSection(params) {
284
+ return this.wsServer.sendCommand('CREATE_SECTION', params);
285
+ }
283
286
  async createTable(params) {
284
287
  return this.wsServer.sendCommand('CREATE_TABLE', params, 30000);
285
288
  }
@@ -71,6 +71,8 @@ export class FigmaWebSocketServer extends EventEmitter {
71
71
  this.documentChangeBufferSize = 200;
72
72
  /** Cached plugin UI HTML content — loaded once and served to bootloader requests */
73
73
  this._pluginUIContent = null;
74
+ /** Heartbeat interval for detecting dead connections via ping/pong */
75
+ this._heartbeatInterval = null;
74
76
  this.options = options;
75
77
  this._startedAt = Date.now();
76
78
  }
@@ -103,12 +105,15 @@ export class FigmaWebSocketServer extends EventEmitter {
103
105
  }
104
106
  // Health/version endpoint
105
107
  if (url === '/health' || url === '/') {
108
+ const now = Date.now();
109
+ const connectedClients = Array.from(this.clients.values()).filter(c => c.ws.readyState === WebSocket.OPEN && (now - c.lastPongAt) < 90000).length;
106
110
  res.writeHead(200, { 'Content-Type': 'application/json' });
107
111
  res.end(JSON.stringify({
108
112
  status: 'ok',
109
113
  version: SERVER_VERSION,
110
114
  clients: this.clients.size,
111
- uptime: Math.floor((Date.now() - this._startedAt) / 1000),
115
+ connectedClients,
116
+ uptime: Math.floor((now - this._startedAt) / 1000),
112
117
  }));
113
118
  return;
114
119
  }
@@ -185,6 +190,7 @@ export class FigmaWebSocketServer extends EventEmitter {
185
190
  // Start listening on the HTTP server (which also handles WS upgrades)
186
191
  this.httpServer.listen(this.options.port, this.options.host || 'localhost', () => {
187
192
  this._isStarted = true;
193
+ this.startHeartbeat();
188
194
  logger.info({ port: this.options.port, host: this.options.host || 'localhost' }, 'WebSocket bridge server started (with HTTP plugin UI endpoint)');
189
195
  resolve();
190
196
  });
@@ -242,6 +248,18 @@ export class FigmaWebSocketServer extends EventEmitter {
242
248
  ws.on('error', (error) => {
243
249
  logger.error({ error }, 'WebSocket client error');
244
250
  });
251
+ // Track pong responses for heartbeat-based liveness detection.
252
+ // Browser WebSocket clients auto-respond to pings per RFC 6455 —
253
+ // no plugin-side code changes needed.
254
+ ws.isAlive = true;
255
+ ws.on('pong', () => {
256
+ ws.isAlive = true;
257
+ // Update lastPongAt on the named client if identified
258
+ const found = this.findClientByWs(ws);
259
+ if (found) {
260
+ found.client.lastPongAt = Date.now();
261
+ }
262
+ });
245
263
  });
246
264
  }
247
265
  catch (error) {
@@ -420,6 +438,7 @@ export class FigmaWebSocketServer extends EventEmitter {
420
438
  documentChanges: existing?.documentChanges || [],
421
439
  consoleLogs: existing?.consoleLogs || [],
422
440
  lastActivity: Date.now(),
441
+ lastPongAt: Date.now(),
423
442
  gracePeriodTimer: null,
424
443
  });
425
444
  // Most recently connected file becomes active (user just opened the plugin there).
@@ -528,11 +547,35 @@ export class FigmaWebSocketServer extends EventEmitter {
528
547
  });
529
548
  }
530
549
  /**
531
- * Check if any named client is connected (transport availability check)
550
+ * Start the heartbeat interval that pings all connected clients every 30s.
551
+ * Detects silently dropped connections (e.g., macOS sleep, network change)
552
+ * that the OS TCP keepalive would take 30-120s to catch.
553
+ * Browser WebSocket clients auto-respond to pings per RFC 6455.
554
+ */
555
+ startHeartbeat() {
556
+ this._heartbeatInterval = setInterval(() => {
557
+ if (!this.wss)
558
+ return;
559
+ for (const ws of this.wss.clients) {
560
+ if (ws.isAlive === false) {
561
+ logger.info('Terminating unresponsive WebSocket client (missed heartbeat pong)');
562
+ ws.terminate();
563
+ continue;
564
+ }
565
+ ws.isAlive = false;
566
+ ws.ping();
567
+ }
568
+ }, 30000);
569
+ }
570
+ /**
571
+ * Check if any named client is connected (transport availability check).
572
+ * Checks both socket readyState and heartbeat pong freshness to avoid
573
+ * reporting phantom-connected state on silently dropped connections.
532
574
  */
533
575
  isClientConnected() {
576
+ const now = Date.now();
534
577
  for (const [, client] of this.clients) {
535
- if (client.ws.readyState === WebSocket.OPEN) {
578
+ if (client.ws.readyState === WebSocket.OPEN && (now - client.lastPongAt) < 90000) {
536
579
  return true;
537
580
  }
538
581
  }
@@ -687,6 +730,16 @@ export class FigmaWebSocketServer extends EventEmitter {
687
730
  }
688
731
  return files;
689
732
  }
733
+ /**
734
+ * Get the last pong timestamp for the active client.
735
+ * Returns null if no active client or no pong received yet.
736
+ */
737
+ getActiveClientLastPongAt() {
738
+ if (!this._activeFileKey)
739
+ return null;
740
+ const client = this.clients.get(this._activeFileKey);
741
+ return client?.lastPongAt ?? null;
742
+ }
690
743
  /**
691
744
  * Set the active file by fileKey. Returns true if the file is connected.
692
745
  */
@@ -745,6 +798,11 @@ export class FigmaWebSocketServer extends EventEmitter {
745
798
  * Stop the server and clean up all connections
746
799
  */
747
800
  async stop() {
801
+ // Clear heartbeat interval
802
+ if (this._heartbeatInterval) {
803
+ clearInterval(this._heartbeatInterval);
804
+ this._heartbeatInterval = null;
805
+ }
748
806
  // Clear all per-client grace period timers
749
807
  for (const [, client] of this.clients) {
750
808
  if (client.gracePeriodTimer) {
@@ -68,7 +68,7 @@ export class FigmaConsoleMCPv3 extends McpAgent {
68
68
  super(...arguments);
69
69
  this.server = new McpServer({
70
70
  name: "Figma Console MCP",
71
- version: "1.20.0",
71
+ version: "1.21.1",
72
72
  });
73
73
  this.browserManager = null;
74
74
  this.consoleMonitor = null;
@@ -1046,7 +1046,7 @@ export default {
1046
1046
  });
1047
1047
  const statelessServer = new McpServer({
1048
1048
  name: "Figma Console MCP",
1049
- version: "1.20.0",
1049
+ version: "1.21.1",
1050
1050
  });
1051
1051
  // ================================================================
1052
1052
  // Cloud Write Relay — Pairing Tool (stateless /mcp path)
@@ -1510,9 +1510,7 @@ export default {
1510
1510
  const expiresIn = tokenData.expires_in;
1511
1511
  logger.info({
1512
1512
  sessionId,
1513
- hasAccessToken: !!accessToken,
1514
- accessTokenPreview: accessToken ? accessToken.substring(0, 10) + "..." : null,
1515
- hasRefreshToken: !!refreshToken,
1513
+ hasTokens: !!accessToken && !!refreshToken,
1516
1514
  expiresIn
1517
1515
  }, "Token exchange successful");
1518
1516
  // IMPORTANT: Use KV storage for tokens since Durable Object storage is instance-specific
@@ -1683,7 +1681,7 @@ export default {
1683
1681
  return new Response(JSON.stringify({
1684
1682
  status: "healthy",
1685
1683
  service: "Figma Console MCP",
1686
- version: "1.20.0",
1684
+ version: "1.21.1",
1687
1685
  endpoints: {
1688
1686
  mcp: ["/sse", "/mcp"],
1689
1687
  oauth_mcp_spec: ["/.well-known/oauth-authorization-server", "/authorize", "/token", "/oauth/register"],
@@ -1729,13 +1727,13 @@ export default {
1729
1727
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1730
1728
  <title>Figma Console MCP - The Most Comprehensive MCP Server for Figma</title>
1731
1729
  <link rel="icon" type="image/svg+xml" href="https://docs.figma-console-mcp.southleft.com/favicon.svg">
1732
- <meta name="description" content="Turn your Figma design system into a living API. 91+ tools give AI assistants deep access to design tokens, component specs, variables, and programmatic design creation.">
1730
+ <meta name="description" content="Turn your Figma design system into a living API. 92+ tools give AI assistants deep access to design tokens, component specs, variables, and programmatic design creation.">
1733
1731
 
1734
1732
  <!-- Open Graph -->
1735
1733
  <meta property="og:type" content="website">
1736
1734
  <meta property="og:url" content="https://figma-console-mcp.southleft.com">
1737
1735
  <meta property="og:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
1738
- <meta property="og:description" content="The most comprehensive MCP server for Figma. 91+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1736
+ <meta property="og:description" content="The most comprehensive MCP server for Figma. 92+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1739
1737
  <meta property="og:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
1740
1738
  <meta property="og:image:width" content="1200">
1741
1739
  <meta property="og:image:height" content="630">
@@ -1743,7 +1741,7 @@ export default {
1743
1741
  <!-- Twitter -->
1744
1742
  <meta name="twitter:card" content="summary_large_image">
1745
1743
  <meta name="twitter:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
1746
- <meta name="twitter:description" content="The most comprehensive MCP server for Figma. 91+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1744
+ <meta name="twitter:description" content="The most comprehensive MCP server for Figma. 92+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1747
1745
  <meta name="twitter:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
1748
1746
 
1749
1747
  <meta name="theme-color" content="#0D9488">
@@ -2630,7 +2628,7 @@ export default {
2630
2628
  <div class="grid-cell showcase-cell rule-left">
2631
2629
  <div class="showcase-label">What AI Can Access</div>
2632
2630
  <div class="showcase-stat">
2633
- <span class="number">91+</span>
2631
+ <span class="number">92+</span>
2634
2632
  <span class="label">MCP tools for Figma</span>
2635
2633
  </div>
2636
2634
  <div class="capability-list">
@@ -1 +1 @@
1
- {"version":3,"file":"figjam-tools.d.ts","sourceRoot":"","sources":["../../src/core/figjam-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4DzE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,SAAS,EACjB,mBAAmB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACrC,IAAI,CA2eN"}
1
+ {"version":3,"file":"figjam-tools.d.ts","sourceRoot":"","sources":["../../src/core/figjam-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4DzE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,SAAS,EACjB,mBAAmB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACrC,IAAI,CAikBN"}