@mp3wizard/figma-console-mcp 1.17.3 → 1.19.2

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 (68) hide show
  1. package/README.md +13 -12
  2. package/dist/cloudflare/core/annotation-tools.js +230 -0
  3. package/dist/cloudflare/core/cloud-websocket-connector.js +93 -0
  4. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  5. package/dist/cloudflare/core/design-code-tools.js +65 -7
  6. package/dist/cloudflare/core/enrichment/enrichment-service.js +108 -12
  7. package/dist/cloudflare/core/figjam-tools.js +485 -0
  8. package/dist/cloudflare/core/figma-api.js +7 -4
  9. package/dist/cloudflare/core/figma-desktop-connector.js +108 -0
  10. package/dist/cloudflare/core/figma-tools.js +445 -55
  11. package/dist/cloudflare/core/port-discovery.js +88 -0
  12. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  13. package/dist/cloudflare/core/slides-tools.js +607 -0
  14. package/dist/cloudflare/core/websocket-connector.js +93 -0
  15. package/dist/cloudflare/core/websocket-server.js +18 -9
  16. package/dist/cloudflare/index.js +164 -41
  17. package/dist/core/annotation-tools.d.ts +14 -0
  18. package/dist/core/annotation-tools.d.ts.map +1 -0
  19. package/dist/core/annotation-tools.js +231 -0
  20. package/dist/core/annotation-tools.js.map +1 -0
  21. package/dist/core/deep-component-tools.d.ts +14 -0
  22. package/dist/core/deep-component-tools.d.ts.map +1 -0
  23. package/dist/core/deep-component-tools.js +129 -0
  24. package/dist/core/deep-component-tools.js.map +1 -0
  25. package/dist/core/design-code-tools.d.ts.map +1 -1
  26. package/dist/core/design-code-tools.js +65 -7
  27. package/dist/core/design-code-tools.js.map +1 -1
  28. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
  29. package/dist/core/enrichment/enrichment-service.js +108 -12
  30. package/dist/core/enrichment/enrichment-service.js.map +1 -1
  31. package/dist/core/figma-api.d.ts +1 -1
  32. package/dist/core/figma-api.d.ts.map +1 -1
  33. package/dist/core/figma-api.js +7 -4
  34. package/dist/core/figma-api.js.map +1 -1
  35. package/dist/core/figma-connector.d.ts +5 -0
  36. package/dist/core/figma-connector.d.ts.map +1 -1
  37. package/dist/core/figma-desktop-connector.d.ts +20 -0
  38. package/dist/core/figma-desktop-connector.d.ts.map +1 -1
  39. package/dist/core/figma-desktop-connector.js +83 -0
  40. package/dist/core/figma-desktop-connector.js.map +1 -1
  41. package/dist/core/figma-tools.d.ts.map +1 -1
  42. package/dist/core/figma-tools.js +355 -26
  43. package/dist/core/figma-tools.js.map +1 -1
  44. package/dist/core/port-discovery.d.ts +21 -0
  45. package/dist/core/port-discovery.d.ts.map +1 -1
  46. package/dist/core/port-discovery.js +88 -0
  47. package/dist/core/port-discovery.js.map +1 -1
  48. package/dist/core/resolve-package-root.d.ts +2 -0
  49. package/dist/core/resolve-package-root.d.ts.map +1 -0
  50. package/dist/core/resolve-package-root.js +12 -0
  51. package/dist/core/resolve-package-root.js.map +1 -0
  52. package/dist/core/types/design-code.d.ts +1 -0
  53. package/dist/core/types/design-code.d.ts.map +1 -1
  54. package/dist/core/websocket-connector.d.ts +5 -0
  55. package/dist/core/websocket-connector.d.ts.map +1 -1
  56. package/dist/core/websocket-connector.js +18 -0
  57. package/dist/core/websocket-connector.js.map +1 -1
  58. package/dist/core/websocket-server.d.ts.map +1 -1
  59. package/dist/core/websocket-server.js +7 -9
  60. package/dist/core/websocket-server.js.map +1 -1
  61. package/dist/local.d.ts +6 -0
  62. package/dist/local.d.ts.map +1 -1
  63. package/dist/local.js +58 -1
  64. package/dist/local.js.map +1 -1
  65. package/figma-desktop-bridge/code.js +906 -4
  66. package/figma-desktop-bridge/ui-full.html +80 -0
  67. package/figma-desktop-bridge/ui.html +82 -0
  68. package/package.json +1 -1
@@ -3,6 +3,8 @@
3
3
  * MCP tool definitions for Figma REST API data extraction
4
4
  */
5
5
  import { z } from "zod";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
6
8
  import { extractFileKey, extractFigmaUrlInfo, formatVariables, formatComponentData, withTimeout } from "./figma-api.js";
7
9
  import { createChildLogger } from "./logger.js";
8
10
  import { EnrichmentService } from "./enrichment/index.js";
@@ -11,6 +13,74 @@ import { extractNodeSpec, validateReconstructionSpec, listVariants } from "./fig
11
13
  const logger = createChildLogger({ component: "figma-tools" });
12
14
  // Initialize enrichment service
13
15
  const enrichmentService = new EnrichmentService(logger);
16
+ /**
17
+ * Scan a codebase components directory to discover existing components.
18
+ * Returns a registry of component names, paths, and exports.
19
+ * Works with any framework — looks for index.ts/tsx/js barrel exports.
20
+ */
21
+ function scanCodebaseComponents(componentsDir) {
22
+ const registry = [];
23
+ try {
24
+ if (!fs.existsSync(componentsDir))
25
+ return registry;
26
+ const dirs = fs.readdirSync(componentsDir, { withFileTypes: true });
27
+ for (const dir of dirs) {
28
+ if (!dir.isDirectory())
29
+ continue;
30
+ const compDir = path.join(componentsDir, dir.name);
31
+ // Look for barrel export (index.ts, index.tsx, index.js)
32
+ const barrelFiles = ["index.ts", "index.tsx", "index.js"];
33
+ let barrelPath = "";
34
+ for (const bf of barrelFiles) {
35
+ const candidate = path.join(compDir, bf);
36
+ if (fs.existsSync(candidate)) {
37
+ barrelPath = candidate;
38
+ break;
39
+ }
40
+ }
41
+ if (!barrelPath) {
42
+ // No barrel — check for a main component file matching the directory name
43
+ const mainFiles = [`${dir.name}.tsx`, `${dir.name}.ts`, `${dir.name}.jsx`, `${dir.name}.js`];
44
+ for (const mf of mainFiles) {
45
+ if (fs.existsSync(path.join(compDir, mf))) {
46
+ registry.push({ name: dir.name, path: `src/components/${dir.name}`, exports: [dir.name] });
47
+ break;
48
+ }
49
+ }
50
+ continue;
51
+ }
52
+ // Parse exports from barrel file
53
+ try {
54
+ const content = fs.readFileSync(barrelPath, "utf-8");
55
+ const exportNames = [];
56
+ // Match: export { Foo, Bar } from ...
57
+ const namedExports = content.matchAll(/export\s*\{([^}]+)\}/g);
58
+ for (const match of namedExports) {
59
+ const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/).pop()?.trim() || "").filter(Boolean);
60
+ exportNames.push(...names.filter(n => !n.startsWith("type ")));
61
+ }
62
+ // Match: export default ...
63
+ if (content.includes("export default")) {
64
+ exportNames.push("default");
65
+ }
66
+ // Filter out type-only exports
67
+ const cleanExports = exportNames.filter(n => !n.startsWith("type") || n[4] !== " ");
68
+ registry.push({
69
+ name: dir.name,
70
+ path: `src/components/${dir.name}`,
71
+ exports: cleanExports.length > 0 ? cleanExports : [dir.name],
72
+ });
73
+ }
74
+ catch {
75
+ registry.push({ name: dir.name, path: `src/components/${dir.name}`, exports: [dir.name] });
76
+ }
77
+ }
78
+ }
79
+ catch (err) {
80
+ logger.debug({ err, componentsDir }, "Could not scan codebase components directory");
81
+ }
82
+ return registry;
83
+ }
14
84
  // Initialize snippet injector
15
85
  const snippetInjector = new SnippetInjector();
16
86
  // ============================================================================
@@ -1474,7 +1544,11 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1474
1544
  await connector.initialize();
1475
1545
  }
1476
1546
  logger.info({ transport: connector.getTransportType?.() || 'unknown' }, "Desktop connector ready");
1477
- const desktopResult = await connector.getVariablesFromPluginUI(fileKey);
1547
+ // When refreshCache is requested, bypass the plugin UI's stale snapshot
1548
+ // and fetch live data directly from the Figma Plugin API
1549
+ const desktopResult = refreshCache
1550
+ ? await connector.getVariables(fileKey)
1551
+ : await connector.getVariablesFromPluginUI(fileKey);
1478
1552
  if (desktopResult.success && desktopResult.variables) {
1479
1553
  logger.info({
1480
1554
  variableCount: desktopResult.variables.length,
@@ -1644,7 +1718,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
1644
1718
  format: format || 'full',
1645
1719
  timestamp: dataForCache.timestamp,
1646
1720
  data: responseData,
1647
- cached: true,
1721
+ cached: false,
1648
1722
  }),
1649
1723
  },
1650
1724
  ],
@@ -2003,6 +2077,21 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2003
2077
  };
2004
2078
  formatted = await enrichmentService.enrichComponent(formatted, fileKey, enrichmentOptions);
2005
2079
  }
2080
+ // Surface annotation summary at top level for easy AI consumption
2081
+ const annotations = formatted.annotations || [];
2082
+ const annotationSummary = annotations.length > 0
2083
+ ? {
2084
+ count: annotations.length,
2085
+ labels: annotations
2086
+ .filter((a) => a.label || a.labelMarkdown)
2087
+ .map((a) => a.label || (a.labelMarkdown ? a.labelMarkdown.substring(0, 100) : null))
2088
+ .filter(Boolean),
2089
+ pinnedProperties: annotations
2090
+ .filter((a) => a.properties && a.properties.length > 0)
2091
+ .flatMap((a) => a.properties.map((p) => p.type)),
2092
+ hint: "Use figma_get_annotations for full annotation details including categories and markdown content",
2093
+ }
2094
+ : { count: 0, hint: "No annotations found. Designers can add annotations in Dev Mode to communicate specs." };
2006
2095
  return {
2007
2096
  content: [
2008
2097
  {
@@ -2011,6 +2100,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2011
2100
  fileKey,
2012
2101
  nodeId,
2013
2102
  component: formatted,
2103
+ annotations: annotationSummary,
2014
2104
  source: "desktop_bridge_plugin",
2015
2105
  enriched: enrich || false,
2016
2106
  note: "Retrieved via Desktop Bridge plugin - description fields and annotations are reliable and current"
@@ -2400,7 +2490,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2400
2490
  }
2401
2491
  });
2402
2492
  // Tool 13: Get Component for Development (UI Implementation)
2403
- server.tool("figma_get_component_for_development", "Get component data optimized for UI implementation, includes rendered image + filtered implementation context (layout, typography, visual properties). Use when user asks to: 'build this component', 'implement this in React/Vue', 'generate code for', or needs both visual reference and technical specs. Automatically includes 2x scale image unless includeImage=false. Best for: UI development, code generation, design-to-code workflows. For just metadata, use figma_get_component; for just image, use figma_get_component_image.", {
2493
+ server.tool("figma_get_component_for_development", "Get component data optimized for high-fidelity UI implementation. Returns a deep component tree (depth 4) with design tokens (boundVariables), interaction states (reactions), sizing constraints (min/max/layoutSizing), text behavior (autoResize, truncation), and design annotations. Automatically includes 2x rendered image. Use when user asks to: 'build this component', 'implement this in React/Vue', 'generate code for', or needs both visual reference and technical specs for production-quality, accessible, token-aware code. For just metadata/descriptions, use figma_get_component. For just image, use figma_get_component_image. For full annotation details, use figma_get_annotations. To resolve variable IDs to names/values, use figma_get_variables.", {
2404
2494
  fileUrl: z
2405
2495
  .string()
2406
2496
  .url()
@@ -2414,7 +2504,11 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2414
2504
  .optional()
2415
2505
  .default(true)
2416
2506
  .describe("Include rendered image for visual reference (default: true)"),
2417
- }, async ({ fileUrl, nodeId, includeImage }) => {
2507
+ codebasePath: z
2508
+ .string()
2509
+ .optional()
2510
+ .describe("Path to target codebase components directory (e.g., '/Users/me/project/src/components'). When provided, scans for existing components and includes a registry in the response to prevent recreating components that already exist. Strongly recommended for design-to-code workflows."),
2511
+ }, async ({ fileUrl, nodeId, includeImage, codebasePath }) => {
2418
2512
  try {
2419
2513
  let api;
2420
2514
  try {
@@ -2439,13 +2533,14 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2439
2533
  throw new Error(`Invalid Figma URL: ${url}`);
2440
2534
  }
2441
2535
  logger.info({ fileKey, nodeId, includeImage }, "Fetching component for development");
2442
- // Get node data with depth for children
2443
- const nodeData = await api.getNodes(fileKey, [nodeId], { depth: 2 });
2536
+ // Get node data with depth 4 for nested component structures
2537
+ // (depth 2 was too shallow for complex components like data tables, nested menus, etc.)
2538
+ const nodeData = await api.getNodes(fileKey, [nodeId], { depth: 4 });
2444
2539
  const node = nodeData.nodes?.[nodeId]?.document;
2445
2540
  if (!node) {
2446
2541
  throw new Error(`Component not found: ${nodeId}`);
2447
2542
  }
2448
- // Filter to visual/layout properties only
2543
+ // Filter to development-relevant properties — visual, layout, tokens, interactions
2449
2544
  const filterForDevelopment = (n) => {
2450
2545
  if (!n)
2451
2546
  return n;
@@ -2492,10 +2587,27 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2492
2587
  result.paddingBottom = n.paddingBottom;
2493
2588
  if (n.itemSpacing !== undefined)
2494
2589
  result.itemSpacing = n.itemSpacing;
2590
+ if (n.counterAxisSpacing !== undefined)
2591
+ result.counterAxisSpacing = n.counterAxisSpacing;
2495
2592
  if (n.itemReverseZIndex)
2496
2593
  result.itemReverseZIndex = n.itemReverseZIndex;
2497
2594
  if (n.strokesIncludedInLayout)
2498
2595
  result.strokesIncludedInLayout = n.strokesIncludedInLayout;
2596
+ if (n.layoutWrap)
2597
+ result.layoutWrap = n.layoutWrap;
2598
+ // Sizing constraints (maps to CSS min/max-width/height, width: auto/100%/fixed)
2599
+ if (n.layoutSizingHorizontal)
2600
+ result.layoutSizingHorizontal = n.layoutSizingHorizontal;
2601
+ if (n.layoutSizingVertical)
2602
+ result.layoutSizingVertical = n.layoutSizingVertical;
2603
+ if (n.minWidth !== undefined)
2604
+ result.minWidth = n.minWidth;
2605
+ if (n.maxWidth !== undefined)
2606
+ result.maxWidth = n.maxWidth;
2607
+ if (n.minHeight !== undefined)
2608
+ result.minHeight = n.minHeight;
2609
+ if (n.maxHeight !== undefined)
2610
+ result.maxHeight = n.maxHeight;
2499
2611
  // Visual properties
2500
2612
  if (n.fills)
2501
2613
  result.fills = n.fills;
@@ -2525,6 +2637,19 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2525
2637
  result.isMask = n.isMask;
2526
2638
  if (n.clipsContent)
2527
2639
  result.clipsContent = n.clipsContent;
2640
+ // Design tokens — variable bindings (maps fills/strokes/spacing/etc. to design tokens)
2641
+ if (n.boundVariables)
2642
+ result.boundVariables = n.boundVariables;
2643
+ if (n.styles)
2644
+ result.styles = n.styles;
2645
+ // Vector geometry (SVG path data — only for vector/icon nodes, not regular frames)
2646
+ const isVectorLike = n.type === 'VECTOR' || n.type === 'BOOLEAN_OPERATION' || n.type === 'LINE' || n.type === 'REGULAR_POLYGON' || n.type === 'STAR' || n.type === 'ELLIPSE';
2647
+ if (isVectorLike) {
2648
+ if (n.fillGeometry)
2649
+ result.fillGeometry = n.fillGeometry;
2650
+ if (n.strokeGeometry)
2651
+ result.strokeGeometry = n.strokeGeometry;
2652
+ }
2528
2653
  // Typography
2529
2654
  if (n.characters)
2530
2655
  result.characters = n.characters;
@@ -2534,15 +2659,49 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2534
2659
  result.characterStyleOverrides = n.characterStyleOverrides;
2535
2660
  if (n.styleOverrideTable)
2536
2661
  result.styleOverrideTable = n.styleOverrideTable;
2662
+ // Text behavior (maps to CSS overflow, text-overflow, white-space, text-transform)
2663
+ if (n.textAutoResize)
2664
+ result.textAutoResize = n.textAutoResize;
2665
+ if (n.textTruncation)
2666
+ result.textTruncation = n.textTruncation;
2667
+ if (n.textCase)
2668
+ result.textCase = n.textCase;
2669
+ if (n.textDecoration)
2670
+ result.textDecoration = n.textDecoration;
2537
2671
  // Component properties & variants
2538
- if (n.componentProperties)
2539
- result.componentProperties = n.componentProperties;
2672
+ if (n.componentProperties) {
2673
+ // Cap componentProperties size — icon instances can have 200KB+ of swap variants
2674
+ const cpJson = JSON.stringify(n.componentProperties);
2675
+ if (cpJson.length > 10000) {
2676
+ // Extract just the property names and types, not the full value catalogs
2677
+ const summary = {};
2678
+ for (const [key, val] of Object.entries(n.componentProperties)) {
2679
+ summary[key] = { type: val.type, value: typeof val.value === 'string' && val.value.length > 200 ? val.value.substring(0, 200) + '...' : val.value };
2680
+ }
2681
+ result.componentProperties = summary;
2682
+ result._componentPropertiesTruncated = true;
2683
+ }
2684
+ else {
2685
+ result.componentProperties = n.componentProperties;
2686
+ }
2687
+ }
2540
2688
  if (n.componentPropertyDefinitions)
2541
2689
  result.componentPropertyDefinitions = n.componentPropertyDefinitions;
2690
+ if (n.componentPropertyReferences)
2691
+ result.componentPropertyReferences = n.componentPropertyReferences;
2542
2692
  if (n.variantProperties)
2543
2693
  result.variantProperties = n.variantProperties;
2544
2694
  if (n.componentId)
2545
2695
  result.componentId = n.componentId;
2696
+ // Prototype interactions (hover, click, focus states and transitions)
2697
+ if (n.reactions && n.reactions.length > 0)
2698
+ result.reactions = n.reactions;
2699
+ if (n.transitionNodeID)
2700
+ result.transitionNodeID = n.transitionNodeID;
2701
+ if (n.transitionDuration !== undefined)
2702
+ result.transitionDuration = n.transitionDuration;
2703
+ if (n.transitionEasing)
2704
+ result.transitionEasing = n.transitionEasing;
2546
2705
  // State
2547
2706
  if (n.visible !== undefined)
2548
2707
  result.visible = n.visible;
@@ -2555,6 +2714,56 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2555
2714
  return result;
2556
2715
  };
2557
2716
  const componentData = filterForDevelopment(node);
2717
+ // Fetch annotations and descriptions via Desktop Bridge if available
2718
+ // (REST API never has annotations; Desktop Bridge has reliable descriptions)
2719
+ let annotations = [];
2720
+ let annotationSummary = { count: 0 };
2721
+ if (getDesktopConnector) {
2722
+ try {
2723
+ const connector = await getDesktopConnector();
2724
+ // Fetch annotations with child traversal (depth matches REST traversal)
2725
+ const annotResult = await connector.getAnnotations(nodeId, true, 4);
2726
+ if (annotResult?.success !== false && annotResult?.data) {
2727
+ const data = annotResult.data;
2728
+ annotations = data.annotations || [];
2729
+ const childAnnotations = data.children || [];
2730
+ const allAnnotations = [
2731
+ ...annotations,
2732
+ ...childAnnotations.flatMap((c) => (c.annotations || []).map((a) => ({ ...a, nodeId: c.nodeId, nodeName: c.nodeName })))
2733
+ ];
2734
+ annotationSummary = allAnnotations.length > 0
2735
+ ? {
2736
+ count: allAnnotations.length,
2737
+ labels: allAnnotations
2738
+ .filter((a) => a.label || a.labelMarkdown)
2739
+ .map((a) => ({
2740
+ text: a.labelMarkdown || a.label,
2741
+ ...(a.nodeId ? { onNode: a.nodeName } : {}),
2742
+ })),
2743
+ pinnedProperties: allAnnotations
2744
+ .filter((a) => a.properties && a.properties.length > 0)
2745
+ .flatMap((a) => a.properties.map((p) => p.type)),
2746
+ }
2747
+ : { count: 0 };
2748
+ }
2749
+ // Also fetch description from bridge if REST returned empty
2750
+ if (!componentData.description && !componentData.descriptionMarkdown) {
2751
+ const bridgeResult = await connector.getComponentFromPluginUI(nodeId);
2752
+ if (bridgeResult?.success && bridgeResult.component) {
2753
+ if (bridgeResult.component.descriptionMarkdown) {
2754
+ componentData.descriptionMarkdown = bridgeResult.component.descriptionMarkdown;
2755
+ }
2756
+ if (bridgeResult.component.description) {
2757
+ componentData.description = bridgeResult.component.description;
2758
+ }
2759
+ }
2760
+ }
2761
+ }
2762
+ catch {
2763
+ // Desktop Bridge unavailable — continue without annotations
2764
+ logger.debug("Desktop Bridge unavailable for annotations/description enrichment");
2765
+ }
2766
+ }
2558
2767
  // Get image if requested
2559
2768
  let imageUrl = null;
2560
2769
  if (includeImage) {
@@ -2570,23 +2779,142 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2570
2779
  logger.warn({ error }, "Failed to render component image, continuing without it");
2571
2780
  }
2572
2781
  }
2573
- // Build response with component data and image URL
2782
+ // Extract composition dependencies every INSTANCE sub-component used
2783
+ // This tells the AI which sub-components must exist before building this component
2784
+ const compositionDeps = new Map();
2785
+ const walkForInstances = (n) => {
2786
+ if (!n)
2787
+ return;
2788
+ if (n.type === "INSTANCE" && n.componentId) {
2789
+ const existing = compositionDeps.get(n.componentId);
2790
+ if (existing) {
2791
+ existing.count++;
2792
+ }
2793
+ else {
2794
+ compositionDeps.set(n.componentId, {
2795
+ name: n.name,
2796
+ componentId: n.componentId,
2797
+ count: 1,
2798
+ props: n.componentProperties ? Object.keys(n.componentProperties) : [],
2799
+ });
2800
+ }
2801
+ }
2802
+ if (n.children) {
2803
+ for (const child of n.children) {
2804
+ walkForInstances(child);
2805
+ }
2806
+ }
2807
+ };
2808
+ walkForInstances(componentData);
2809
+ const dependencies = Array.from(compositionDeps.values());
2810
+ // Scan codebase for existing components if path provided
2811
+ let codebaseRegistry = undefined;
2812
+ if (codebasePath) {
2813
+ const existingComponents = scanCodebaseComponents(codebasePath);
2814
+ if (existingComponents.length > 0) {
2815
+ // Cross-reference Figma dependencies against codebase components
2816
+ // Normalize a name to keywords for fuzzy matching
2817
+ // "Input label" → ["input", "label"], "FormLabel" → ["form", "label"], "_Helper text" → ["helper", "text"]
2818
+ const toKeywords = (name) => name.replace(/^_+/, "").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_/]/g, " ").toLowerCase().split(/\s+/).filter(w => w.length > 1);
2819
+ const crossRef = dependencies.map(dep => {
2820
+ const depNameLower = dep.name.replace(/^_/, "").replace(/\s+/g, "").toLowerCase();
2821
+ const depKeywords = toKeywords(dep.name);
2822
+ const match = existingComponents.find(c => {
2823
+ const cNameLower = c.name.toLowerCase();
2824
+ const cKeywords = toKeywords(c.name);
2825
+ // Exact name match
2826
+ if (cNameLower === depNameLower)
2827
+ return true;
2828
+ // Export name match
2829
+ if (c.exports.some(e => e.toLowerCase() === depNameLower))
2830
+ return true;
2831
+ // Substring containment
2832
+ if (depNameLower.includes(cNameLower) || cNameLower.includes(depNameLower))
2833
+ return true;
2834
+ // Keyword overlap — if most keywords from either name match, it's likely the same component
2835
+ // "Input label" ∩ "FormLabel" → ["label"] overlaps, plus "input" ~ "form" (both form-related)
2836
+ const overlap = depKeywords.filter(k => cKeywords.some(ck => ck.includes(k) || k.includes(ck)));
2837
+ if (overlap.length > 0 && overlap.length >= Math.min(depKeywords.length, cKeywords.length) * 0.5)
2838
+ return true;
2839
+ return false;
2840
+ });
2841
+ return {
2842
+ figmaComponent: dep.name,
2843
+ componentId: dep.componentId,
2844
+ codebaseMatch: match ? { name: match.name, path: match.path, exports: match.exports } : null,
2845
+ action: match ? "IMPORT_EXISTING" : "BUILD_NEW",
2846
+ };
2847
+ });
2848
+ codebaseRegistry = {
2849
+ scannedPath: codebasePath,
2850
+ existingComponents: existingComponents.map(c => ({ name: c.name, path: c.path, exports: c.exports })),
2851
+ componentCount: existingComponents.length,
2852
+ crossReference: crossRef.length > 0 ? crossRef : undefined,
2853
+ ai_instruction: `Found ${existingComponents.length} existing components in the target codebase. Components marked IMPORT_EXISTING MUST be imported — never recreate them. Components marked BUILD_NEW need to be created as standalone components (own directory, file, CSS module, stories) before building the parent.`,
2854
+ };
2855
+ }
2856
+ }
2857
+ // Build the full response
2858
+ const response = {
2859
+ fileKey,
2860
+ nodeId,
2861
+ imageUrl,
2862
+ component: componentData,
2863
+ annotations: annotationSummary,
2864
+ codebaseRegistry: codebaseRegistry || undefined,
2865
+ compositionDependencies: dependencies.length > 0 ? {
2866
+ count: dependencies.length,
2867
+ components: dependencies,
2868
+ ai_instruction: codebaseRegistry
2869
+ ? `MANDATORY: Cross-reference each dependency against codebaseRegistry.crossReference above. Components marked IMPORT_EXISTING must be imported from their listed path. Components marked BUILD_NEW must be created as standalone components (own directory, file, CSS module, stories) before building the parent. Never inline sub-component logic.`
2870
+ : "MANDATORY BEFORE WRITING ANY CODE: Scan the target codebase's component directory for existing implementations. If a matching component exists, IMPORT it — never recreate with inline markup. Each sub-component that does NOT exist must be built FIRST as standalone (own directory, file, CSS module, stories, barrel export) before building the parent.",
2871
+ } : undefined,
2872
+ metadata: {
2873
+ purpose: "component_development",
2874
+ treeDepth: 4,
2875
+ note: [
2876
+ imageUrl ? "Image URL provided (valid for 30 days)." : null,
2877
+ "Component data optimized for UI implementation with design tokens (boundVariables), interaction states (reactions), sizing constraints, and text behavior.",
2878
+ annotationSummary.count > 0 ? `${annotationSummary.count} design annotation(s) found — check annotations field for implementation specs.` : null,
2879
+ dependencies.length > 0 ? `COMPOSITION: ${dependencies.length} sub-component(s) detected (${dependencies.map(d => d.name).join(", ")}). Build these as standalone components first, then compose.` : null,
2880
+ "Use figma_get_annotations for full annotation details. Use figma_get_variables to resolve variable IDs to token names/values.",
2881
+ ].filter(Boolean).join(" "),
2882
+ },
2883
+ };
2884
+ // Adaptive compression for large responses (depth 4 can produce large payloads)
2885
+ const responseJson = JSON.stringify(response);
2886
+ const responseSizeKB = Math.round(responseJson.length / 1024);
2887
+ if (responseSizeKB > 500) {
2888
+ // Emergency: strip children beyond depth 2 and add truncation note
2889
+ logger.warn({ responseSizeKB, nodeId }, "Component response exceeds 500KB, truncating deep children");
2890
+ const truncate = (n, currentDepth) => {
2891
+ if (!n)
2892
+ return n;
2893
+ const copy = { ...n };
2894
+ if (copy.children && currentDepth >= 2) {
2895
+ copy.children = copy.children.map((c) => ({
2896
+ id: c.id, name: c.name, type: c.type,
2897
+ ...(c.componentId ? { componentId: c.componentId } : {}),
2898
+ ...(c.variantProperties ? { variantProperties: c.variantProperties } : {}),
2899
+ childCount: c.children?.length,
2900
+ }));
2901
+ copy._truncated = true;
2902
+ }
2903
+ else if (copy.children) {
2904
+ copy.children = copy.children.map((c) => truncate(c, currentDepth + 1));
2905
+ }
2906
+ return copy;
2907
+ };
2908
+ response.component = truncate(componentData, 0);
2909
+ response.metadata.truncated = true;
2910
+ response.metadata.originalSizeKB = responseSizeKB;
2911
+ response.metadata.note += " Response was truncated due to size. Use figma_execute for deeper traversal of specific subtrees.";
2912
+ }
2574
2913
  return {
2575
2914
  content: [
2576
2915
  {
2577
2916
  type: "text",
2578
- text: JSON.stringify({
2579
- fileKey,
2580
- nodeId,
2581
- imageUrl,
2582
- component: componentData,
2583
- metadata: {
2584
- purpose: "component_development",
2585
- note: imageUrl
2586
- ? "Image URL provided above (valid for 30 days). Full component data optimized for UI implementation."
2587
- : "Full component data optimized for UI implementation.",
2588
- },
2589
- }),
2917
+ text: JSON.stringify(response),
2590
2918
  },
2591
2919
  ],
2592
2920
  };
@@ -2798,7 +3126,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2798
3126
  // Tool 15: Capture Screenshot via Plugin (Desktop Bridge)
2799
3127
  // This uses exportAsync() which reads the current plugin runtime state, not the cloud state
2800
3128
  // Solves race condition where REST API screenshots show stale data after changes
2801
- server.tool("figma_capture_screenshot", "Capture a screenshot of a node using the plugin's exportAsync API. IMPORTANT: This tool captures the CURRENT state from the plugin runtime (not cloud state like REST API), making it reliable for validating changes immediately after making them. Use this instead of figma_get_component_image when you need to verify that changes were applied correctly. Requires Desktop Bridge connection (Figma Desktop with plugin running).", {
3129
+ server.tool("figma_capture_screenshot", "Capture a screenshot of a node using the plugin's exportAsync API. IMPORTANT: This tool captures the CURRENT state from the plugin runtime (not cloud state like REST API), making it reliable for validating changes immediately after making them. Use this instead of figma_get_component_image when you need to verify that changes were applied correctly. Defaults are AI-optimized: PNG at 1x with automatic downscaling so the longest side stays within the 1568px AI vision processing ceiling. PNG is the default because design tool content (flat colors, text, UI components) compresses significantly better as PNG. Use JPG for photographic or gradient-heavy content. Requires Desktop Bridge connection (Figma Desktop with plugin running).", {
2802
3130
  nodeId: z
2803
3131
  .string()
2804
3132
  .optional()
@@ -2807,14 +3135,14 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2807
3135
  .enum(["PNG", "JPG", "SVG"])
2808
3136
  .optional()
2809
3137
  .default("PNG")
2810
- .describe("Image format (default: PNG)"),
3138
+ .describe("Image format (default: PNG). Use JPG for photographic or gradient-heavy content."),
2811
3139
  scale: z
2812
3140
  .number()
2813
3141
  .min(0.5)
2814
3142
  .max(4)
2815
3143
  .optional()
2816
- .default(2)
2817
- .describe("Scale factor (default: 2 for 2x resolution)"),
3144
+ .default(1)
3145
+ .describe("Scale factor (default: 1). The plugin automatically caps the effective scale so the exported image does not exceed 1568px on its longest side (the AI vision processing ceiling)."),
2818
3146
  }, async ({ nodeId, format, scale }) => {
2819
3147
  try {
2820
3148
  logger.info({ nodeId, format, scale }, "Capturing screenshot via Desktop Bridge");
@@ -2882,6 +3210,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
2882
3210
  metadata: {
2883
3211
  source: "plugin_export_async",
2884
3212
  note: "Screenshot captured successfully. The image is included below for visual analysis. This shows the CURRENT plugin runtime state (guaranteed to reflect recent changes).",
3213
+ formatAdvice: result.image.formatAdvice || undefined,
2885
3214
  },
2886
3215
  }),
2887
3216
  },