@mp3wizard/figma-console-mcp 1.14.0
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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
- package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
- package/dist/apps/design-system-dashboard/server.d.ts +24 -0
- package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/server.js +160 -0
- package/dist/apps/design-system-dashboard/server.js.map +1 -0
- package/dist/apps/token-browser/server.d.ts +26 -0
- package/dist/apps/token-browser/server.d.ts.map +1 -0
- package/dist/apps/token-browser/server.js +137 -0
- package/dist/apps/token-browser/server.js.map +1 -0
- package/dist/browser/base.d.ts +58 -0
- package/dist/browser/base.d.ts.map +1 -0
- package/dist/browser/base.js +6 -0
- package/dist/browser/base.js.map +1 -0
- package/dist/browser/local.d.ts +87 -0
- package/dist/browser/local.d.ts.map +1 -0
- package/dist/browser/local.js +318 -0
- package/dist/browser/local.js.map +1 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
- package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
- package/dist/cloudflare/apps/token-browser/server.js +136 -0
- package/dist/cloudflare/browser/base.js +5 -0
- package/dist/cloudflare/browser/cloudflare.js +156 -0
- package/dist/cloudflare/browser-manager.js +157 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +267 -0
- package/dist/cloudflare/core/cloud-websocket-relay.js +199 -0
- package/dist/cloudflare/core/comment-tools.js +292 -0
- package/dist/cloudflare/core/config.js +161 -0
- package/dist/cloudflare/core/console-monitor.js +427 -0
- package/dist/cloudflare/core/design-code-tools.js +2504 -0
- package/dist/cloudflare/core/design-system-manifest.js +260 -0
- package/dist/cloudflare/core/design-system-tools.js +863 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +272 -0
- package/dist/cloudflare/core/enrichment/index.js +7 -0
- package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
- package/dist/cloudflare/core/enrichment/style-resolver.js +326 -0
- package/dist/cloudflare/core/figma-api.js +409 -0
- package/dist/cloudflare/core/figma-connector.js +7 -0
- package/dist/cloudflare/core/figma-desktop-connector.js +1184 -0
- package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
- package/dist/cloudflare/core/figma-style-extractor.js +311 -0
- package/dist/cloudflare/core/figma-tools.js +2947 -0
- package/dist/cloudflare/core/logger.js +53 -0
- package/dist/cloudflare/core/port-discovery.js +282 -0
- package/dist/cloudflare/core/snippet-injector.js +96 -0
- package/dist/cloudflare/core/types/design-code.js +4 -0
- package/dist/cloudflare/core/types/enriched.js +5 -0
- package/dist/cloudflare/core/types/index.js +4 -0
- package/dist/cloudflare/core/websocket-connector.js +256 -0
- package/dist/cloudflare/core/websocket-server.js +646 -0
- package/dist/cloudflare/core/write-tools.js +2091 -0
- package/dist/cloudflare/index.js +2899 -0
- package/dist/cloudflare/test-browser.js +88 -0
- package/dist/core/comment-tools.d.ts +11 -0
- package/dist/core/comment-tools.d.ts.map +1 -0
- package/dist/core/comment-tools.js +293 -0
- package/dist/core/comment-tools.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +162 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/console-monitor.d.ts +82 -0
- package/dist/core/console-monitor.d.ts.map +1 -0
- package/dist/core/console-monitor.js +428 -0
- package/dist/core/console-monitor.js.map +1 -0
- package/dist/core/design-code-tools.d.ts +127 -0
- package/dist/core/design-code-tools.d.ts.map +1 -0
- package/dist/core/design-code-tools.js +2505 -0
- package/dist/core/design-code-tools.js.map +1 -0
- package/dist/core/design-system-manifest.d.ts +272 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -0
- package/dist/core/design-system-manifest.js +261 -0
- package/dist/core/design-system-manifest.js.map +1 -0
- package/dist/core/design-system-tools.d.ts +17 -0
- package/dist/core/design-system-tools.d.ts.map +1 -0
- package/dist/core/design-system-tools.js +864 -0
- package/dist/core/design-system-tools.js.map +1 -0
- package/dist/core/enrichment/enrichment-service.d.ts +52 -0
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
- package/dist/core/enrichment/enrichment-service.js +273 -0
- package/dist/core/enrichment/enrichment-service.js.map +1 -0
- package/dist/core/enrichment/index.d.ts +8 -0
- package/dist/core/enrichment/index.d.ts.map +1 -0
- package/dist/core/enrichment/index.js +8 -0
- package/dist/core/enrichment/index.js.map +1 -0
- package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
- package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
- package/dist/core/enrichment/relationship-mapper.js +352 -0
- package/dist/core/enrichment/relationship-mapper.js.map +1 -0
- package/dist/core/enrichment/style-resolver.d.ts +80 -0
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
- package/dist/core/enrichment/style-resolver.js +327 -0
- package/dist/core/enrichment/style-resolver.js.map +1 -0
- package/dist/core/figma-api.d.ts +201 -0
- package/dist/core/figma-api.d.ts.map +1 -0
- package/dist/core/figma-api.js +410 -0
- package/dist/core/figma-api.js.map +1 -0
- package/dist/core/figma-connector.d.ts +48 -0
- package/dist/core/figma-connector.d.ts.map +1 -0
- package/dist/core/figma-connector.js +8 -0
- package/dist/core/figma-connector.js.map +1 -0
- package/dist/core/figma-desktop-connector.d.ts +265 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -0
- package/dist/core/figma-desktop-connector.js +1184 -0
- package/dist/core/figma-desktop-connector.js.map +1 -0
- package/dist/core/figma-reconstruction-spec.d.ts +166 -0
- package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
- package/dist/core/figma-reconstruction-spec.js +403 -0
- package/dist/core/figma-reconstruction-spec.js.map +1 -0
- package/dist/core/figma-style-extractor.d.ts +76 -0
- package/dist/core/figma-style-extractor.d.ts.map +1 -0
- package/dist/core/figma-style-extractor.js +312 -0
- package/dist/core/figma-style-extractor.js.map +1 -0
- package/dist/core/figma-tools.d.ts +23 -0
- package/dist/core/figma-tools.d.ts.map +1 -0
- package/dist/core/figma-tools.js +2948 -0
- package/dist/core/figma-tools.js.map +1 -0
- package/dist/core/logger.d.ts +22 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/port-discovery.d.ts +110 -0
- package/dist/core/port-discovery.d.ts.map +1 -0
- package/dist/core/port-discovery.js +283 -0
- package/dist/core/port-discovery.js.map +1 -0
- package/dist/core/snippet-injector.d.ts +24 -0
- package/dist/core/snippet-injector.d.ts.map +1 -0
- package/dist/core/snippet-injector.js +97 -0
- package/dist/core/snippet-injector.js.map +1 -0
- package/dist/core/types/design-code.d.ts +262 -0
- package/dist/core/types/design-code.d.ts.map +1 -0
- package/dist/core/types/design-code.js +5 -0
- package/dist/core/types/design-code.js.map +1 -0
- package/dist/core/types/enriched.d.ts +213 -0
- package/dist/core/types/enriched.d.ts.map +1 -0
- package/dist/core/types/enriched.js +6 -0
- package/dist/core/types/enriched.js.map +1 -0
- package/dist/core/types/index.d.ts +112 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +5 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/websocket-connector.d.ts +55 -0
- package/dist/core/websocket-connector.d.ts.map +1 -0
- package/dist/core/websocket-connector.js +257 -0
- package/dist/core/websocket-connector.js.map +1 -0
- package/dist/core/websocket-server.d.ts +191 -0
- package/dist/core/websocket-server.d.ts.map +1 -0
- package/dist/core/websocket-server.js +647 -0
- package/dist/core/websocket-server.js.map +1 -0
- package/dist/core/write-tools.d.ts +7 -0
- package/dist/core/write-tools.d.ts.map +1 -0
- package/dist/core/write-tools.js +2092 -0
- package/dist/core/write-tools.js.map +1 -0
- package/dist/local.d.ts +84 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +5039 -0
- package/dist/local.js.map +1 -0
- package/figma-desktop-bridge/README.md +313 -0
- package/figma-desktop-bridge/code.js +2818 -0
- package/figma-desktop-bridge/manifest.json +67 -0
- package/figma-desktop-bridge/ui.html +1236 -0
- package/package.json +87 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud WebSocket Relay — Durable Object
|
|
3
|
+
*
|
|
4
|
+
* Bridges the Figma Desktop Bridge plugin to the cloud MCP server.
|
|
5
|
+
* The plugin connects via WebSocket (hibernation-aware); the MCP DO
|
|
6
|
+
* sends commands via fetch() RPC and receives responses.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Uses hibernation-safe patterns throughout:
|
|
9
|
+
* - WebSocket retrieved via this.ctx.getWebSockets() (not class property)
|
|
10
|
+
* - File info persisted in DO storage (not in-memory)
|
|
11
|
+
* - Pending requests kept in-memory (safe: fetch keeps DO alive)
|
|
12
|
+
*
|
|
13
|
+
* Routes:
|
|
14
|
+
* /ws/connect — WebSocket upgrade from plugin (paired via code)
|
|
15
|
+
* /relay/command — RPC from MCP DO → plugin (holds response open)
|
|
16
|
+
* /relay/status — Connection & file info query
|
|
17
|
+
*/
|
|
18
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
19
|
+
/**
|
|
20
|
+
* Generate a 6-character alphanumeric pairing code (uppercase).
|
|
21
|
+
*/
|
|
22
|
+
export function generatePairingCode() {
|
|
23
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // No 0/O/1/I confusion
|
|
24
|
+
let code = '';
|
|
25
|
+
const arr = new Uint8Array(6);
|
|
26
|
+
crypto.getRandomValues(arr);
|
|
27
|
+
for (let i = 0; i < 6; i++) {
|
|
28
|
+
code += chars[arr[i] % chars.length];
|
|
29
|
+
}
|
|
30
|
+
return code;
|
|
31
|
+
}
|
|
32
|
+
export class PluginRelayDO extends DurableObject {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
this.pendingRequests = new Map();
|
|
36
|
+
this.requestIdCounter = 0;
|
|
37
|
+
}
|
|
38
|
+
// ======================================================================
|
|
39
|
+
// Hibernation-safe WebSocket retrieval
|
|
40
|
+
// ======================================================================
|
|
41
|
+
/**
|
|
42
|
+
* Get the active plugin WebSocket. Uses ctx.getWebSockets() which
|
|
43
|
+
* survives DO hibernation (unlike a class property reference).
|
|
44
|
+
*/
|
|
45
|
+
getPluginWs() {
|
|
46
|
+
const sockets = this.ctx.getWebSockets('plugin');
|
|
47
|
+
return sockets.length > 0 ? sockets[0] : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Incoming fetch handler — dispatches to routes.
|
|
51
|
+
*/
|
|
52
|
+
async fetch(request) {
|
|
53
|
+
const url = new URL(request.url);
|
|
54
|
+
if (url.pathname === '/ws/connect') {
|
|
55
|
+
return this.handlePluginConnect(request);
|
|
56
|
+
}
|
|
57
|
+
if (url.pathname === '/relay/command') {
|
|
58
|
+
return this.handleRelayCommand(request);
|
|
59
|
+
}
|
|
60
|
+
if (url.pathname === '/relay/status') {
|
|
61
|
+
return this.handleRelayStatus();
|
|
62
|
+
}
|
|
63
|
+
return new Response('Not found', { status: 404 });
|
|
64
|
+
}
|
|
65
|
+
// ==========================================================================
|
|
66
|
+
// WebSocket — plugin connects here
|
|
67
|
+
// ==========================================================================
|
|
68
|
+
handlePluginConnect(request) {
|
|
69
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
70
|
+
if (!upgradeHeader || upgradeHeader.toLowerCase() !== 'websocket') {
|
|
71
|
+
return new Response('Expected WebSocket upgrade', { status: 426 });
|
|
72
|
+
}
|
|
73
|
+
// Close any existing plugin connection (e.g., re-pairing)
|
|
74
|
+
const existing = this.getPluginWs();
|
|
75
|
+
if (existing) {
|
|
76
|
+
try {
|
|
77
|
+
existing.close(1000, 'Replaced by new connection');
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
const pair = new WebSocketPair();
|
|
82
|
+
const [client, server] = Object.values(pair);
|
|
83
|
+
// Accept with 'plugin' tag — this.ctx.getWebSockets('plugin') retrieves
|
|
84
|
+
// the socket even after the DO wakes from hibernation.
|
|
85
|
+
this.ctx.acceptWebSocket(server, ['plugin']);
|
|
86
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Hibernation callback — incoming message from plugin WebSocket.
|
|
90
|
+
*/
|
|
91
|
+
async webSocketMessage(ws, message) {
|
|
92
|
+
if (typeof message !== 'string')
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(message);
|
|
96
|
+
// FILE_INFO identification from plugin — persist to DO storage
|
|
97
|
+
if (data.type === 'FILE_INFO' && data.data) {
|
|
98
|
+
const fileInfo = {
|
|
99
|
+
fileName: data.data.fileName,
|
|
100
|
+
fileKey: data.data.fileKey || null,
|
|
101
|
+
currentPage: data.data.currentPage,
|
|
102
|
+
currentPageId: data.data.currentPageId || null,
|
|
103
|
+
connectedAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
await this.ctx.storage.put('fileInfo', fileInfo);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Event broadcasts from plugin (PAGE_CHANGE, etc.)
|
|
109
|
+
if (data.type === 'PAGE_CHANGE' && data.data) {
|
|
110
|
+
const fileInfo = await this.ctx.storage.get('fileInfo');
|
|
111
|
+
if (fileInfo) {
|
|
112
|
+
fileInfo.currentPage = data.data.pageName;
|
|
113
|
+
fileInfo.currentPageId = data.data.pageId || null;
|
|
114
|
+
await this.ctx.storage.put('fileInfo', fileInfo);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Response to a relayed command
|
|
119
|
+
if (data.id && this.pendingRequests.has(data.id)) {
|
|
120
|
+
const pending = this.pendingRequests.get(data.id);
|
|
121
|
+
clearTimeout(pending.timeoutId);
|
|
122
|
+
this.pendingRequests.delete(data.id);
|
|
123
|
+
const body = JSON.stringify(data.error
|
|
124
|
+
? { error: data.error }
|
|
125
|
+
: { result: data.result });
|
|
126
|
+
pending.resolve(new Response(body, {
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Malformed message — ignore
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Hibernation callback — WebSocket closed.
|
|
137
|
+
*/
|
|
138
|
+
webSocketClose(ws, code, reason, wasClean) {
|
|
139
|
+
this.handleDisconnect();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Hibernation callback — WebSocket error.
|
|
143
|
+
*/
|
|
144
|
+
webSocketError(ws, error) {
|
|
145
|
+
this.handleDisconnect();
|
|
146
|
+
}
|
|
147
|
+
handleDisconnect() {
|
|
148
|
+
// Clear persisted file info
|
|
149
|
+
this.ctx.storage.delete('fileInfo');
|
|
150
|
+
// Reject all in-flight commands
|
|
151
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
152
|
+
clearTimeout(pending.timeoutId);
|
|
153
|
+
pending.resolve(new Response(JSON.stringify({ error: 'Plugin disconnected' }), { status: 502, headers: { 'Content-Type': 'application/json' } }));
|
|
154
|
+
}
|
|
155
|
+
this.pendingRequests.clear();
|
|
156
|
+
}
|
|
157
|
+
// ==========================================================================
|
|
158
|
+
// Relay — MCP DO sends commands here
|
|
159
|
+
// ==========================================================================
|
|
160
|
+
async handleRelayCommand(request) {
|
|
161
|
+
const pluginWs = this.getPluginWs();
|
|
162
|
+
if (!pluginWs) {
|
|
163
|
+
return new Response(JSON.stringify({ error: 'No plugin connected. User must pair the Desktop Bridge plugin first.' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
164
|
+
}
|
|
165
|
+
const body = await request.json();
|
|
166
|
+
const { method, params = {}, timeoutMs = 15000 } = body;
|
|
167
|
+
const safeTimeout = Math.min(Math.max(timeoutMs, 1000), 60000); // clamp 1s–60s
|
|
168
|
+
const id = `relay_${++this.requestIdCounter}_${Date.now()}`;
|
|
169
|
+
// Send command to plugin
|
|
170
|
+
try {
|
|
171
|
+
pluginWs.send(JSON.stringify({ id, method, params }));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return new Response(JSON.stringify({ error: 'Failed to send command to plugin — connection may be stale' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
175
|
+
}
|
|
176
|
+
// Wait for plugin response (the DO stays alive because fetch is active)
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
const timeoutId = setTimeout(() => {
|
|
179
|
+
if (this.pendingRequests.has(id)) {
|
|
180
|
+
this.pendingRequests.delete(id);
|
|
181
|
+
resolve(new Response(JSON.stringify({ error: `Command ${method} timed out after ${safeTimeout}ms` }), { status: 504, headers: { 'Content-Type': 'application/json' } }));
|
|
182
|
+
}
|
|
183
|
+
}, safeTimeout);
|
|
184
|
+
this.pendingRequests.set(id, { resolve, reject: () => { }, timeoutId });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ==========================================================================
|
|
188
|
+
// Status
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
async handleRelayStatus() {
|
|
191
|
+
const pluginWs = this.getPluginWs();
|
|
192
|
+
const fileInfo = await this.ctx.storage.get('fileInfo');
|
|
193
|
+
return new Response(JSON.stringify({
|
|
194
|
+
connected: pluginWs !== null,
|
|
195
|
+
fileInfo: fileInfo || null,
|
|
196
|
+
pendingCommands: this.pendingRequests.size,
|
|
197
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma Comments MCP Tools
|
|
3
|
+
* Tools for getting, posting, and deleting comments on Figma files via REST API.
|
|
4
|
+
* Works in both local and Cloudflare Workers modes — no Plugin API dependency.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { extractFileKey } from "./figma-api.js";
|
|
8
|
+
import { createChildLogger } from "./logger.js";
|
|
9
|
+
const logger = createChildLogger({ component: "comment-tools" });
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Tool Registration
|
|
12
|
+
// ============================================================================
|
|
13
|
+
export function registerCommentTools(server, getFigmaAPI, getCurrentUrl, options) {
|
|
14
|
+
// -----------------------------------------------------------------------
|
|
15
|
+
// Tool: figma_get_comments
|
|
16
|
+
// -----------------------------------------------------------------------
|
|
17
|
+
server.tool("figma_get_comments", "Get comments on a Figma file. Returns comment threads with author, message, timestamps, and pinned node locations. Use include_resolved to also see resolved comments.", {
|
|
18
|
+
fileUrl: z
|
|
19
|
+
.string()
|
|
20
|
+
.url()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Figma file URL. Uses current URL if omitted."),
|
|
23
|
+
as_md: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(false)
|
|
27
|
+
.describe("Return comment message bodies as markdown. Default: false"),
|
|
28
|
+
include_resolved: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe("Include resolved (completed) comment threads. Default: false (only active comments)"),
|
|
33
|
+
}, async ({ fileUrl, as_md = false, include_resolved = false }) => {
|
|
34
|
+
try {
|
|
35
|
+
const url = fileUrl || getCurrentUrl();
|
|
36
|
+
if (!url) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
error: "no_file_url",
|
|
43
|
+
message: "No Figma file URL available. Pass the fileUrl parameter or ensure the Desktop Bridge plugin is open in Figma.",
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const fileKey = extractFileKey(url);
|
|
51
|
+
if (!fileKey) {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: JSON.stringify({
|
|
57
|
+
error: "invalid_url",
|
|
58
|
+
message: `Invalid Figma URL: ${url}`,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
logger.info({ fileKey, as_md, include_resolved }, "Fetching comments");
|
|
66
|
+
const api = await getFigmaAPI();
|
|
67
|
+
const response = await api.getComments(fileKey, { as_md });
|
|
68
|
+
const allComments = response.comments || [];
|
|
69
|
+
// Filter out resolved comments unless explicitly requested
|
|
70
|
+
const comments = include_resolved
|
|
71
|
+
? allComments
|
|
72
|
+
: allComments.filter((c) => !c.resolved_at);
|
|
73
|
+
const result = {
|
|
74
|
+
comments,
|
|
75
|
+
summary: {
|
|
76
|
+
total: allComments.length,
|
|
77
|
+
active: allComments.filter((c) => !c.resolved_at).length,
|
|
78
|
+
resolved: allComments.filter((c) => c.resolved_at).length,
|
|
79
|
+
returned: comments.length,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: JSON.stringify(result),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
logger.error({ error }, "Failed to get comments");
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: JSON.stringify({
|
|
99
|
+
error: "get_comments_failed",
|
|
100
|
+
message: `Cannot get comments. ${message}`,
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
// Tool: figma_post_comment
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
server.tool("figma_post_comment", "Post a comment on a Figma file, optionally pinned to a specific design node. Use after figma_check_design_parity to notify designers of drift when code is the canonical source. Supports replies to existing comment threads. Limitation: @mentions are a Figma UI-only feature — including '@name' in the message renders as plain text, not a clickable mention tag, and does not trigger Figma notifications.", {
|
|
112
|
+
fileUrl: z
|
|
113
|
+
.string()
|
|
114
|
+
.url()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("Figma file URL. Uses current URL if omitted."),
|
|
117
|
+
message: z
|
|
118
|
+
.string()
|
|
119
|
+
.describe("The comment message text. Supports basic formatting."),
|
|
120
|
+
node_id: z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Node ID to pin the comment to (e.g., '695:313'). Comment appears on that element in Figma."),
|
|
124
|
+
x: z
|
|
125
|
+
.number()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("X coordinate for comment placement (absolute canvas position). Used with node_id."),
|
|
128
|
+
y: z
|
|
129
|
+
.number()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Y coordinate for comment placement (absolute canvas position). Used with node_id."),
|
|
132
|
+
reply_to_comment_id: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("ID of an existing comment to reply to. Creates a threaded reply instead of a new top-level comment."),
|
|
136
|
+
}, async ({ fileUrl, message, node_id, x, y, reply_to_comment_id }) => {
|
|
137
|
+
try {
|
|
138
|
+
const url = fileUrl || getCurrentUrl();
|
|
139
|
+
if (!url) {
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: JSON.stringify({
|
|
145
|
+
error: "no_file_url",
|
|
146
|
+
message: "No Figma file URL available. Pass the fileUrl parameter or ensure the Desktop Bridge plugin is open in Figma.",
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const fileKey = extractFileKey(url);
|
|
154
|
+
if (!fileKey) {
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: JSON.stringify({
|
|
160
|
+
error: "invalid_url",
|
|
161
|
+
message: `Invalid Figma URL: ${url}`,
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
logger.info({ fileKey, node_id, reply_to_comment_id }, "Posting comment");
|
|
169
|
+
const api = await getFigmaAPI();
|
|
170
|
+
// Build client_meta for pinning to a node/position
|
|
171
|
+
// Figma API requires node_offset when node_id is present — default to (0,0) if not specified
|
|
172
|
+
let clientMeta;
|
|
173
|
+
if (node_id) {
|
|
174
|
+
clientMeta = {
|
|
175
|
+
node_id,
|
|
176
|
+
node_offset: { x: x ?? 0, y: y ?? 0 },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const result = await api.postComment(fileKey, message, clientMeta, reply_to_comment_id);
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: JSON.stringify({
|
|
185
|
+
success: true,
|
|
186
|
+
comment: {
|
|
187
|
+
id: result.id,
|
|
188
|
+
message: result.message,
|
|
189
|
+
created_at: result.created_at,
|
|
190
|
+
user: result.user,
|
|
191
|
+
client_meta: result.client_meta,
|
|
192
|
+
order_id: result.order_id,
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const message_text = error instanceof Error ? error.message : String(error);
|
|
201
|
+
logger.error({ error }, "Failed to post comment");
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: JSON.stringify({
|
|
207
|
+
error: "post_comment_failed",
|
|
208
|
+
message: `Cannot post comment. ${message_text}`,
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
isError: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// -----------------------------------------------------------------------
|
|
217
|
+
// Tool: figma_delete_comment
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
server.tool("figma_delete_comment", "Delete a comment from a Figma file by its comment ID. Use figma_get_comments to find comment IDs first.", {
|
|
220
|
+
fileUrl: z
|
|
221
|
+
.string()
|
|
222
|
+
.url()
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("Figma file URL. Uses current URL if omitted."),
|
|
225
|
+
comment_id: z
|
|
226
|
+
.string()
|
|
227
|
+
.describe("The ID of the comment to delete. Get IDs from figma_get_comments."),
|
|
228
|
+
}, async ({ fileUrl, comment_id }) => {
|
|
229
|
+
try {
|
|
230
|
+
const url = fileUrl || getCurrentUrl();
|
|
231
|
+
if (!url) {
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: JSON.stringify({
|
|
237
|
+
error: "no_file_url",
|
|
238
|
+
message: "No Figma file URL available. Pass the fileUrl parameter or ensure the Desktop Bridge plugin is open in Figma.",
|
|
239
|
+
}),
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
isError: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const fileKey = extractFileKey(url);
|
|
246
|
+
if (!fileKey) {
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: JSON.stringify({
|
|
252
|
+
error: "invalid_url",
|
|
253
|
+
message: `Invalid Figma URL: ${url}`,
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
isError: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
logger.info({ fileKey, comment_id }, "Deleting comment");
|
|
261
|
+
const api = await getFigmaAPI();
|
|
262
|
+
await api.deleteComment(fileKey, comment_id);
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: JSON.stringify({
|
|
268
|
+
success: true,
|
|
269
|
+
deleted_comment_id: comment_id,
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
logger.error({ error }, "Failed to delete comment");
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify({
|
|
283
|
+
error: "delete_comment_failed",
|
|
284
|
+
message: `Cannot delete comment. ${message}`,
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
isError: true,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Figma Console MCP server
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* Auto-detect server mode based on environment
|
|
9
|
+
*/
|
|
10
|
+
function detectMode() {
|
|
11
|
+
// If running in Workers environment, return cloudflare
|
|
12
|
+
if (typeof globalThis !== 'undefined' && 'caches' in globalThis) {
|
|
13
|
+
return 'cloudflare';
|
|
14
|
+
}
|
|
15
|
+
// Explicit env var override
|
|
16
|
+
const modeEnv = process.env.FIGMA_MCP_MODE?.toLowerCase();
|
|
17
|
+
if (modeEnv === 'local' || modeEnv === 'cloudflare') {
|
|
18
|
+
return modeEnv;
|
|
19
|
+
}
|
|
20
|
+
// Default to local for Node.js environments
|
|
21
|
+
return 'local';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Default configuration values
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_CONFIG = {
|
|
27
|
+
mode: detectMode(),
|
|
28
|
+
browser: {
|
|
29
|
+
headless: false,
|
|
30
|
+
args: [
|
|
31
|
+
'--disable-blink-features=AutomationControlled',
|
|
32
|
+
'--disable-dev-shm-usage',
|
|
33
|
+
// '--no-sandbox' removed from defaults; enable via config file if required by your environment
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
console: {
|
|
37
|
+
bufferSize: 1000,
|
|
38
|
+
filterLevels: ['log', 'info', 'warn', 'error', 'debug'],
|
|
39
|
+
truncation: {
|
|
40
|
+
maxStringLength: 500,
|
|
41
|
+
maxArrayLength: 10,
|
|
42
|
+
maxObjectDepth: 3,
|
|
43
|
+
removeDuplicates: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
screenshots: {
|
|
47
|
+
defaultFormat: 'png',
|
|
48
|
+
quality: 90,
|
|
49
|
+
storePath: join(process.env.TMPDIR || '/tmp', 'figma-console-mcp', 'screenshots'),
|
|
50
|
+
},
|
|
51
|
+
local: {
|
|
52
|
+
debugHost: process.env.FIGMA_DEBUG_HOST || 'localhost',
|
|
53
|
+
debugPort: parseInt(process.env.FIGMA_DEBUG_PORT || '9222', 10),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Possible config file locations (checked in order)
|
|
58
|
+
*/
|
|
59
|
+
const CONFIG_PATHS = [
|
|
60
|
+
// Environment variable override
|
|
61
|
+
process.env.FIGMA_CONSOLE_CONFIG,
|
|
62
|
+
// Project-local config
|
|
63
|
+
join(process.cwd(), '.figma-console-mcp.json'),
|
|
64
|
+
join(process.cwd(), 'figma-console-mcp.json'),
|
|
65
|
+
// User home config
|
|
66
|
+
join(homedir(), '.config', 'figma-console-mcp', 'config.json'),
|
|
67
|
+
join(homedir(), '.figma-console-mcp.json'),
|
|
68
|
+
].filter((path) => path !== undefined);
|
|
69
|
+
/**
|
|
70
|
+
* Load configuration from file or use defaults
|
|
71
|
+
*/
|
|
72
|
+
export function loadConfig() {
|
|
73
|
+
// Try to load from config file
|
|
74
|
+
for (const configPath of CONFIG_PATHS) {
|
|
75
|
+
if (existsSync(configPath)) {
|
|
76
|
+
try {
|
|
77
|
+
const fileContent = readFileSync(configPath, 'utf-8');
|
|
78
|
+
const userConfig = JSON.parse(fileContent);
|
|
79
|
+
// Deep merge with defaults
|
|
80
|
+
const config = mergeConfig(DEFAULT_CONFIG, userConfig);
|
|
81
|
+
return config;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`Failed to load config from ${configPath}:`, error);
|
|
85
|
+
// Continue to next config path
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// No config file found, use defaults
|
|
90
|
+
return DEFAULT_CONFIG;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Deep merge two configuration objects
|
|
94
|
+
*/
|
|
95
|
+
function mergeConfig(defaults, overrides) {
|
|
96
|
+
return {
|
|
97
|
+
mode: overrides.mode || defaults.mode,
|
|
98
|
+
browser: {
|
|
99
|
+
...defaults.browser,
|
|
100
|
+
...(overrides.browser || {}),
|
|
101
|
+
},
|
|
102
|
+
console: {
|
|
103
|
+
...defaults.console,
|
|
104
|
+
...(overrides.console || {}),
|
|
105
|
+
truncation: {
|
|
106
|
+
...defaults.console.truncation,
|
|
107
|
+
...(overrides.console?.truncation || {}),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
screenshots: {
|
|
111
|
+
...defaults.screenshots,
|
|
112
|
+
...(overrides.screenshots || {}),
|
|
113
|
+
},
|
|
114
|
+
local: {
|
|
115
|
+
...defaults.local,
|
|
116
|
+
...(overrides.local || {}),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate configuration
|
|
122
|
+
*/
|
|
123
|
+
export function validateConfig(config) {
|
|
124
|
+
// Validate browser config
|
|
125
|
+
if (!Array.isArray(config.browser.args)) {
|
|
126
|
+
throw new Error('browser.args must be an array');
|
|
127
|
+
}
|
|
128
|
+
// Validate console config
|
|
129
|
+
if (config.console.bufferSize <= 0) {
|
|
130
|
+
throw new Error('console.bufferSize must be positive');
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(config.console.filterLevels)) {
|
|
133
|
+
throw new Error('console.filterLevels must be an array');
|
|
134
|
+
}
|
|
135
|
+
// Validate truncation config
|
|
136
|
+
const { truncation } = config.console;
|
|
137
|
+
if (truncation.maxStringLength <= 0) {
|
|
138
|
+
throw new Error('console.truncation.maxStringLength must be positive');
|
|
139
|
+
}
|
|
140
|
+
if (truncation.maxArrayLength <= 0) {
|
|
141
|
+
throw new Error('console.truncation.maxArrayLength must be positive');
|
|
142
|
+
}
|
|
143
|
+
if (truncation.maxObjectDepth <= 0) {
|
|
144
|
+
throw new Error('console.truncation.maxObjectDepth must be positive');
|
|
145
|
+
}
|
|
146
|
+
// Validate screenshot config
|
|
147
|
+
if (!['png', 'jpeg'].includes(config.screenshots.defaultFormat)) {
|
|
148
|
+
throw new Error('screenshots.defaultFormat must be "png" or "jpeg"');
|
|
149
|
+
}
|
|
150
|
+
if (config.screenshots.quality < 0 || config.screenshots.quality > 100) {
|
|
151
|
+
throw new Error('screenshots.quality must be between 0 and 100');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get configuration with validation
|
|
156
|
+
*/
|
|
157
|
+
export function getConfig() {
|
|
158
|
+
const config = loadConfig();
|
|
159
|
+
validateConfig(config);
|
|
160
|
+
return config;
|
|
161
|
+
}
|