@atezer/figma-mcp-bridge 1.4.0 → 1.4.1

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 (34) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +10 -2
  3. package/dist/browser/base.d.ts +0 -50
  4. package/dist/browser/base.d.ts.map +0 -1
  5. package/dist/browser/base.js +0 -6
  6. package/dist/browser/base.js.map +0 -1
  7. package/dist/browser/local.d.ts +0 -81
  8. package/dist/browser/local.d.ts.map +0 -1
  9. package/dist/browser/local.js +0 -283
  10. package/dist/browser/local.js.map +0 -1
  11. package/dist/cloudflare/browser/base.js +0 -5
  12. package/dist/cloudflare/browser/cloudflare.js +0 -156
  13. package/dist/cloudflare/browser-manager.js +0 -157
  14. package/dist/cloudflare/core/audit-log.js +0 -62
  15. package/dist/cloudflare/core/config.js +0 -163
  16. package/dist/cloudflare/core/console-monitor.js +0 -427
  17. package/dist/cloudflare/core/design-system-manifest.js +0 -260
  18. package/dist/cloudflare/core/enrichment/enrichment-service.js +0 -272
  19. package/dist/cloudflare/core/enrichment/index.js +0 -7
  20. package/dist/cloudflare/core/enrichment/relationship-mapper.js +0 -351
  21. package/dist/cloudflare/core/enrichment/style-resolver.js +0 -326
  22. package/dist/cloudflare/core/figma-api.js +0 -273
  23. package/dist/cloudflare/core/figma-desktop-connector.js +0 -1041
  24. package/dist/cloudflare/core/figma-reconstruction-spec.js +0 -402
  25. package/dist/cloudflare/core/figma-tools.js +0 -2919
  26. package/dist/cloudflare/core/figma-url.js +0 -48
  27. package/dist/cloudflare/core/logger.js +0 -53
  28. package/dist/cloudflare/core/plugin-bridge-connector.js +0 -197
  29. package/dist/cloudflare/core/plugin-bridge-server.js +0 -375
  30. package/dist/cloudflare/core/snippet-injector.js +0 -96
  31. package/dist/cloudflare/core/types/enriched.js +0 -5
  32. package/dist/cloudflare/core/types/index.js +0 -4
  33. package/dist/cloudflare/index.js +0 -1061
  34. package/dist/cloudflare/test-browser.js +0 -88
@@ -1,1061 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * F-MCP ATezer (Figma MCP Bridge) Server
4
- * Entry point for the MCP server that enables AI assistants to access
5
- * Figma plugin console logs and screenshots.
6
- *
7
- * This implementation uses Cloudflare's McpAgent pattern for deployment
8
- * on Cloudflare Workers with Browser Rendering API support.
9
- */
10
- import { McpAgent } from "agents/mcp";
11
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
- import { z } from "zod";
13
- import { BrowserManager } from "./browser-manager.js";
14
- import { ConsoleMonitor } from "./core/console-monitor.js";
15
- import { getConfig } from "./core/config.js";
16
- import { createChildLogger } from "./core/logger.js";
17
- import { testBrowserRendering } from "./test-browser.js";
18
- import { FigmaAPI, extractFileKey } from "./core/figma-api.js";
19
- import { registerFigmaAPITools } from "./core/figma-tools.js";
20
- const logger = createChildLogger({ component: "mcp-server" });
21
- /**
22
- * F-MCP ATezer Agent
23
- * Extends McpAgent to provide Figma-specific debugging tools
24
- */
25
- export class FigmaMCP extends McpAgent {
26
- constructor() {
27
- super(...arguments);
28
- this.server = new McpServer({
29
- name: "F-MCP ATezer",
30
- version: "0.1.0",
31
- });
32
- this.browserManager = null;
33
- this.consoleMonitor = null;
34
- this.figmaAPI = null;
35
- this.config = getConfig();
36
- this.sessionId = null;
37
- }
38
- /**
39
- * Refresh an expired OAuth token using the refresh token
40
- */
41
- async refreshOAuthToken(sessionId, refreshToken) {
42
- const env = this.env;
43
- if (!env.FIGMA_OAUTH_CLIENT_ID || !env.FIGMA_OAUTH_CLIENT_SECRET) {
44
- throw new Error("OAuth not configured on server");
45
- }
46
- logger.info({ sessionId }, "Attempting to refresh OAuth token");
47
- const credentials = btoa(`${env.FIGMA_OAUTH_CLIENT_ID}:${env.FIGMA_OAUTH_CLIENT_SECRET}`);
48
- const tokenParams = new URLSearchParams({
49
- grant_type: "refresh_token",
50
- refresh_token: refreshToken
51
- });
52
- const tokenResponse = await fetch("https://api.figma.com/v1/oauth/token", {
53
- method: "POST",
54
- headers: {
55
- "Content-Type": "application/x-www-form-urlencoded",
56
- "Authorization": `Basic ${credentials}`
57
- },
58
- body: tokenParams.toString()
59
- });
60
- if (!tokenResponse.ok) {
61
- const errorData = await tokenResponse.json().catch(() => ({}));
62
- logger.error({ errorData, status: tokenResponse.status }, "Token refresh failed");
63
- throw new Error(`Token refresh failed: ${JSON.stringify(errorData)}`);
64
- }
65
- const tokenData = await tokenResponse.json();
66
- // Store refreshed token in KV
67
- const tokenKey = `oauth_token:${sessionId}`;
68
- const storedToken = {
69
- accessToken: tokenData.access_token,
70
- refreshToken: tokenData.refresh_token || refreshToken, // Use new refresh token or keep existing
71
- expiresAt: Date.now() + (tokenData.expires_in * 1000)
72
- };
73
- await env.OAUTH_TOKENS.put(tokenKey, JSON.stringify(storedToken), {
74
- expirationTtl: tokenData.expires_in
75
- });
76
- logger.info({ sessionId }, "OAuth token refreshed successfully");
77
- return storedToken;
78
- }
79
- /**
80
- * Generate a cryptographically secure random state token for CSRF protection
81
- */
82
- static generateStateToken() {
83
- const array = new Uint8Array(32);
84
- crypto.getRandomValues(array);
85
- return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
86
- }
87
- /**
88
- * Load or create persistent session ID from Durable Object storage
89
- * Uses a fixed session ID for the MCP server to ensure OAuth tokens persist across reconnections
90
- */
91
- async ensureSessionId() {
92
- if (this.sessionId) {
93
- return; // Already loaded
94
- }
95
- // IMPORTANT: Use a fixed session ID for all MCP connections
96
- // This ensures OAuth tokens persist across MCP server reconnections
97
- // Each user of this MCP server will share the same OAuth token
98
- const FIXED_SESSION_ID = "figma-mcp-bridge-default-session";
99
- // Try to load from Durable Object storage
100
- // @ts-ignore - this.ctx is available in Durable Object context
101
- const storage = this.ctx?.storage;
102
- if (storage) {
103
- try {
104
- const storedSessionId = await storage.get('sessionId');
105
- if (storedSessionId) {
106
- this.sessionId = storedSessionId;
107
- logger.info({ sessionId: this.sessionId }, "Loaded persistent session ID from storage");
108
- return;
109
- }
110
- else {
111
- // Store the fixed session ID
112
- this.sessionId = FIXED_SESSION_ID;
113
- await storage.put('sessionId', this.sessionId);
114
- logger.info({ sessionId: this.sessionId }, "Initialized fixed session ID");
115
- return;
116
- }
117
- }
118
- catch (e) {
119
- logger.warn({ error: e }, "Failed to access Durable Object storage for session ID");
120
- }
121
- }
122
- // Fallback: use fixed session ID directly
123
- this.sessionId = FIXED_SESSION_ID;
124
- logger.info({ sessionId: this.sessionId }, "Using fixed session ID (storage unavailable)");
125
- }
126
- /**
127
- * Get session ID for this Durable Object instance
128
- * Returns the session ID loaded by ensureSessionId()
129
- */
130
- getSessionId() {
131
- if (!this.sessionId) {
132
- // This shouldn't happen if ensureSessionId() was called, but provide fallback
133
- this.sessionId = FigmaMCP.generateStateToken();
134
- logger.warn({ sessionId: this.sessionId }, "Session ID not initialized, generated ephemeral ID");
135
- }
136
- return this.sessionId;
137
- }
138
- /**
139
- * Get or create Figma API client with OAuth token from session
140
- */
141
- async getFigmaAPI() {
142
- // Ensure session ID is loaded from storage
143
- await this.ensureSessionId();
144
- // @ts-ignore - this.env is available in Agent/Durable Object context
145
- const env = this.env;
146
- // Try OAuth first (per-user authentication)
147
- try {
148
- const sessionId = this.getSessionId();
149
- logger.info({ sessionId }, "Attempting to retrieve OAuth token from KV");
150
- // Retrieve token from KV (accessible across all Durable Object instances)
151
- const tokenKey = `oauth_token:${sessionId}`;
152
- const tokenJson = await env.OAUTH_TOKENS.get(tokenKey);
153
- if (!tokenJson) {
154
- logger.warn({ sessionId, tokenKey }, "No OAuth token found in KV");
155
- throw new Error("No token found");
156
- }
157
- let tokenData = JSON.parse(tokenJson);
158
- logger.info({
159
- sessionId,
160
- hasToken: !!tokenData?.accessToken,
161
- expiresAt: tokenData?.expiresAt,
162
- isExpired: tokenData?.expiresAt ? Date.now() > tokenData.expiresAt : null
163
- }, "Token retrieval result from KV");
164
- if (tokenData?.accessToken) {
165
- // Check if token is expired or will expire soon (within 5 minutes)
166
- const isExpired = tokenData.expiresAt && Date.now() > tokenData.expiresAt;
167
- const willExpireSoon = tokenData.expiresAt && Date.now() > (tokenData.expiresAt - 5 * 60 * 1000);
168
- if (isExpired || willExpireSoon) {
169
- if (tokenData.refreshToken) {
170
- try {
171
- // Attempt to refresh the token
172
- tokenData = await this.refreshOAuthToken(sessionId, tokenData.refreshToken);
173
- logger.info({ sessionId }, "Successfully refreshed expired/expiring token");
174
- }
175
- catch (refreshError) {
176
- logger.error({ sessionId, refreshError }, "Failed to refresh token");
177
- throw new Error("Token expired and refresh failed. Please re-authenticate.");
178
- }
179
- }
180
- else {
181
- logger.warn({ sessionId }, "Token expired but no refresh token available");
182
- throw new Error("Token expired. Please re-authenticate.");
183
- }
184
- }
185
- logger.info({ sessionId }, "Using OAuth token from KV for Figma API");
186
- return new FigmaAPI({ accessToken: tokenData.accessToken });
187
- }
188
- logger.warn({ sessionId }, "OAuth token exists in KV but missing accessToken");
189
- throw new Error("Invalid token data");
190
- }
191
- catch (error) {
192
- const errorMessage = error instanceof Error ? error.message : String(error);
193
- const sessionId = this.getSessionId();
194
- // Check if this is a "no token found" error (user hasn't authenticated yet)
195
- if (errorMessage.includes("No token found")) {
196
- logger.info({ sessionId }, "No OAuth token found - user needs to authenticate");
197
- // No authentication available - direct user to OAuth flow
198
- const baseUrl = this.env.MCP_OAUTH_BASE_URL || "https://your-deployment.workers.dev";
199
- const authUrl = `${baseUrl.replace(/\/$/, "")}/oauth/authorize?session_id=${sessionId}`;
200
- // Only use PAT fallback if explicitly configured AND no OAuth token exists
201
- if (env?.FIGMA_ACCESS_TOKEN) {
202
- logger.warn("FIGMA_ACCESS_TOKEN fallback is deprecated. User should authenticate via OAuth for proper per-user authentication.");
203
- return new FigmaAPI({ accessToken: env.FIGMA_ACCESS_TOKEN });
204
- }
205
- throw new Error(JSON.stringify({
206
- error: "authentication_required",
207
- message: "Please authenticate with Figma to use API features",
208
- auth_url: authUrl,
209
- instructions: "Your browser will open automatically to complete authentication. If it doesn't, copy the auth_url and open it manually."
210
- }));
211
- }
212
- // For other OAuth errors (expired token, refresh failed, etc.), do NOT fall back to PAT
213
- logger.error({ error, sessionId }, "OAuth token retrieval failed - re-authentication required");
214
- const baseUrl = this.env.MCP_OAUTH_BASE_URL || "https://your-deployment.workers.dev";
215
- const authUrl = `${baseUrl.replace(/\/$/, "")}/oauth/authorize?session_id=${sessionId}`;
216
- throw new Error(JSON.stringify({
217
- error: "oauth_error",
218
- message: errorMessage,
219
- auth_url: authUrl,
220
- instructions: "Please re-authenticate with Figma. Your browser will open automatically."
221
- }));
222
- }
223
- }
224
- /**
225
- * Initialize browser and console monitoring
226
- */
227
- async ensureInitialized() {
228
- try {
229
- // Ensure session ID is loaded from storage first
230
- await this.ensureSessionId();
231
- if (!this.browserManager) {
232
- logger.info("Initializing BrowserManager");
233
- // Access env from Durable Object context
234
- // @ts-ignore - this.env is available in Agent/Durable Object context
235
- const env = this.env;
236
- if (!env) {
237
- throw new Error("Environment not available - this.env is undefined");
238
- }
239
- if (!env.BROWSER) {
240
- throw new Error("BROWSER binding not found in environment. Check wrangler.jsonc configuration.");
241
- }
242
- logger.info("Creating BrowserManager with BROWSER binding");
243
- this.browserManager = new BrowserManager(env, this.config.browser);
244
- }
245
- if (!this.consoleMonitor) {
246
- logger.info("Initializing ConsoleMonitor");
247
- this.consoleMonitor = new ConsoleMonitor(this.config.console);
248
- // Start browser and begin monitoring
249
- logger.info("Getting browser page");
250
- const page = await this.browserManager.getPage();
251
- logger.info("Starting console monitoring");
252
- await this.consoleMonitor.startMonitoring(page);
253
- logger.info("Browser and console monitor initialized successfully");
254
- }
255
- }
256
- catch (error) {
257
- logger.error({ error }, "Failed to initialize browser/monitor");
258
- throw new Error(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`);
259
- }
260
- }
261
- async init() {
262
- // Tool 1: Get Console Logs
263
- this.server.tool("figma_get_console_logs", "Retrieve console logs from Figma. Captures all plugin console output including [Main], [Swapper], etc. prefixes. Call figma_navigate first to initialize browser monitoring.", {
264
- count: z.number().optional().default(100).describe("Number of recent logs to retrieve"),
265
- level: z
266
- .enum(["log", "info", "warn", "error", "debug", "all"])
267
- .optional()
268
- .default("all")
269
- .describe("Filter by log level"),
270
- since: z
271
- .number()
272
- .optional()
273
- .describe("Only logs after this timestamp (Unix ms)"),
274
- }, async ({ count, level, since }) => {
275
- try {
276
- await this.ensureInitialized();
277
- if (!this.consoleMonitor) {
278
- throw new Error("Console monitor not initialized");
279
- }
280
- const logs = this.consoleMonitor.getLogs({
281
- count,
282
- level,
283
- since,
284
- });
285
- // Add AI instruction when no logs are found
286
- const responseData = {
287
- logs,
288
- totalCount: logs.length,
289
- oldestTimestamp: logs[0]?.timestamp,
290
- newestTimestamp: logs[logs.length - 1]?.timestamp,
291
- status: this.consoleMonitor.getStatus(),
292
- };
293
- // If no logs found, add helpful AI instruction
294
- if (logs.length === 0) {
295
- responseData.ai_instruction = "No console logs found. This usually means the Figma plugin hasn't run since monitoring started. Please inform the user: 'No console logs found yet. Try running your Figma plugin now, then I'll check for logs again.' The MCP only captures logs AFTER monitoring starts - it cannot retrieve historical logs from before the browser connected.";
296
- }
297
- return {
298
- content: [
299
- {
300
- type: "text",
301
- text: JSON.stringify(responseData, null, 2),
302
- },
303
- ],
304
- };
305
- }
306
- catch (error) {
307
- logger.error({ error }, "Failed to get console logs");
308
- const errorMessage = error instanceof Error ? error.message : String(error);
309
- return {
310
- content: [
311
- {
312
- type: "text",
313
- text: JSON.stringify({
314
- error: errorMessage,
315
- message: "Failed to retrieve console logs. Make sure to call figma_navigate first to initialize the browser.",
316
- hint: "Try: figma_navigate({ url: 'https://www.figma.com/design/your-file' })",
317
- }, null, 2),
318
- },
319
- ],
320
- isError: true,
321
- };
322
- }
323
- });
324
- // Tool 2: Take Screenshot (using Figma REST API)
325
- // Note: For screenshots of specific components, use figma_get_component_image instead
326
- this.server.tool("figma_take_screenshot", "Export an image of the currently viewed Figma page or specific node using Figma's REST API. Returns an image URL (valid for 30 days). For specific components, use figma_get_component_image instead.", {
327
- nodeId: z
328
- .string()
329
- .optional()
330
- .describe("Optional node ID to screenshot. If not provided, uses the currently viewed page/frame from the browser URL."),
331
- scale: z
332
- .number()
333
- .min(0.01)
334
- .max(4)
335
- .optional()
336
- .default(2)
337
- .describe("Image scale factor (0.01-4, default: 2 for high quality)"),
338
- format: z
339
- .enum(["png", "jpg", "svg", "pdf"])
340
- .optional()
341
- .default("png")
342
- .describe("Image format (default: png)"),
343
- }, async ({ nodeId, scale, format }) => {
344
- try {
345
- const api = await this.getFigmaAPI();
346
- // Get current URL to extract file key and node ID if not provided
347
- const currentUrl = this.browserManager?.getCurrentUrl() || null;
348
- if (!currentUrl) {
349
- throw new Error("No Figma file open. Either provide a nodeId parameter or call figma_navigate first to open a Figma file.");
350
- }
351
- const fileKey = extractFileKey(currentUrl);
352
- if (!fileKey) {
353
- throw new Error(`Invalid Figma URL: ${currentUrl}`);
354
- }
355
- // Extract node ID from URL if not provided
356
- let targetNodeId = nodeId;
357
- if (!targetNodeId) {
358
- const urlObj = new URL(currentUrl);
359
- const nodeIdParam = urlObj.searchParams.get('node-id');
360
- if (nodeIdParam) {
361
- // Convert 123-456 to 123:456
362
- targetNodeId = nodeIdParam.replace(/-/g, ':');
363
- }
364
- else {
365
- throw new Error("No node ID found. Either provide nodeId parameter or ensure the Figma URL contains a node-id parameter (e.g., ?node-id=123-456)");
366
- }
367
- }
368
- logger.info({ fileKey, nodeId: targetNodeId, scale, format }, "Rendering image via Figma API");
369
- // Use Figma REST API to get image
370
- const result = await api.getImages(fileKey, targetNodeId, {
371
- scale,
372
- format: format === 'jpg' ? 'jpg' : format, // normalize jpeg -> jpg
373
- contents_only: true,
374
- });
375
- const imageUrl = result.images[targetNodeId];
376
- if (!imageUrl) {
377
- throw new Error(`Failed to render image for node ${targetNodeId}. The node may not exist or may not be renderable.`);
378
- }
379
- return {
380
- content: [
381
- {
382
- type: "text",
383
- text: JSON.stringify({
384
- fileKey,
385
- nodeId: targetNodeId,
386
- imageUrl,
387
- scale,
388
- format,
389
- expiresIn: "30 days",
390
- note: "Image URL provided above. Use this URL to view or download the screenshot. URLs expire after 30 days.",
391
- }, null, 2),
392
- },
393
- ],
394
- };
395
- }
396
- catch (error) {
397
- logger.error({ error }, "Failed to capture screenshot");
398
- const errorMessage = error instanceof Error ? error.message : String(error);
399
- return {
400
- content: [
401
- {
402
- type: "text",
403
- text: JSON.stringify({
404
- error: errorMessage,
405
- message: "Failed to capture screenshot via Figma API",
406
- hint: "Make sure you've called figma_navigate to open a file, or provide a valid nodeId parameter",
407
- }, null, 2),
408
- },
409
- ],
410
- isError: true,
411
- };
412
- }
413
- });
414
- // Tool 3: Watch Console (Real-time streaming)
415
- this.server.tool("figma_watch_console", {
416
- duration: z
417
- .number()
418
- .optional()
419
- .default(30)
420
- .describe("How long to watch in seconds"),
421
- level: z
422
- .enum(["log", "info", "warn", "error", "debug", "all"])
423
- .optional()
424
- .default("all")
425
- .describe("Filter by log level"),
426
- }, async ({ duration, level }) => {
427
- await this.ensureInitialized();
428
- if (!this.consoleMonitor) {
429
- throw new Error("Console monitor not initialized. Call figma_navigate first.");
430
- }
431
- const consoleMonitor = this.consoleMonitor;
432
- if (!consoleMonitor.getStatus().isMonitoring) {
433
- throw new Error("Console monitoring not active. Call figma_navigate first.");
434
- }
435
- const startTime = Date.now();
436
- const endTime = startTime + duration * 1000;
437
- const startLogCount = consoleMonitor.getStatus().logCount;
438
- // Wait for the specified duration while collecting logs
439
- await new Promise(resolve => setTimeout(resolve, duration * 1000));
440
- // Get logs captured during watch period
441
- const watchedLogs = consoleMonitor.getLogs({
442
- level: level === 'all' ? undefined : level,
443
- since: startTime,
444
- });
445
- const endLogCount = consoleMonitor.getStatus().logCount;
446
- const newLogsCount = endLogCount - startLogCount;
447
- return {
448
- content: [
449
- {
450
- type: "text",
451
- text: JSON.stringify({
452
- status: "completed",
453
- duration: `${duration} seconds`,
454
- startTime: new Date(startTime).toISOString(),
455
- endTime: new Date(endTime).toISOString(),
456
- filter: level,
457
- statistics: {
458
- totalLogsInBuffer: endLogCount,
459
- logsAddedDuringWatch: newLogsCount,
460
- logsMatchingFilter: watchedLogs.length,
461
- },
462
- logs: watchedLogs,
463
- }, null, 2),
464
- },
465
- ],
466
- };
467
- });
468
- // Tool 4: Reload Plugin
469
- this.server.tool("figma_reload_plugin", {
470
- clearConsole: z
471
- .boolean()
472
- .optional()
473
- .default(true)
474
- .describe("Clear console logs before reload"),
475
- }, async ({ clearConsole: clearConsoleBefore }) => {
476
- try {
477
- await this.ensureInitialized();
478
- if (!this.browserManager) {
479
- throw new Error("Browser manager not initialized");
480
- }
481
- // Clear console buffer if requested
482
- let clearedCount = 0;
483
- if (clearConsoleBefore && this.consoleMonitor) {
484
- clearedCount = this.consoleMonitor.clear();
485
- }
486
- // Reload the page
487
- await this.browserManager.reload();
488
- const currentUrl = this.browserManager.getCurrentUrl();
489
- return {
490
- content: [
491
- {
492
- type: "text",
493
- text: JSON.stringify({
494
- status: "reloaded",
495
- timestamp: Date.now(),
496
- url: currentUrl,
497
- consoleCleared: clearConsoleBefore,
498
- clearedCount: clearConsoleBefore ? clearedCount : 0,
499
- }, null, 2),
500
- },
501
- ],
502
- };
503
- }
504
- catch (error) {
505
- logger.error({ error }, "Failed to reload plugin");
506
- return {
507
- content: [
508
- {
509
- type: "text",
510
- text: JSON.stringify({
511
- error: String(error),
512
- message: "Failed to reload plugin",
513
- }, null, 2),
514
- },
515
- ],
516
- isError: true,
517
- };
518
- }
519
- });
520
- // Tool 5: Clear Console
521
- this.server.tool("figma_clear_console", {}, async () => {
522
- try {
523
- await this.ensureInitialized();
524
- if (!this.consoleMonitor) {
525
- throw new Error("Console monitor not initialized");
526
- }
527
- const clearedCount = this.consoleMonitor.clear();
528
- return {
529
- content: [
530
- {
531
- type: "text",
532
- text: JSON.stringify({
533
- status: "cleared",
534
- clearedCount,
535
- timestamp: Date.now(),
536
- ai_instruction: "⚠️ CRITICAL: Console cleared successfully, but this operation disrupts the monitoring connection. You MUST reconnect the MCP server using `/mcp reconnect figma-mcp-bridge` before calling figma_get_console_logs again. Best practice: Avoid clearing console - filter/parse logs instead to maintain monitoring connection.",
537
- }, null, 2),
538
- },
539
- ],
540
- };
541
- }
542
- catch (error) {
543
- logger.error({ error }, "Failed to clear console");
544
- return {
545
- content: [
546
- {
547
- type: "text",
548
- text: JSON.stringify({
549
- error: String(error),
550
- message: "Failed to clear console buffer",
551
- }, null, 2),
552
- },
553
- ],
554
- isError: true,
555
- };
556
- }
557
- });
558
- // Tool 6: Navigate to Figma
559
- this.server.tool("figma_navigate", {
560
- url: z
561
- .string()
562
- .url()
563
- .describe("Figma URL to navigate to (e.g., https://www.figma.com/design/abc123)"),
564
- }, async ({ url }) => {
565
- try {
566
- await this.ensureInitialized();
567
- if (!this.browserManager) {
568
- throw new Error("Browser manager not initialized");
569
- }
570
- // Navigate to the URL
571
- await this.browserManager.navigateToFigma(url);
572
- // Give page time to load and start capturing logs
573
- await new Promise((resolve) => setTimeout(resolve, 2000));
574
- const currentUrl = this.browserManager.getCurrentUrl();
575
- return {
576
- content: [
577
- {
578
- type: "text",
579
- text: JSON.stringify({
580
- status: "navigated",
581
- url: currentUrl,
582
- timestamp: Date.now(),
583
- message: "Browser navigated to Figma. Console monitoring is active.",
584
- }, null, 2),
585
- },
586
- ],
587
- };
588
- }
589
- catch (error) {
590
- logger.error({ error }, "Failed to navigate to Figma");
591
- const errorMessage = error instanceof Error ? error.message : String(error);
592
- return {
593
- content: [
594
- {
595
- type: "text",
596
- text: JSON.stringify({
597
- error: errorMessage,
598
- message: "Failed to navigate to Figma URL",
599
- details: errorMessage.includes("BROWSER")
600
- ? "Browser Rendering API binding is missing. This is a configuration issue."
601
- : "Unable to launch browser or navigate to URL.",
602
- troubleshooting: [
603
- "Verify the Figma URL is valid and accessible",
604
- "Check that the Browser Rendering API is properly configured in wrangler.jsonc",
605
- "Try again in a few moments if this is a temporary issue"
606
- ]
607
- }, null, 2),
608
- },
609
- ],
610
- isError: true,
611
- };
612
- }
613
- });
614
- // Tool 7: Get Status
615
- this.server.tool("figma_get_status", {}, async () => {
616
- try {
617
- const browserRunning = this.browserManager?.isRunning() ?? false;
618
- const monitorStatus = this.consoleMonitor?.getStatus() ?? null;
619
- const currentUrl = this.browserManager?.getCurrentUrl() ?? null;
620
- return {
621
- content: [
622
- {
623
- type: "text",
624
- text: JSON.stringify({
625
- browser: {
626
- running: browserRunning,
627
- currentUrl,
628
- },
629
- consoleMonitor: monitorStatus,
630
- initialized: this.browserManager !== null && this.consoleMonitor !== null,
631
- timestamp: Date.now(),
632
- }, null, 2),
633
- },
634
- ],
635
- };
636
- }
637
- catch (error) {
638
- logger.error({ error }, "Failed to get status");
639
- return {
640
- content: [
641
- {
642
- type: "text",
643
- text: JSON.stringify({
644
- error: String(error),
645
- message: "Failed to retrieve status",
646
- }, null, 2),
647
- },
648
- ],
649
- isError: true,
650
- };
651
- }
652
- });
653
- // Register Figma API tools (Tools 8-14)
654
- registerFigmaAPITools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, () => this.consoleMonitor || null, () => this.browserManager || null, () => this.ensureInitialized());
655
- }
656
- }
657
- /**
658
- * Cloudflare Workers fetch handler
659
- * Routes requests to appropriate MCP endpoints
660
- */
661
- export default {
662
- async fetch(request, env, ctx) {
663
- const url = new URL(request.url);
664
- // SSE endpoint for remote MCP clients
665
- if (url.pathname === "/sse" || url.pathname === "/sse/message") {
666
- return FigmaMCP.serveSSE("/sse").fetch(request, env, ctx);
667
- }
668
- // HTTP endpoint for direct MCP communication
669
- if (url.pathname === "/mcp") {
670
- return FigmaMCP.serve("/mcp").fetch(request, env, ctx);
671
- }
672
- // OAuth authorization initiation
673
- if (url.pathname === "/oauth/authorize") {
674
- const sessionId = url.searchParams.get("session_id");
675
- if (!sessionId) {
676
- return new Response("Missing session_id parameter", { status: 400 });
677
- }
678
- // Check if OAuth credentials are configured
679
- if (!env.FIGMA_OAUTH_CLIENT_ID) {
680
- return new Response(JSON.stringify({
681
- error: "OAuth not configured",
682
- message: "Server administrator needs to configure FIGMA_OAUTH_CLIENT_ID",
683
- docs: "https://github.com/atezer/FMCP#oauth-setup"
684
- }), {
685
- status: 500,
686
- headers: { "Content-Type": "application/json" }
687
- });
688
- }
689
- // Generate cryptographically secure state token for CSRF protection
690
- const stateToken = FigmaMCP.generateStateToken();
691
- // Store state token with sessionId in KV (10 minute expiration)
692
- await env.OAUTH_STATE.put(stateToken, sessionId, {
693
- expirationTtl: 600 // 10 minutes
694
- });
695
- const redirectUri = `${url.origin}/oauth/callback`;
696
- const figmaAuthUrl = new URL("https://www.figma.com/oauth");
697
- figmaAuthUrl.searchParams.set("client_id", env.FIGMA_OAUTH_CLIENT_ID);
698
- figmaAuthUrl.searchParams.set("redirect_uri", redirectUri);
699
- figmaAuthUrl.searchParams.set("scope", "file_content:read,library_content:read,file_variables:read");
700
- figmaAuthUrl.searchParams.set("state", stateToken);
701
- figmaAuthUrl.searchParams.set("response_type", "code");
702
- return Response.redirect(figmaAuthUrl.toString(), 302);
703
- }
704
- // OAuth callback handler
705
- if (url.pathname === "/oauth/callback") {
706
- const code = url.searchParams.get("code");
707
- const stateToken = url.searchParams.get("state");
708
- const error = url.searchParams.get("error");
709
- // Handle OAuth errors
710
- if (error) {
711
- return new Response(`<html><body>
712
- <h1>❌ Authentication Failed</h1>
713
- <p>Error: ${error}</p>
714
- <p>Description: ${url.searchParams.get("error_description") || "Unknown error"}</p>
715
- <p>You can close this window and try again.</p>
716
- </body></html>`, {
717
- status: 400,
718
- headers: { "Content-Type": "text/html" }
719
- });
720
- }
721
- if (!code || !stateToken) {
722
- return new Response("Missing code or state parameter", { status: 400 });
723
- }
724
- // Validate state token (CSRF protection)
725
- const sessionId = await env.OAUTH_STATE.get(stateToken);
726
- if (!sessionId) {
727
- return new Response(`<html><body>
728
- <h1>❌ Invalid or Expired Request</h1>
729
- <p>The authentication request has expired or is invalid.</p>
730
- <p>Please try authenticating again.</p>
731
- </body></html>`, {
732
- status: 400,
733
- headers: { "Content-Type": "text/html" }
734
- });
735
- }
736
- // Delete state token after validation (one-time use)
737
- await env.OAUTH_STATE.delete(stateToken);
738
- try {
739
- // Exchange authorization code for access token
740
- // Use Basic auth in Authorization header (Figma's recommended method)
741
- const credentials = btoa(`${env.FIGMA_OAUTH_CLIENT_ID}:${env.FIGMA_OAUTH_CLIENT_SECRET}`);
742
- const tokenParams = new URLSearchParams({
743
- redirect_uri: `${url.origin}/oauth/callback`,
744
- code,
745
- grant_type: "authorization_code"
746
- });
747
- const tokenResponse = await fetch("https://api.figma.com/v1/oauth/token", {
748
- method: "POST",
749
- headers: {
750
- "Content-Type": "application/x-www-form-urlencoded",
751
- "Authorization": `Basic ${credentials}`
752
- },
753
- body: tokenParams.toString()
754
- });
755
- if (!tokenResponse.ok) {
756
- const errorText = await tokenResponse.text();
757
- let errorData;
758
- try {
759
- errorData = JSON.parse(errorText);
760
- }
761
- catch {
762
- errorData = { error: "Unknown error", raw: errorText, status: tokenResponse.status };
763
- }
764
- logger.error({ errorData, status: tokenResponse.status }, "Token exchange failed");
765
- throw new Error(`Token exchange failed: ${JSON.stringify(errorData)}`);
766
- }
767
- const tokenData = await tokenResponse.json();
768
- const accessToken = tokenData.access_token;
769
- const refreshToken = tokenData.refresh_token;
770
- const expiresIn = tokenData.expires_in;
771
- logger.info({
772
- sessionId,
773
- hasAccessToken: !!accessToken,
774
- accessTokenPreview: accessToken ? accessToken.substring(0, 10) + "..." : null,
775
- hasRefreshToken: !!refreshToken,
776
- expiresIn
777
- }, "Token exchange successful");
778
- // IMPORTANT: Use KV storage for tokens since Durable Object storage is instance-specific
779
- // Store token in Workers KV so it's accessible across all Durable Object instances
780
- const tokenKey = `oauth_token:${sessionId}`;
781
- const storedToken = {
782
- accessToken,
783
- refreshToken,
784
- expiresAt: Date.now() + (expiresIn * 1000)
785
- };
786
- // Store in KV with 90-day expiration (matching token lifetime)
787
- await env.OAUTH_TOKENS.put(tokenKey, JSON.stringify(storedToken), {
788
- expirationTtl: expiresIn
789
- });
790
- logger.info({ sessionId, tokenKey }, "Token stored successfully in KV");
791
- return new Response(`<!DOCTYPE html>
792
- <html>
793
- <head>
794
- <meta charset="UTF-8">
795
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
796
- <title>Authentication Successful</title>
797
- <link rel="icon" type="image/jpeg" href="https://p198.p4.n0.cdn.zight.com/items/Qwu1Dywx/b61b7b8f-05dc-4063-8a40-53fa4f8e3e97.jpg">
798
- <style>
799
- * {
800
- margin: 0;
801
- padding: 0;
802
- box-sizing: border-box;
803
- }
804
- body {
805
- font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
806
- background: #ffffff;
807
- color: #000000;
808
- display: flex;
809
- align-items: center;
810
- justify-content: center;
811
- min-height: 100vh;
812
- padding: 24px;
813
- }
814
- .container {
815
- max-width: 480px;
816
- text-align: center;
817
- }
818
- .icon {
819
- width: 64px;
820
- height: 64px;
821
- margin: 0 auto 24px;
822
- background: #18a0fb;
823
- border-radius: 50%;
824
- display: flex;
825
- align-items: center;
826
- justify-content: center;
827
- font-size: 32px;
828
- color: white;
829
- }
830
- h1 {
831
- font-size: 32px;
832
- font-weight: 700;
833
- margin-bottom: 16px;
834
- letter-spacing: -0.02em;
835
- }
836
- p {
837
- font-size: 16px;
838
- color: #666666;
839
- line-height: 1.6;
840
- margin-bottom: 32px;
841
- }
842
- .button {
843
- display: inline-block;
844
- padding: 12px 24px;
845
- background: #000000;
846
- color: #ffffff;
847
- text-decoration: none;
848
- border-radius: 8px;
849
- font-weight: 500;
850
- font-size: 16px;
851
- border: none;
852
- cursor: pointer;
853
- transition: background 0.2s;
854
- }
855
- .button:hover {
856
- background: #333333;
857
- }
858
- .footer {
859
- margin-top: 48px;
860
- font-size: 14px;
861
- color: #999999;
862
- }
863
- </style>
864
- </head>
865
- <body>
866
- <div class="container">
867
- <div class="icon">✓</div>
868
- <h1>Authentication successful</h1>
869
- <p>You've successfully connected F-MCP ATezer to your Figma account. You can now close this window and return to Claude.</p>
870
- <button class="button" onclick="window.close()">Close this window</button>
871
- <div class="footer">This window will automatically close in 5 seconds</div>
872
- </div>
873
- <script>
874
- setTimeout(() => window.close(), 5000);
875
- </script>
876
- </body>
877
- </html>`, {
878
- headers: {
879
- "Content-Type": "text/html; charset=utf-8"
880
- }
881
- });
882
- }
883
- catch (error) {
884
- logger.error({ error, sessionId }, "OAuth callback failed");
885
- return new Response(`<html><body>
886
- <h1>❌ Authentication Error</h1>
887
- <p>Failed to complete authentication: ${error instanceof Error ? error.message : String(error)}</p>
888
- <p>Please try again or contact support.</p>
889
- </body></html>`, {
890
- status: 500,
891
- headers: { "Content-Type": "text/html" }
892
- });
893
- }
894
- }
895
- // Health check endpoint
896
- if (url.pathname === "/health") {
897
- return new Response(JSON.stringify({
898
- status: "healthy",
899
- service: "F-MCP ATezer",
900
- version: "0.1.0",
901
- endpoints: ["/sse", "/mcp", "/test-browser", "/oauth/authorize", "/oauth/callback"],
902
- oauth_configured: !!env.FIGMA_OAUTH_CLIENT_ID
903
- }), {
904
- headers: { "Content-Type": "application/json" },
905
- });
906
- }
907
- // Browser Rendering API test endpoint
908
- if (url.pathname === "/test-browser") {
909
- const results = await testBrowserRendering(env);
910
- return new Response(JSON.stringify(results, null, 2), {
911
- headers: { "Content-Type": "application/json" },
912
- });
913
- }
914
- // Serve favicon
915
- if (url.pathname === "/favicon.ico") {
916
- // Redirect to custom Figma Console icon
917
- return Response.redirect("https://p198.p4.n0.cdn.zight.com/items/Qwu1Dywx/b61b7b8f-05dc-4063-8a40-53fa4f8e3e97.jpg", 302);
918
- }
919
- // Root path - serve landing page with proper meta tags
920
- if (url.pathname === "/") {
921
- return new Response(`<!DOCTYPE html>
922
- <html>
923
- <head>
924
- <meta charset="UTF-8">
925
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
926
- <title>F-MCP ATezer</title>
927
- <link rel="icon" type="image/jpeg" href="https://p198.p4.n0.cdn.zight.com/items/Qwu1Dywx/b61b7b8f-05dc-4063-8a40-53fa4f8e3e97.jpg">
928
- <meta name="description" content="Model Context Protocol server for Figma debugging and design system extraction">
929
- <style>
930
- * {
931
- margin: 0;
932
- padding: 0;
933
- box-sizing: border-box;
934
- }
935
- body {
936
- font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
937
- background: #ffffff;
938
- color: #000000;
939
- line-height: 1.5;
940
- }
941
- .header {
942
- padding: 24px 48px;
943
- border-bottom: 1px solid #e5e5e5;
944
- }
945
- .logo {
946
- display: flex;
947
- align-items: center;
948
- gap: 12px;
949
- font-size: 18px;
950
- font-weight: 600;
951
- }
952
- .logo img {
953
- width: 32px;
954
- height: 32px;
955
- }
956
- .container {
957
- max-width: 1200px;
958
- margin: 0 auto;
959
- padding: 80px 48px;
960
- }
961
- h1 {
962
- font-size: 64px;
963
- font-weight: 700;
964
- margin-bottom: 24px;
965
- letter-spacing: -0.02em;
966
- line-height: 1.1;
967
- }
968
- .subtitle {
969
- font-size: 20px;
970
- color: #666666;
971
- margin-bottom: 48px;
972
- max-width: 600px;
973
- }
974
- .cta {
975
- display: inline-flex;
976
- align-items: center;
977
- gap: 8px;
978
- padding: 12px 24px;
979
- background: #000000;
980
- color: #ffffff;
981
- text-decoration: none;
982
- border-radius: 8px;
983
- font-weight: 500;
984
- font-size: 16px;
985
- transition: background 0.2s;
986
- }
987
- .cta:hover {
988
- background: #333333;
989
- }
990
- .features {
991
- display: grid;
992
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
993
- gap: 32px;
994
- margin-top: 80px;
995
- }
996
- .feature {
997
- padding: 24px;
998
- border: 1px solid #e5e5e5;
999
- border-radius: 8px;
1000
- }
1001
- .feature h3 {
1002
- font-size: 20px;
1003
- font-weight: 600;
1004
- margin-bottom: 12px;
1005
- }
1006
- .feature p {
1007
- color: #666666;
1008
- font-size: 15px;
1009
- }
1010
- .footer {
1011
- padding: 48px;
1012
- text-align: center;
1013
- color: #999999;
1014
- font-size: 14px;
1015
- border-top: 1px solid #e5e5e5;
1016
- margin-top: 120px;
1017
- }
1018
- </style>
1019
- </head>
1020
- <body>
1021
- <div class="header">
1022
- <div class="logo">
1023
- <img src="https://p198.p4.n0.cdn.zight.com/items/Qwu1Dywx/b61b7b8f-05dc-4063-8a40-53fa4f8e3e97.jpg" alt="F-MCP ATezer">
1024
- F-MCP ATezer
1025
- </div>
1026
- </div>
1027
- <div class="container">
1028
- <h1>Debug Figma plugins<br>with AI assistance</h1>
1029
- <p class="subtitle">Model Context Protocol server that gives AI assistants real-time access to Figma console logs, design system data, and visual debugging tools.</p>
1030
- <a href="https://github.com/atezer/FMCP" class="cta">
1031
- <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
1032
- <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
1033
- </svg>
1034
- View Documentation
1035
- </a>
1036
- <div class="features">
1037
- <div class="feature">
1038
- <h3>Real-time Console Access</h3>
1039
- <p>Capture plugin console logs, errors, and stack traces as they happen</p>
1040
- </div>
1041
- <div class="feature">
1042
- <h3>Design System Extraction</h3>
1043
- <p>Pull variables, components, and styles directly from Figma files</p>
1044
- </div>
1045
- <div class="feature">
1046
- <h3>Visual Debugging</h3>
1047
- <p>Take screenshots and export component images for visual reference</p>
1048
- </div>
1049
- </div>
1050
- </div>
1051
- <div class="footer">
1052
- © 2025 F-MCP ATezer · MIT License
1053
- </div>
1054
- </body>
1055
- </html>`, {
1056
- headers: { "Content-Type": "text/html; charset=utf-8" }
1057
- });
1058
- }
1059
- return new Response("Not found", { status: 404 });
1060
- },
1061
- };