figma-console-mcp 1.7.0 → 1.8.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 (39) hide show
  1. package/README.md +33 -5
  2. package/dist/cloudflare/core/design-code-tools.js +2 -2
  3. package/dist/cloudflare/core/figma-connector.js +8 -0
  4. package/dist/cloudflare/core/figma-desktop-connector.js +69 -2
  5. package/dist/cloudflare/core/figma-tools.js +118 -98
  6. package/dist/cloudflare/core/websocket-connector.js +235 -0
  7. package/dist/cloudflare/core/websocket-server.js +603 -0
  8. package/dist/cloudflare/index.js +5 -5
  9. package/dist/core/design-code-tools.js +2 -2
  10. package/dist/core/design-code-tools.js.map +1 -1
  11. package/dist/core/figma-connector.d.ts +47 -0
  12. package/dist/core/figma-connector.d.ts.map +1 -0
  13. package/dist/core/figma-connector.js +9 -0
  14. package/dist/core/figma-connector.js.map +1 -0
  15. package/dist/core/figma-desktop-connector.d.ts +14 -1
  16. package/dist/core/figma-desktop-connector.d.ts.map +1 -1
  17. package/dist/core/figma-desktop-connector.js +69 -2
  18. package/dist/core/figma-desktop-connector.js.map +1 -1
  19. package/dist/core/figma-tools.d.ts +1 -1
  20. package/dist/core/figma-tools.d.ts.map +1 -1
  21. package/dist/core/figma-tools.js +118 -98
  22. package/dist/core/figma-tools.js.map +1 -1
  23. package/dist/core/websocket-connector.d.ts +53 -0
  24. package/dist/core/websocket-connector.d.ts.map +1 -0
  25. package/dist/core/websocket-connector.js +236 -0
  26. package/dist/core/websocket-connector.js.map +1 -0
  27. package/dist/core/websocket-server.d.ts +183 -0
  28. package/dist/core/websocket-server.d.ts.map +1 -0
  29. package/dist/core/websocket-server.js +604 -0
  30. package/dist/core/websocket-server.js.map +1 -0
  31. package/dist/local.d.ts +10 -3
  32. package/dist/local.d.ts.map +1 -1
  33. package/dist/local.js +820 -274
  34. package/dist/local.js.map +1 -1
  35. package/figma-desktop-bridge/README.md +30 -16
  36. package/figma-desktop-bridge/code.js +195 -3
  37. package/figma-desktop-bridge/manifest.json +4 -1
  38. package/figma-desktop-bridge/ui.html +229 -0
  39. package/package.json +6 -4
package/README.md CHANGED
@@ -42,7 +42,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
42
42
  | **Edit existing designs** | ✅ | ❌ |
43
43
  | **Manage design tokens/variables** | ✅ | ❌ |
44
44
  | Desktop Bridge plugin | ✅ | ❌ |
45
- | **Total tools available** | **72+** | **16** |
45
+ | **Total tools available** | **53+** | **16** |
46
46
 
47
47
  > **Bottom line:** Remote SSE is **read-only**. If you want AI to actually design in Figma, use NPX Setup.
48
48
 
@@ -52,7 +52,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
52
52
 
53
53
  **Best for:** Designers who want full AI-assisted design capabilities.
54
54
 
55
- **What you get:** All 72+ tools including design creation, variable management, and component instantiation.
55
+ **What you get:** All 53+ tools including design creation, variable management, and component instantiation.
56
56
 
57
57
  #### Prerequisites
58
58
 
@@ -125,7 +125,7 @@ Create a simple frame with a blue background
125
125
 
126
126
  **Best for:** Developers who want to modify source code or contribute to the project.
127
127
 
128
- **What you get:** Same 72+ tools as NPX, plus full source code access.
128
+ **What you get:** Same 53+ tools as NPX, plus full source code access.
129
129
 
130
130
  #### Quick Setup
131
131
 
@@ -212,7 +212,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
212
212
  | Feature | NPX (Recommended) | Local Git | Remote SSE |
213
213
  |---------|-------------------|-----------|------------|
214
214
  | **Setup time** | ~10 minutes | ~15 minutes | ~2 minutes |
215
- | **Total tools** | **72+** | **72+** | **16** (read-only) |
215
+ | **Total tools** | **53+** | **53+** | **16** (read-only) |
216
216
  | **Design creation** | ✅ | ✅ | ❌ |
217
217
  | **Variable management** | ✅ | ✅ | ❌ |
218
218
  | **Component instantiation** | ✅ | ✅ | ❌ |
@@ -224,7 +224,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
224
224
  | **Automatic updates** | ✅ (`@latest`) | Manual (`git pull`) | ✅ |
225
225
  | **Source code access** | ❌ | ✅ | ❌ |
226
226
 
227
- > **Key insight:** Remote SSE is read-only with ~22% of the tools. Use NPX for full capabilities.
227
+ > **Key insight:** Remote SSE is read-only with ~30% of the tools. Use NPX for full capabilities.
228
228
 
229
229
  **📖 [Complete Feature Comparison](docs/mode-comparison.md)**
230
230
 
@@ -483,6 +483,34 @@ The **Figma Desktop Bridge** plugin enables powerful capabilities:
483
483
 
484
484
  **📖 [Desktop Bridge Documentation](figma-desktop-bridge/README.md)**
485
485
 
486
+ ### 🔌 WebSocket Bridge
487
+
488
+ Figma Desktop is transitioning away from Chrome DevTools Protocol (CDP) support. The MCP server includes a **WebSocket bridge** that provides seamless connectivity regardless of which transport is available.
489
+
490
+ **How it works:**
491
+ - The server tries **WebSocket first** (port 9223, instant check) via the Desktop Bridge plugin
492
+ - If no WebSocket client is connected, it falls back to **CDP** (port 9222)
493
+ - The transport is selected automatically per-command — no configuration needed
494
+ - All 53+ tools work identically through either transport
495
+
496
+ **Setup:**
497
+ 1. Install the Desktop Bridge plugin in Figma (Plugins > Development > Import from manifest)
498
+ 2. Open the plugin in your Figma file — it connects to ws://localhost:9223 automatically
499
+ 3. The MCP server detects the connection and routes commands through WebSocket
500
+
501
+ **If you also have CDP available** (Figma launched with `--remote-debugging-port=9222`), CDP serves as a fallback. Console tools work with both transports — CDP captures all page-level logs while WebSocket captures plugin-context logs. `figma_navigate` requires CDP for browser-level navigation; in WebSocket mode it returns the connected file info with guidance instead.
502
+
503
+ **Multiple files:** The WebSocket server supports multiple simultaneous plugin connections — one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs). The MCP server routes commands to the appropriate file based on active context. If a plugin instance is replaced (e.g., plugin reloaded in the same file), the displaced client stops reconnecting automatically.
504
+
505
+ **Environment variables:**
506
+ - `FIGMA_WS_PORT` — Override the server-side WebSocket port (default: 9223). Note: the plugin UI and manifest are hard-coded to port 9223. Using a custom port also requires updating `wsPort` in `ui.html` and `allowedDomains` in `manifest.json`.
507
+
508
+ **Checking transport status:**
509
+ ```
510
+ Ask Claude: "Check Figma status"
511
+ ```
512
+ The response will show which transport is active (CDP or WebSocket).
513
+
486
514
  ---
487
515
 
488
516
  ## 🧩 MCP Apps (Experimental)
@@ -1287,7 +1287,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
1287
1287
  try {
1288
1288
  const url = fileUrl || getCurrentUrl();
1289
1289
  if (!url) {
1290
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
1290
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
1291
1291
  }
1292
1292
  const fileKey = extractFileKey(url);
1293
1293
  if (!fileKey) {
@@ -1474,7 +1474,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
1474
1474
  try {
1475
1475
  const url = fileUrl || getCurrentUrl();
1476
1476
  if (!url) {
1477
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
1477
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
1478
1478
  }
1479
1479
  const fileKey = extractFileKey(url);
1480
1480
  if (!fileKey) {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Figma Connector Interface
3
+ *
4
+ * Transport abstraction that both CDP (FigmaDesktopConnector) and WebSocket
5
+ * (WebSocketConnector) implement. Allows getDesktopConnector() to return
6
+ * whichever transport is available — WebSocket preferred, CDP fallback.
7
+ */
8
+ export {};
@@ -45,6 +45,9 @@ export class FigmaDesktopConnector {
45
45
  async initialize() {
46
46
  logger.info('Figma Desktop connector initialized (using Puppeteer Worker API)');
47
47
  }
48
+ getTransportType() {
49
+ return 'cdp';
50
+ }
48
51
  /**
49
52
  * Execute code in Figma's plugin context where the figma API is available
50
53
  * Uses Puppeteer's direct worker access instead of CDP context enumeration
@@ -707,7 +710,18 @@ export class FigmaDesktopConnector {
707
710
  logger.info({ variableId, newName }, 'Renaming variable via plugin UI');
708
711
  const frame = await this.findPluginUIFrame();
709
712
  try {
710
- const result = await frame.evaluate(`window.renameVariable(${JSON.stringify(variableId)}, ${JSON.stringify(newName)})`);
713
+ // Look up old name from cached variables data, then rename — single CDP roundtrip
714
+ const result = await frame.evaluate(`(async () => {
715
+ var oldName;
716
+ var vars = window.__figmaVariablesData;
717
+ if (vars && vars.variables) {
718
+ var v = vars.variables.find(function(v) { return v.id === ${JSON.stringify(variableId)}; });
719
+ if (v) oldName = v.name;
720
+ }
721
+ var result = await window.renameVariable(${JSON.stringify(variableId)}, ${JSON.stringify(newName)});
722
+ if (oldName) result.oldName = oldName;
723
+ return result;
724
+ })()`);
711
725
  logger.info({ success: result.success, oldName: result.oldName, newName: result.variable?.name }, 'Variable renamed');
712
726
  await this.logToFigmaConsole((oldN, newN) => {
713
727
  console.log(`[DESKTOP_CONNECTOR] ✅ Variable renamed from "${oldN}" to "${newN}"`);
@@ -773,7 +787,25 @@ export class FigmaDesktopConnector {
773
787
  logger.info({ collectionId, modeId, newName }, 'Renaming mode via plugin UI');
774
788
  const frame = await this.findPluginUIFrame();
775
789
  try {
776
- const result = await frame.evaluate(`window.renameMode(${JSON.stringify(collectionId)}, ${JSON.stringify(modeId)}, ${JSON.stringify(newName)})`);
790
+ // Look up old mode name from cached variables data, then rename — single CDP roundtrip
791
+ const result = await frame.evaluate(`(async () => {
792
+ var oldName;
793
+ var vars = window.__figmaVariablesData;
794
+ var colls = vars && (vars.variableCollections || vars.collections);
795
+ if (colls) {
796
+ for (var i = 0; i < colls.length; i++) {
797
+ var c = colls[i];
798
+ if (c.id === ${JSON.stringify(collectionId)}) {
799
+ var mode = c.modes.find(function(m) { return m.modeId === ${JSON.stringify(modeId)}; });
800
+ if (mode) oldName = mode.name;
801
+ break;
802
+ }
803
+ }
804
+ }
805
+ var result = await window.renameMode(${JSON.stringify(collectionId)}, ${JSON.stringify(modeId)}, ${JSON.stringify(newName)});
806
+ if (oldName) result.oldName = oldName;
807
+ return result;
808
+ })()`);
777
809
  logger.info({ success: result.success, oldName: result.oldName, newName }, 'Mode renamed');
778
810
  await this.logToFigmaConsole((oldN, newN) => {
779
811
  console.log(`[DESKTOP_CONNECTOR] ✅ Mode renamed from "${oldN}" to "${newN}"`);
@@ -1080,5 +1112,40 @@ export class FigmaDesktopConnector {
1080
1112
  throw error;
1081
1113
  }
1082
1114
  }
1115
+ // ============================================================================
1116
+ // SCREENSHOT & INSTANCE PROPERTIES (via plugin UI)
1117
+ // ============================================================================
1118
+ /**
1119
+ * Capture screenshot via plugin's exportAsync
1120
+ */
1121
+ async captureScreenshot(nodeId, options) {
1122
+ logger.info({ nodeId, options }, 'Capturing screenshot via plugin UI');
1123
+ const frame = await this.findPluginUIFrame();
1124
+ try {
1125
+ const result = await frame.evaluate(`window.captureScreenshot(${JSON.stringify(nodeId)}, ${JSON.stringify(options || {})})`);
1126
+ logger.info({ success: result.success }, 'Screenshot captured');
1127
+ return result;
1128
+ }
1129
+ catch (error) {
1130
+ logger.error({ error, nodeId }, 'Capture screenshot failed');
1131
+ throw error;
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Set component instance properties
1136
+ */
1137
+ async setInstanceProperties(nodeId, properties) {
1138
+ logger.info({ nodeId, properties: Object.keys(properties || {}) }, 'Setting instance properties via plugin UI');
1139
+ const frame = await this.findPluginUIFrame();
1140
+ try {
1141
+ const result = await frame.evaluate(`window.setInstanceProperties(${JSON.stringify(nodeId)}, ${JSON.stringify(properties)})`);
1142
+ logger.info({ success: result.success }, 'Instance properties set');
1143
+ return result;
1144
+ }
1145
+ catch (error) {
1146
+ logger.error({ error, nodeId }, 'Set instance properties failed');
1147
+ throw error;
1148
+ }
1149
+ }
1083
1150
  }
1084
1151
  FigmaDesktopConnector.DEBUG = process.env.DEBUG === '1' || process.env.DEBUG === 'true';
@@ -574,7 +574,7 @@ function resolveVariableAliases(variables, allVariablesMap, collectionsMap) {
574
574
  /**
575
575
  * Register Figma API tools with the MCP server
576
576
  */
577
- export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getConsoleMonitor, getBrowserManager, ensureInitialized, variablesCache, options) {
577
+ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getConsoleMonitor, getBrowserManager, ensureInitialized, variablesCache, options, getDesktopConnector) {
578
578
  const isRemoteMode = options?.isRemoteMode ?? false;
579
579
  // Tool 8: Get File Data (General Purpose)
580
580
  // NOTE: For specific use cases, consider using specialized tools:
@@ -585,7 +585,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
585
585
  .string()
586
586
  .url()
587
587
  .optional()
588
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
588
+ .describe("Figma file URL (e.g., https://figma.com/design/abc123). Auto-detected from CDP browser or WebSocket Desktop Bridge connection. Only required if neither is connected."),
589
589
  depth: z
590
590
  .number()
591
591
  .min(0)
@@ -626,7 +626,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
626
626
  // Use provided URL or current URL from browser
627
627
  const url = fileUrl || getCurrentUrl();
628
628
  if (!url) {
629
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
629
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
630
630
  }
631
631
  const fileKey = extractFileKey(url);
632
632
  if (!fileKey) {
@@ -786,7 +786,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
786
786
  .string()
787
787
  .url()
788
788
  .optional()
789
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
789
+ .describe("Figma file URL (e.g., https://figma.com/design/abc123). Auto-detected from CDP browser or WebSocket Desktop Bridge connection. Only required if neither is connected."),
790
790
  includePublished: z
791
791
  .boolean()
792
792
  .optional()
@@ -895,8 +895,8 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
895
895
  {
896
896
  type: "text",
897
897
  text: JSON.stringify({
898
- error: "No Figma file URL provided",
899
- message: "Either pass fileUrl parameter or call figma_navigate first."
898
+ error: "No Figma file URL available",
899
+ message: "Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode)."
900
900
  }),
901
901
  },
902
902
  ],
@@ -1447,16 +1447,17 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1447
1447
  }
1448
1448
  }
1449
1449
  // FALLBACK: Try Desktop connection (when no token available OR as secondary fallback)
1450
- // Ensure browser manager is initialized
1451
- if (ensureInitialized && !parseFromConsole && (!hasToken || !restApiSucceeded)) {
1452
- logger.info("Calling ensureInitialized to initialize browser manager");
1450
+ // Only call ensureInitialized for CDP path — skip when transport-agnostic connector exists
1451
+ if (ensureInitialized && !getDesktopConnector && !parseFromConsole && (!hasToken || !restApiSucceeded)) {
1452
+ logger.info("Calling ensureInitialized to initialize browser manager (CDP path)");
1453
1453
  await ensureInitialized();
1454
1454
  }
1455
1455
  const browserManager = getBrowserManager?.();
1456
- logger.info({ hasBrowserManager: !!browserManager, parseFromConsole, hasToken, restApiSucceeded }, "Desktop connection check");
1456
+ const hasDesktopConnection = !!getDesktopConnector || !!browserManager;
1457
+ logger.info({ hasBrowserManager: !!browserManager, hasDesktopConnector: !!getDesktopConnector, parseFromConsole, hasToken, restApiSucceeded }, "Desktop connection check");
1457
1458
  // Debug: Log why Desktop connection might be skipped
1458
- if (!browserManager) {
1459
- logger.error("Desktop connection skipped: browserManager is not available");
1459
+ if (!hasDesktopConnection) {
1460
+ logger.error("Desktop connection skipped: neither connector nor browserManager available");
1460
1461
  }
1461
1462
  else if (parseFromConsole) {
1462
1463
  logger.info("Desktop connection skipped: parseFromConsole is true");
@@ -1464,26 +1465,21 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1464
1465
  else if (restApiSucceeded) {
1465
1466
  logger.info("Desktop connection skipped: REST API already succeeded");
1466
1467
  }
1467
- if (browserManager && !parseFromConsole && (!hasToken || !restApiSucceeded)) {
1468
+ if (hasDesktopConnection && !parseFromConsole && (!hasToken || !restApiSucceeded)) {
1468
1469
  try {
1469
1470
  logger.info({ fileKey }, "Attempting to get variables via Desktop connection");
1470
- // Import and use the Desktop connector
1471
- const { FigmaDesktopConnector } = await import('./figma-desktop-connector.js');
1472
- const page = await browserManager.getPage();
1473
- logger.info("Got page from browser manager");
1474
- // Log to browser console for MCP capture
1475
- await page.evaluate(() => {
1476
- console.log('[FIGMA_TOOLS] 🚀 Got page from browser manager, creating Desktop connector...');
1477
- });
1478
- const connector = new FigmaDesktopConnector(page);
1479
- await page.evaluate(() => {
1480
- console.log('[FIGMA_TOOLS] ✅ Desktop connector created, initializing...');
1481
- });
1482
- await connector.initialize();
1483
- logger.info("Desktop connector initialized, calling getVariablesFromPluginUI...");
1484
- await page.evaluate(() => {
1485
- console.log('[FIGMA_TOOLS] ✅ Desktop connector initialized, calling getVariablesFromPluginUI...');
1486
- });
1471
+ let connector;
1472
+ if (getDesktopConnector) {
1473
+ connector = await getDesktopConnector();
1474
+ }
1475
+ else {
1476
+ // Fallback: direct CDP connector (legacy path)
1477
+ const { FigmaDesktopConnector } = await import('./figma-desktop-connector.js');
1478
+ const page = await browserManager.getPage();
1479
+ connector = new FigmaDesktopConnector(page);
1480
+ await connector.initialize();
1481
+ }
1482
+ logger.info({ transport: connector.getTransportType?.() || 'unknown' }, "Desktop connector ready");
1487
1483
  const desktopResult = await connector.getVariablesFromPluginUI(fileKey);
1488
1484
  if (desktopResult.success && desktopResult.variables) {
1489
1485
  logger.info({
@@ -1841,7 +1837,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1841
1837
  .string()
1842
1838
  .url()
1843
1839
  .optional()
1844
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
1840
+ .describe("Figma file URL (e.g., https://figma.com/design/abc123). Auto-detected from CDP browser or WebSocket Desktop Bridge connection. Only required if neither is connected."),
1845
1841
  nodeId: z
1846
1842
  .string()
1847
1843
  .describe("Component node ID (e.g., '123:456')"),
@@ -1858,7 +1854,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1858
1854
  try {
1859
1855
  const url = fileUrl || getCurrentUrl();
1860
1856
  if (!url) {
1861
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
1857
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
1862
1858
  }
1863
1859
  const fileKey = extractFileKey(url);
1864
1860
  if (!fileKey) {
@@ -1866,18 +1862,26 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1866
1862
  }
1867
1863
  logger.info({ fileKey, nodeId, format, enrich }, "Fetching component data");
1868
1864
  // PRIORITY 1: Try Desktop Bridge plugin UI first (has reliable description field!)
1869
- if (getBrowserManager && ensureInitialized) {
1865
+ if (getDesktopConnector || (getBrowserManager && ensureInitialized)) {
1870
1866
  try {
1871
1867
  logger.info({ nodeId }, "Attempting to get component via Desktop Bridge plugin UI");
1872
- await ensureInitialized();
1873
- const browserManager = getBrowserManager();
1874
- if (!browserManager) {
1875
- throw new Error("Browser manager not available after initialization");
1868
+ let connector;
1869
+ if (getDesktopConnector) {
1870
+ connector = await getDesktopConnector();
1871
+ }
1872
+ else {
1873
+ // Fallback: direct CDP connector (legacy path)
1874
+ if (ensureInitialized)
1875
+ await ensureInitialized();
1876
+ const browserManager = getBrowserManager?.();
1877
+ if (!browserManager) {
1878
+ throw new Error("Browser manager not available after initialization");
1879
+ }
1880
+ const { FigmaDesktopConnector } = await import('./figma-desktop-connector.js');
1881
+ const page = await browserManager.getPage();
1882
+ connector = new FigmaDesktopConnector(page);
1883
+ await connector.initialize();
1876
1884
  }
1877
- const { FigmaDesktopConnector } = await import('./figma-desktop-connector.js');
1878
- const page = await browserManager.getPage();
1879
- const connector = new FigmaDesktopConnector(page);
1880
- await connector.initialize();
1881
1885
  const desktopResult = await connector.getComponentFromPluginUI(nodeId);
1882
1886
  if (desktopResult.success && desktopResult.component) {
1883
1887
  logger.info({
@@ -1969,7 +1973,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1969
1973
  catch (apiError) {
1970
1974
  const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
1971
1975
  throw new Error(`Cannot retrieve component data. Both Desktop Bridge and REST API are unavailable.\n` +
1972
- `Desktop Bridge: ${getBrowserManager && ensureInitialized ? 'Failed (see logs above)' : 'Not available (local mode only)'}\n` +
1976
+ `Desktop Bridge: ${getDesktopConnector || (getBrowserManager && ensureInitialized) ? 'Failed (see logs above)' : 'Not available (local mode only)'}\n` +
1973
1977
  `REST API: ${errorMessage}\n\n` +
1974
1978
  `To fix:\n` +
1975
1979
  `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable, OR ensure Figma Desktop Bridge plugin is running\n` +
@@ -2073,7 +2077,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2073
2077
  .string()
2074
2078
  .url()
2075
2079
  .optional()
2076
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
2080
+ .describe("Figma file URL (e.g., https://figma.com/design/abc123). Auto-detected from CDP browser or WebSocket Desktop Bridge connection. Only required if neither is connected."),
2077
2081
  verbosity: z
2078
2082
  .enum(["summary", "standard", "full"])
2079
2083
  .optional()
@@ -2111,7 +2115,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2111
2115
  }
2112
2116
  const url = fileUrl || getCurrentUrl();
2113
2117
  if (!url) {
2114
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2118
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
2115
2119
  }
2116
2120
  const fileKey = extractFileKey(url);
2117
2121
  if (!fileKey) {
@@ -2218,7 +2222,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2218
2222
  .string()
2219
2223
  .url()
2220
2224
  .optional()
2221
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
2225
+ .describe("Figma file URL (e.g., https://figma.com/design/abc123). Auto-detected from CDP browser or WebSocket Desktop Bridge connection. Only required if neither is connected."),
2222
2226
  nodeId: z
2223
2227
  .string()
2224
2228
  .describe("Component node ID to render as image (e.g., '695:313')"),
@@ -2252,7 +2256,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2252
2256
  }
2253
2257
  const url = fileUrl || getCurrentUrl();
2254
2258
  if (!url) {
2255
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2259
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
2256
2260
  }
2257
2261
  const fileKey = extractFileKey(url);
2258
2262
  if (!fileKey) {
@@ -2260,8 +2264,8 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2260
2264
  }
2261
2265
  logger.info({ fileKey, nodeId, scale, format }, "Rendering component image");
2262
2266
  // First, fetch the node to check if it's a COMPONENT_SET
2263
- const fileData = await api.getFile(fileKey, { ids: [nodeId] });
2264
- const node = fileData.nodes[nodeId]?.document;
2267
+ const fileData = await api.getNodes(fileKey, [nodeId]);
2268
+ const node = fileData.nodes?.[nodeId]?.document;
2265
2269
  if (!node) {
2266
2270
  throw new Error(`Node ${nodeId} not found in file ${fileKey}. Please verify the node ID is correct.`);
2267
2271
  }
@@ -2367,7 +2371,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2367
2371
  }
2368
2372
  const url = fileUrl || getCurrentUrl();
2369
2373
  if (!url) {
2370
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2374
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
2371
2375
  }
2372
2376
  const fileKey = extractFileKey(url);
2373
2377
  if (!fileKey) {
@@ -2577,7 +2581,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2577
2581
  }
2578
2582
  const url = fileUrl || getCurrentUrl();
2579
2583
  if (!url) {
2580
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2584
+ throw new Error("No Figma file URL available. Pass the fileUrl parameter, call figma_navigate (CDP mode), or ensure the Desktop Bridge plugin is connected (WebSocket mode).");
2581
2585
  }
2582
2586
  const fileKey = extractFileKey(url);
2583
2587
  if (!fileKey) {
@@ -2752,33 +2756,43 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2752
2756
  .describe("Scale factor (default: 2 for 2x resolution)"),
2753
2757
  }, async ({ nodeId, format, scale }) => {
2754
2758
  try {
2755
- // Check for Desktop Bridge connection
2756
- const browserManager = getBrowserManager?.();
2757
- if (!browserManager) {
2758
- throw new Error("Desktop Bridge not available. To capture screenshots:\n" +
2759
- "1. Open your Figma file in Figma Desktop\n" +
2760
- "2. Install and run the 'Figma Console MCP' plugin\n" +
2761
- "3. Ensure the plugin shows 'MCP ready' status");
2762
- }
2763
- // Ensure browser is initialized
2764
- if (ensureInitialized) {
2765
- await ensureInitialized();
2766
- }
2767
- const page = await browserManager.getPage();
2768
2759
  logger.info({ nodeId, format, scale }, "Capturing screenshot via Desktop Bridge");
2769
- // Find the plugin UI frame and call the captureScreenshot function
2770
- const frames = page.frames();
2771
2760
  let result = null;
2772
- for (const frame of frames) {
2773
- try {
2774
- const hasFunction = await frame.evaluate('typeof window.captureScreenshot === "function"');
2775
- if (hasFunction) {
2776
- result = await frame.evaluate(`window.captureScreenshot(${JSON.stringify(nodeId)}, ${JSON.stringify({ format, scale })})`);
2777
- break;
2778
- }
2761
+ // Use the connector abstraction (supports both CDP and WebSocket)
2762
+ if (getDesktopConnector) {
2763
+ const connector = await getDesktopConnector();
2764
+ logger.info({ transport: connector.getTransportType?.() || 'unknown' }, "Screenshot via connector");
2765
+ result = await connector.captureScreenshot(nodeId || '', { format, scale });
2766
+ // Wrap in expected format only if connector returns raw data without a success flag
2767
+ if (result && typeof result.success === 'undefined' && result.image) {
2768
+ result = { success: true, image: result };
2769
+ }
2770
+ }
2771
+ // Legacy CDP fallback (only when no connector factory is available)
2772
+ if (!result && !getDesktopConnector) {
2773
+ const browserManager = getBrowserManager?.();
2774
+ if (!browserManager) {
2775
+ throw new Error("Desktop Bridge not available. To capture screenshots:\n" +
2776
+ "1. Open your Figma file in Figma Desktop\n" +
2777
+ "2. Install and run the 'Figma Console MCP' plugin\n" +
2778
+ "3. Ensure the plugin shows 'MCP ready' status");
2779
2779
  }
2780
- catch {
2781
- continue;
2780
+ if (ensureInitialized) {
2781
+ await ensureInitialized();
2782
+ }
2783
+ const page = await browserManager.getPage();
2784
+ const frames = page.frames();
2785
+ for (const frame of frames) {
2786
+ try {
2787
+ const hasFunction = await frame.evaluate('typeof window.captureScreenshot === "function"');
2788
+ if (hasFunction) {
2789
+ result = await frame.evaluate(`window.captureScreenshot(${JSON.stringify(nodeId || '')}, ${JSON.stringify({ format, scale })})`);
2790
+ break;
2791
+ }
2792
+ }
2793
+ catch {
2794
+ continue;
2795
+ }
2782
2796
  }
2783
2797
  }
2784
2798
  if (!result) {
@@ -2850,33 +2864,39 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2850
2864
  "The tool automatically handles the #nodeId suffix for TEXT/BOOLEAN/INSTANCE_SWAP properties."),
2851
2865
  }, async ({ nodeId, properties }) => {
2852
2866
  try {
2853
- // Check for Desktop Bridge connection
2854
- const browserManager = getBrowserManager?.();
2855
- if (!browserManager) {
2856
- throw new Error("Desktop Bridge not available. To set instance properties:\n" +
2857
- "1. Open your Figma file in Figma Desktop\n" +
2858
- "2. Install and run the 'Figma Console MCP' plugin\n" +
2859
- "3. Ensure the plugin shows 'MCP ready' status");
2860
- }
2861
- // Ensure browser is initialized
2862
- if (ensureInitialized) {
2863
- await ensureInitialized();
2864
- }
2865
- const page = await browserManager.getPage();
2866
2867
  logger.info({ nodeId, properties: Object.keys(properties) }, "Setting instance properties via Desktop Bridge");
2867
- // Find the plugin UI frame and call the setInstanceProperties function
2868
- const frames = page.frames();
2869
2868
  let result = null;
2870
- for (const frame of frames) {
2871
- try {
2872
- const hasFunction = await frame.evaluate('typeof window.setInstanceProperties === "function"');
2873
- if (hasFunction) {
2874
- result = await frame.evaluate(`window.setInstanceProperties(${JSON.stringify(nodeId)}, ${JSON.stringify(properties)})`);
2875
- break;
2876
- }
2869
+ // Use the connector abstraction (supports both CDP and WebSocket)
2870
+ if (getDesktopConnector) {
2871
+ const connector = await getDesktopConnector();
2872
+ logger.info({ transport: connector.getTransportType?.() || 'unknown' }, "Instance properties via connector");
2873
+ result = await connector.setInstanceProperties(nodeId, properties);
2874
+ }
2875
+ // Legacy CDP fallback (only when no connector factory is available)
2876
+ if (!result && !getDesktopConnector) {
2877
+ const browserManager = getBrowserManager?.();
2878
+ if (!browserManager) {
2879
+ throw new Error("Desktop Bridge not available. To set instance properties:\n" +
2880
+ "1. Open your Figma file in Figma Desktop\n" +
2881
+ "2. Install and run the 'Figma Console MCP' plugin\n" +
2882
+ "3. Ensure the plugin shows 'MCP ready' status");
2883
+ }
2884
+ if (ensureInitialized) {
2885
+ await ensureInitialized();
2877
2886
  }
2878
- catch {
2879
- continue;
2887
+ const page = await browserManager.getPage();
2888
+ const frames = page.frames();
2889
+ for (const frame of frames) {
2890
+ try {
2891
+ const hasFunction = await frame.evaluate('typeof window.setInstanceProperties === "function"');
2892
+ if (hasFunction) {
2893
+ result = await frame.evaluate(`window.setInstanceProperties(${JSON.stringify(nodeId)}, ${JSON.stringify(properties)})`);
2894
+ break;
2895
+ }
2896
+ }
2897
+ catch {
2898
+ continue;
2899
+ }
2880
2900
  }
2881
2901
  }
2882
2902
  if (!result) {