@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.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +816 -0
  3. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
  4. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
  5. package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
  6. package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
  7. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
  8. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
  9. package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
  10. package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
  11. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
  12. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
  13. package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
  14. package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
  15. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
  16. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
  17. package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
  18. package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
  19. package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
  20. package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
  21. package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
  22. package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
  23. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
  24. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
  25. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
  26. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
  27. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
  28. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
  29. package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
  30. package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
  31. package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
  32. package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
  33. package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
  34. package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
  35. package/dist/apps/design-system-dashboard/server.d.ts +24 -0
  36. package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
  37. package/dist/apps/design-system-dashboard/server.js +160 -0
  38. package/dist/apps/design-system-dashboard/server.js.map +1 -0
  39. package/dist/apps/token-browser/server.d.ts +26 -0
  40. package/dist/apps/token-browser/server.d.ts.map +1 -0
  41. package/dist/apps/token-browser/server.js +137 -0
  42. package/dist/apps/token-browser/server.js.map +1 -0
  43. package/dist/browser/base.d.ts +58 -0
  44. package/dist/browser/base.d.ts.map +1 -0
  45. package/dist/browser/base.js +6 -0
  46. package/dist/browser/base.js.map +1 -0
  47. package/dist/browser/local.d.ts +87 -0
  48. package/dist/browser/local.d.ts.map +1 -0
  49. package/dist/browser/local.js +318 -0
  50. package/dist/browser/local.js.map +1 -0
  51. package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
  52. package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
  53. package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
  54. package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
  55. package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
  56. package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
  57. package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
  58. package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
  59. package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
  60. package/dist/cloudflare/apps/token-browser/server.js +136 -0
  61. package/dist/cloudflare/browser/base.js +5 -0
  62. package/dist/cloudflare/browser/cloudflare.js +156 -0
  63. package/dist/cloudflare/browser-manager.js +157 -0
  64. package/dist/cloudflare/core/cloud-websocket-connector.js +267 -0
  65. package/dist/cloudflare/core/cloud-websocket-relay.js +199 -0
  66. package/dist/cloudflare/core/comment-tools.js +292 -0
  67. package/dist/cloudflare/core/config.js +161 -0
  68. package/dist/cloudflare/core/console-monitor.js +427 -0
  69. package/dist/cloudflare/core/design-code-tools.js +2504 -0
  70. package/dist/cloudflare/core/design-system-manifest.js +260 -0
  71. package/dist/cloudflare/core/design-system-tools.js +863 -0
  72. package/dist/cloudflare/core/enrichment/enrichment-service.js +272 -0
  73. package/dist/cloudflare/core/enrichment/index.js +7 -0
  74. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  75. package/dist/cloudflare/core/enrichment/style-resolver.js +326 -0
  76. package/dist/cloudflare/core/figma-api.js +409 -0
  77. package/dist/cloudflare/core/figma-connector.js +7 -0
  78. package/dist/cloudflare/core/figma-desktop-connector.js +1184 -0
  79. package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
  80. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  81. package/dist/cloudflare/core/figma-tools.js +2947 -0
  82. package/dist/cloudflare/core/logger.js +53 -0
  83. package/dist/cloudflare/core/port-discovery.js +282 -0
  84. package/dist/cloudflare/core/snippet-injector.js +96 -0
  85. package/dist/cloudflare/core/types/design-code.js +4 -0
  86. package/dist/cloudflare/core/types/enriched.js +5 -0
  87. package/dist/cloudflare/core/types/index.js +4 -0
  88. package/dist/cloudflare/core/websocket-connector.js +256 -0
  89. package/dist/cloudflare/core/websocket-server.js +646 -0
  90. package/dist/cloudflare/core/write-tools.js +2091 -0
  91. package/dist/cloudflare/index.js +2899 -0
  92. package/dist/cloudflare/test-browser.js +88 -0
  93. package/dist/core/comment-tools.d.ts +11 -0
  94. package/dist/core/comment-tools.d.ts.map +1 -0
  95. package/dist/core/comment-tools.js +293 -0
  96. package/dist/core/comment-tools.js.map +1 -0
  97. package/dist/core/config.d.ts +17 -0
  98. package/dist/core/config.d.ts.map +1 -0
  99. package/dist/core/config.js +162 -0
  100. package/dist/core/config.js.map +1 -0
  101. package/dist/core/console-monitor.d.ts +82 -0
  102. package/dist/core/console-monitor.d.ts.map +1 -0
  103. package/dist/core/console-monitor.js +428 -0
  104. package/dist/core/console-monitor.js.map +1 -0
  105. package/dist/core/design-code-tools.d.ts +127 -0
  106. package/dist/core/design-code-tools.d.ts.map +1 -0
  107. package/dist/core/design-code-tools.js +2505 -0
  108. package/dist/core/design-code-tools.js.map +1 -0
  109. package/dist/core/design-system-manifest.d.ts +272 -0
  110. package/dist/core/design-system-manifest.d.ts.map +1 -0
  111. package/dist/core/design-system-manifest.js +261 -0
  112. package/dist/core/design-system-manifest.js.map +1 -0
  113. package/dist/core/design-system-tools.d.ts +17 -0
  114. package/dist/core/design-system-tools.d.ts.map +1 -0
  115. package/dist/core/design-system-tools.js +864 -0
  116. package/dist/core/design-system-tools.js.map +1 -0
  117. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  118. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  119. package/dist/core/enrichment/enrichment-service.js +273 -0
  120. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  121. package/dist/core/enrichment/index.d.ts +8 -0
  122. package/dist/core/enrichment/index.d.ts.map +1 -0
  123. package/dist/core/enrichment/index.js +8 -0
  124. package/dist/core/enrichment/index.js.map +1 -0
  125. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  126. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  127. package/dist/core/enrichment/relationship-mapper.js +352 -0
  128. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  129. package/dist/core/enrichment/style-resolver.d.ts +80 -0
  130. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  131. package/dist/core/enrichment/style-resolver.js +327 -0
  132. package/dist/core/enrichment/style-resolver.js.map +1 -0
  133. package/dist/core/figma-api.d.ts +201 -0
  134. package/dist/core/figma-api.d.ts.map +1 -0
  135. package/dist/core/figma-api.js +410 -0
  136. package/dist/core/figma-api.js.map +1 -0
  137. package/dist/core/figma-connector.d.ts +48 -0
  138. package/dist/core/figma-connector.d.ts.map +1 -0
  139. package/dist/core/figma-connector.js +8 -0
  140. package/dist/core/figma-connector.js.map +1 -0
  141. package/dist/core/figma-desktop-connector.d.ts +265 -0
  142. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  143. package/dist/core/figma-desktop-connector.js +1184 -0
  144. package/dist/core/figma-desktop-connector.js.map +1 -0
  145. package/dist/core/figma-reconstruction-spec.d.ts +166 -0
  146. package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
  147. package/dist/core/figma-reconstruction-spec.js +403 -0
  148. package/dist/core/figma-reconstruction-spec.js.map +1 -0
  149. package/dist/core/figma-style-extractor.d.ts +76 -0
  150. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  151. package/dist/core/figma-style-extractor.js +312 -0
  152. package/dist/core/figma-style-extractor.js.map +1 -0
  153. package/dist/core/figma-tools.d.ts +23 -0
  154. package/dist/core/figma-tools.d.ts.map +1 -0
  155. package/dist/core/figma-tools.js +2948 -0
  156. package/dist/core/figma-tools.js.map +1 -0
  157. package/dist/core/logger.d.ts +22 -0
  158. package/dist/core/logger.d.ts.map +1 -0
  159. package/dist/core/logger.js +54 -0
  160. package/dist/core/logger.js.map +1 -0
  161. package/dist/core/port-discovery.d.ts +110 -0
  162. package/dist/core/port-discovery.d.ts.map +1 -0
  163. package/dist/core/port-discovery.js +283 -0
  164. package/dist/core/port-discovery.js.map +1 -0
  165. package/dist/core/snippet-injector.d.ts +24 -0
  166. package/dist/core/snippet-injector.d.ts.map +1 -0
  167. package/dist/core/snippet-injector.js +97 -0
  168. package/dist/core/snippet-injector.js.map +1 -0
  169. package/dist/core/types/design-code.d.ts +262 -0
  170. package/dist/core/types/design-code.d.ts.map +1 -0
  171. package/dist/core/types/design-code.js +5 -0
  172. package/dist/core/types/design-code.js.map +1 -0
  173. package/dist/core/types/enriched.d.ts +213 -0
  174. package/dist/core/types/enriched.d.ts.map +1 -0
  175. package/dist/core/types/enriched.js +6 -0
  176. package/dist/core/types/enriched.js.map +1 -0
  177. package/dist/core/types/index.d.ts +112 -0
  178. package/dist/core/types/index.d.ts.map +1 -0
  179. package/dist/core/types/index.js +5 -0
  180. package/dist/core/types/index.js.map +1 -0
  181. package/dist/core/websocket-connector.d.ts +55 -0
  182. package/dist/core/websocket-connector.d.ts.map +1 -0
  183. package/dist/core/websocket-connector.js +257 -0
  184. package/dist/core/websocket-connector.js.map +1 -0
  185. package/dist/core/websocket-server.d.ts +191 -0
  186. package/dist/core/websocket-server.d.ts.map +1 -0
  187. package/dist/core/websocket-server.js +647 -0
  188. package/dist/core/websocket-server.js.map +1 -0
  189. package/dist/core/write-tools.d.ts +7 -0
  190. package/dist/core/write-tools.d.ts.map +1 -0
  191. package/dist/core/write-tools.js +2092 -0
  192. package/dist/core/write-tools.js.map +1 -0
  193. package/dist/local.d.ts +84 -0
  194. package/dist/local.d.ts.map +1 -0
  195. package/dist/local.js +5039 -0
  196. package/dist/local.js.map +1 -0
  197. package/figma-desktop-bridge/README.md +313 -0
  198. package/figma-desktop-bridge/code.js +2818 -0
  199. package/figma-desktop-bridge/manifest.json +67 -0
  200. package/figma-desktop-bridge/ui.html +1236 -0
  201. 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
+ }