@mp3wizard/figma-console-mcp 1.15.3 β 1.17.3
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 +71 -37
- 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/figjam-tools.d.ts +8 -0
- package/dist/core/figjam-tools.d.ts.map +1 -0
- package/dist/core/figjam-tools.js +486 -0
- package/dist/core/figjam-tools.js.map +1 -0
- 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/figma-connector.d.ts +97 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.d.ts +23 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.js +25 -0
- package/dist/core/figma-desktop-connector.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +90 -29
- package/dist/core/figma-tools.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/core/slides-tools.d.ts +8 -0
- package/dist/core/slides-tools.d.ts.map +1 -0
- package/dist/core/slides-tools.js +608 -0
- package/dist/core/slides-tools.js.map +1 -0
- package/dist/core/websocket-connector.d.ts +97 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +75 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts +6 -0
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +11 -0
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +80 -32
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +1103 -5
- package/figma-desktop-bridge/icon.png +0 -0
- package/figma-desktop-bridge/manifest.json +1 -1
- package/figma-desktop-bridge/ui.html +1300 -173
- package/package.json +84 -85
package/README.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# Figma Console MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://modelcontextprotocol.io/)
|
|
4
|
-
[](https://www.npmjs.com/package/figma-console-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/@mp3wizard/figma-console-mcp)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](Security%20review%20report/)
|
|
6
7
|
[](https://docs.figma-console-mcp.southleft.com)
|
|
7
8
|
[](https://github.com/sponsors/southleft)
|
|
8
9
|
|
|
9
10
|
> **Your design system as an API.** Model Context Protocol server that bridges design and developmentβgiving AI assistants complete access to Figma for **extraction**, **creation**, and **debugging**.
|
|
10
11
|
|
|
11
|
-
>
|
|
12
|
+
> **π Security Reviewed Fork:** This fork (`@mp3wizard/figma-console-mcp`) has passed a full security review following OWASP Top 10 and CWE standards, including automated scanning (Semgrep, Trivy, TruffleHog) and manual vulnerability analysis. Review reports are available in the [`Security review report/`](Security%20review%20report/) folder.
|
|
13
|
+
|
|
14
|
+
> **π FigJam + Slides β AI Across All Figma Products:** 24 new tools bring AI to FigJam boards and Figma Slides. Create stickies, flowcharts, and tables on whiteboards. Manage entire presentations β slides, transitions, content, and reordering. [FigJam Guide β](docs/figjam.md) | [Slides Guide β](docs/slides.md)
|
|
12
15
|
|
|
13
16
|
## What is this?
|
|
14
17
|
|
|
@@ -20,6 +23,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
20
23
|
- **βοΈ Design creation** - Create UI components, frames, and layouts directly in Figma
|
|
21
24
|
- **π§ Variable management** - Create, update, rename, and delete design tokens
|
|
22
25
|
- **β‘ Real-time monitoring** - Watch logs as plugins execute
|
|
26
|
+
- **π FigJam boards** - Create stickies, flowcharts, tables, and code blocks on collaborative boards
|
|
23
27
|
- **βοΈ Cloud Write Relay** - Web AI clients (Claude.ai, v0, Replit) can design in Figma via cloud pairing
|
|
24
28
|
- **π Four ways to connect** - Remote SSE, Cloud Mode, NPX, or Local Git
|
|
25
29
|
|
|
@@ -46,12 +50,13 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
46
50
|
| **Create components & frames** | β
| β
| β |
|
|
47
51
|
| **Edit existing designs** | β
| β
| β |
|
|
48
52
|
| **Manage design tokens/variables** | β
| β
| β |
|
|
53
|
+
| **FigJam boards (stickies, flowcharts)** | β
| β
| β |
|
|
49
54
|
| Real-time monitoring (console, selection) | β
| β | β |
|
|
50
55
|
| Desktop Bridge plugin | β
| β
| β |
|
|
51
56
|
| Requires Node.js | Yes | **No** | No |
|
|
52
|
-
| **Total tools available** | **
|
|
57
|
+
| **Total tools available** | **84+** | **43** | **22** |
|
|
53
58
|
|
|
54
|
-
> **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
|
|
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 84+ tools with real-time monitoring.
|
|
55
60
|
|
|
56
61
|
---
|
|
57
62
|
|
|
@@ -59,7 +64,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
59
64
|
|
|
60
65
|
**Best for:** Designers who want full AI-assisted design capabilities.
|
|
61
66
|
|
|
62
|
-
**What you get:** All
|
|
67
|
+
**What you get:** All 84+ tools including design creation, variable management, and component instantiation.
|
|
63
68
|
|
|
64
69
|
#### Prerequisites
|
|
65
70
|
|
|
@@ -78,7 +83,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
78
83
|
|
|
79
84
|
**Claude Code (CLI):**
|
|
80
85
|
```bash
|
|
81
|
-
claude mcp add figma-console -s user -e FIGMA_ACCESS_TOKEN=figd_YOUR_TOKEN_HERE -e ENABLE_MCP_APPS=true -- npx -y figma-console-mcp@latest
|
|
86
|
+
claude mcp add figma-console -s user -e FIGMA_ACCESS_TOKEN=figd_YOUR_TOKEN_HERE -e ENABLE_MCP_APPS=true -- npx -y @mp3wizard/figma-console-mcp@latest
|
|
82
87
|
```
|
|
83
88
|
|
|
84
89
|
**Cursor / Windsurf / Claude Desktop:**
|
|
@@ -90,7 +95,7 @@ Add to your MCP config file (see [Where to find your config file](#-where-to-fin
|
|
|
90
95
|
"mcpServers": {
|
|
91
96
|
"figma-console": {
|
|
92
97
|
"command": "npx",
|
|
93
|
-
"args": ["-y", "figma-console-mcp@latest"],
|
|
98
|
+
"args": ["-y", "@mp3wizard/figma-console-mcp@latest"],
|
|
94
99
|
"env": {
|
|
95
100
|
"FIGMA_ACCESS_TOKEN": "figd_YOUR_TOKEN_HERE",
|
|
96
101
|
"ENABLE_MCP_APPS": "true"
|
|
@@ -127,7 +132,7 @@ If you're not sure where to put the JSON configuration above, here's where each
|
|
|
127
132
|
|
|
128
133
|
> One-time setup. The plugin uses a bootloader that dynamically loads fresh code from the MCP server β no need to re-import when the server updates.
|
|
129
134
|
|
|
130
|
-
> **Upgrading from v1.14 or earlier?** Your existing plugin still works, but to get the bootloader benefits (no more re-importing), do one final re-import from `~/.figma-console-mcp/plugin/manifest.json`. The path is created automatically when the MCP server starts. Run `npx figma-console-mcp@latest --print-path` to see it. After this one-time upgrade, you're done forever.
|
|
135
|
+
> **Upgrading from v1.14 or earlier?** Your existing plugin still works, but to get the bootloader benefits (no more re-importing), do one final re-import from `~/.figma-console-mcp/plugin/manifest.json`. The path is created automatically when the MCP server starts. Run `npx @mp3wizard/figma-console-mcp@latest --print-path` to see it. After this one-time upgrade, you're done forever.
|
|
131
136
|
|
|
132
137
|
#### Step 4: Restart Your MCP Client
|
|
133
138
|
|
|
@@ -153,7 +158,7 @@ Create a simple frame with a blue background
|
|
|
153
158
|
|
|
154
159
|
**Best for:** Developers who want to modify source code or contribute to the project.
|
|
155
160
|
|
|
156
|
-
**What you get:** Same
|
|
161
|
+
**What you get:** Same 84+ tools as NPX, plus full source code access.
|
|
157
162
|
|
|
158
163
|
#### Quick Setup
|
|
159
164
|
|
|
@@ -242,7 +247,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
|
|
|
242
247
|
|
|
243
248
|
**Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs β no Node.js required.
|
|
244
249
|
|
|
245
|
-
**What you get:**
|
|
250
|
+
**What you get:** 76 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.
|
|
246
251
|
|
|
247
252
|
#### Prerequisites
|
|
248
253
|
|
|
@@ -299,10 +304,11 @@ AI Client β Cloud MCP Server β Durable Object Relay β Desktop Bridge Plugi
|
|
|
299
304
|
| Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
|
|
300
305
|
|---------|-------------------|------------|-----------|------------|
|
|
301
306
|
| **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
|
|
302
|
-
| **Total tools** | **
|
|
307
|
+
| **Total tools** | **84+** | **43** | **84+** | **22** (read-only) |
|
|
303
308
|
| **Design creation** | β
| β
| β
| β |
|
|
304
309
|
| **Variable management** | β
| β
| β
| β |
|
|
305
310
|
| **Component instantiation** | β
| β
| β
| β |
|
|
311
|
+
| **FigJam boards** | β
| β
| β
| β |
|
|
306
312
|
| **Real-time monitoring** | β
| β | β
| β |
|
|
307
313
|
| **Desktop Bridge plugin** | β
| β
| β
| β |
|
|
308
314
|
| **Variables (no Enterprise)** | β
| β
| β
| β |
|
|
@@ -313,7 +319,7 @@ AI Client β Cloud MCP Server β Durable Object Relay β Desktop Bridge Plugi
|
|
|
313
319
|
| **Automatic updates** | β
(`@latest`) | β
| Manual (`git pull`) | β
|
|
|
314
320
|
| **Source code access** | β | β | β
| β |
|
|
315
321
|
|
|
316
|
-
> **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
|
|
322
|
+
> **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 84+ tools.
|
|
317
323
|
|
|
318
324
|
**π [Complete Feature Comparison](docs/mode-comparison.md)**
|
|
319
325
|
|
|
@@ -426,6 +432,34 @@ When you first use design system tools:
|
|
|
426
432
|
- `figma_batch_update_variables` - Update up to 100 variable values in one call
|
|
427
433
|
- `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically
|
|
428
434
|
|
|
435
|
+
### π FigJam Board Tools (Local Mode + Cloud Mode)
|
|
436
|
+
- `figjam_create_sticky` - Create a sticky note with color options
|
|
437
|
+
- `figjam_create_stickies` - Batch create up to 200 stickies
|
|
438
|
+
- `figjam_create_connector` - Connect nodes with labeled connector lines
|
|
439
|
+
- `figjam_create_shape_with_text` - Create flowchart shapes (diamond, ellipse, etc.)
|
|
440
|
+
- `figjam_create_table` - Create tables with cell data
|
|
441
|
+
- `figjam_create_code_block` - Add code snippets with syntax highlighting
|
|
442
|
+
- `figjam_auto_arrange` - Arrange nodes in grid, horizontal, or vertical layouts
|
|
443
|
+
- `figjam_get_board_contents` - Read all content from a FigJam board
|
|
444
|
+
- `figjam_get_connections` - Read the connection graph (flowcharts, relationships)
|
|
445
|
+
|
|
446
|
+
### ποΈ Slides Presentation Tools (Local Mode + Cloud Mode)
|
|
447
|
+
- `figma_list_slides` - List all slides with IDs, positions, and skip status
|
|
448
|
+
- `figma_get_slide_content` - Get the full content tree of a slide
|
|
449
|
+
- `figma_get_slide_grid` - Get the 2D grid layout of the presentation
|
|
450
|
+
- `figma_get_slide_transition` - Read transition settings for a slide
|
|
451
|
+
- `figma_get_focused_slide` - Get the currently focused slide
|
|
452
|
+
- `figma_create_slide` - Create a new blank slide
|
|
453
|
+
- `figma_delete_slide` - Delete a slide from the presentation
|
|
454
|
+
- `figma_duplicate_slide` - Clone an existing slide
|
|
455
|
+
- `figma_reorder_slides` - Reorder slides via new 2D grid layout
|
|
456
|
+
- `figma_set_slide_transition` - Set transition effects (22 styles, 8 curves)
|
|
457
|
+
- `figma_skip_slide` - Toggle whether a slide is skipped in presentation mode
|
|
458
|
+
- `figma_add_text_to_slide` - Add text to a specific slide
|
|
459
|
+
- `figma_add_shape_to_slide` - Add rectangle or ellipse shapes with color
|
|
460
|
+
- `figma_set_slides_view_mode` - Toggle grid vs. single-slide view
|
|
461
|
+
- `figma_focus_slide` - Navigate to a specific slide
|
|
462
|
+
|
|
429
463
|
**π [Detailed Tool Documentation](docs/TOOLS.md)**
|
|
430
464
|
|
|
431
465
|
---
|
|
@@ -479,6 +513,25 @@ Check design parity for the Card component before sign-off
|
|
|
479
513
|
Generate component documentation for the Dialog from our design system
|
|
480
514
|
```
|
|
481
515
|
|
|
516
|
+
### FigJam Boards
|
|
517
|
+
```
|
|
518
|
+
Create a retrospective board with "Went Well", "To Improve", and "Action Items" columns
|
|
519
|
+
Build a user flow diagram for the checkout process with decision points
|
|
520
|
+
Read this brainstorming board and summarize the key themes
|
|
521
|
+
Generate an affinity map from these meeting notes
|
|
522
|
+
Create a comparison table of our three platform options
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Slides Presentations
|
|
526
|
+
```
|
|
527
|
+
List all slides and tell me which ones are skipped
|
|
528
|
+
Add a new slide with the title "Thank You" in 72px text
|
|
529
|
+
Set a DISSOLVE transition on the first slide with 0.5 second duration
|
|
530
|
+
Duplicate slide 5 for an A/B comparison
|
|
531
|
+
Skip slides 8 and 9 β they're not ready for the client presentation
|
|
532
|
+
Reorder my slides so the conclusion comes before Q&A
|
|
533
|
+
```
|
|
534
|
+
|
|
482
535
|
### Visual Debugging
|
|
483
536
|
```
|
|
484
537
|
Take a screenshot of the current Figma canvas
|
|
@@ -598,7 +651,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
|
|
|
598
651
|
- The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
|
|
599
652
|
- The server tries port 9223 first, then automatically falls back through ports 9224β9232 if needed
|
|
600
653
|
- The plugin scans all ports in the range and connects to every active server it finds
|
|
601
|
-
- All
|
|
654
|
+
- All 84+ tools work through the WebSocket transport
|
|
602
655
|
|
|
603
656
|
**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).
|
|
604
657
|
|
|
@@ -716,11 +769,12 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
716
769
|
|
|
717
770
|
## π€ vs. Figma Official MCP
|
|
718
771
|
|
|
719
|
-
**Figma Console MCP (This Project)** - Debugging
|
|
772
|
+
**Figma Console MCP (This Project)** - Debugging, data extraction, and design creation
|
|
720
773
|
- β
Real-time console logs from Figma plugins
|
|
721
774
|
- β
Screenshot capture and visual debugging
|
|
722
775
|
- β
Error stack traces and runtime monitoring
|
|
723
776
|
- β
Raw design data extraction (JSON)
|
|
777
|
+
- β
FigJam board creation and reading (stickies, flowcharts, tables)
|
|
724
778
|
- β
Works remotely or locally
|
|
725
779
|
|
|
726
780
|
**Figma Official Dev Mode MCP** - Code generation
|
|
@@ -734,9 +788,11 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
734
788
|
|
|
735
789
|
## π€οΈ Roadmap
|
|
736
790
|
|
|
737
|
-
**Current Status:** v1.
|
|
791
|
+
**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, 84+ tools, Comments API, and MCP Apps
|
|
738
792
|
|
|
739
793
|
**Recent Releases:**
|
|
794
|
+
- [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).
|
|
795
|
+
- [x] **v1.16.0** - FigJam Support: 9 new tools for creating and reading FigJam boards β stickies, flowcharts, tables, code blocks, and connection graphs. Community-contributed by klgral and lukemoderwell.
|
|
740
796
|
- [x] **v1.12.0** - Cloud Write Relay: web AI clients (Claude.ai, v0, Replit, Lovable) can create and modify Figma designs via cloud relay pairing β no Node.js required
|
|
741
797
|
- [x] **v1.11.2** - Screenshot fix: `figma_take_screenshot` works without explicit `nodeId` in WebSocket mode
|
|
742
798
|
- [x] **v1.11.1** - Doc generator fixes: clean markdown tables, Storybook links, property metadata filtering
|
|
@@ -758,28 +814,6 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
758
814
|
|
|
759
815
|
---
|
|
760
816
|
|
|
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
817
|
## π» Development
|
|
784
818
|
|
|
785
819
|
```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
|
}
|