@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.
- package/README.md +13 -12
- package/dist/cloudflare/core/annotation-tools.js +230 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +93 -0
- package/dist/cloudflare/core/deep-component-tools.js +128 -0
- package/dist/cloudflare/core/design-code-tools.js +65 -7
- package/dist/cloudflare/core/enrichment/enrichment-service.js +108 -12
- package/dist/cloudflare/core/figjam-tools.js +485 -0
- package/dist/cloudflare/core/figma-api.js +7 -4
- package/dist/cloudflare/core/figma-desktop-connector.js +108 -0
- package/dist/cloudflare/core/figma-tools.js +445 -55
- package/dist/cloudflare/core/port-discovery.js +88 -0
- package/dist/cloudflare/core/resolve-package-root.js +11 -0
- package/dist/cloudflare/core/slides-tools.js +607 -0
- package/dist/cloudflare/core/websocket-connector.js +93 -0
- package/dist/cloudflare/core/websocket-server.js +18 -9
- package/dist/cloudflare/index.js +164 -41
- package/dist/core/annotation-tools.d.ts +14 -0
- package/dist/core/annotation-tools.d.ts.map +1 -0
- package/dist/core/annotation-tools.js +231 -0
- package/dist/core/annotation-tools.js.map +1 -0
- package/dist/core/deep-component-tools.d.ts +14 -0
- package/dist/core/deep-component-tools.d.ts.map +1 -0
- package/dist/core/deep-component-tools.js +129 -0
- package/dist/core/deep-component-tools.js.map +1 -0
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +65 -7
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +108 -12
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/figma-api.d.ts +1 -1
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +7 -4
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +5 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.d.ts +20 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.js +83 -0
- package/dist/core/figma-desktop-connector.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +355 -26
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +21 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +88 -0
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/resolve-package-root.d.ts +2 -0
- package/dist/core/resolve-package-root.d.ts.map +1 -0
- package/dist/core/resolve-package-root.js +12 -0
- package/dist/core/resolve-package-root.js.map +1 -0
- package/dist/core/types/design-code.d.ts +1 -0
- package/dist/core/types/design-code.d.ts.map +1 -1
- package/dist/core/websocket-connector.d.ts +5 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +18 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +7 -9
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/local.d.ts +6 -0
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +58 -1
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +906 -4
- package/figma-desktop-bridge/ui-full.html +80 -0
- package/figma-desktop-bridge/ui.html +82 -0
- package/package.json +1 -1
package/dist/core/figma-tools.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
|
2443
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
2817
|
-
.describe("Scale factor (default:
|
|
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
|
},
|