@atezer/figma-mcp-bridge 1.9.3 → 1.9.5

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.
@@ -6,7 +6,97 @@
6
6
 
7
7
  // v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
8
8
  // Keep in sync with package.json and src/core/version.ts.
9
- var FMCP_PLUGIN_VERSION = '1.9.2';
9
+ var FMCP_PLUGIN_VERSION = '1.9.5';
10
+
11
+ // v1.9.5: Metadata summary builder for returnMode='summary' (screenshotsuz)
12
+ async function buildNodeSummary(node, maxDepth) {
13
+ var colorUsage = {}; // role → count
14
+ var textRoleCount = {}; // styleId → { count, samples }
15
+ var unboundHexCount = 0;
16
+ var totalNodes = 0;
17
+
18
+ function walk(n, depth) {
19
+ totalNodes++;
20
+ // Fill bound token tracking
21
+ if (Array.isArray(n.fills)) {
22
+ for (var fi = 0; fi < n.fills.length; fi++) {
23
+ var fl = n.fills[fi];
24
+ if (fl && fl.visible !== false && fl.type === 'SOLID') {
25
+ var fbv = n.boundVariables && n.boundVariables.fills;
26
+ var bound = fbv && (Array.isArray(fbv) ? fbv[fi] : true);
27
+ if (bound) {
28
+ var role = (Array.isArray(fbv) ? fbv[fi] : fbv).id || 'unknown';
29
+ colorUsage[role] = (colorUsage[role] || 0) + 1;
30
+ } else {
31
+ unboundHexCount++;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ if (n.type === 'TEXT') {
37
+ var styleId = n.textStyleId || 'inline';
38
+ if (!textRoleCount[styleId]) textRoleCount[styleId] = { count: 0, samples: [] };
39
+ textRoleCount[styleId].count++;
40
+ if (textRoleCount[styleId].samples.length < 3) {
41
+ textRoleCount[styleId].samples.push(String(n.characters || '').slice(0, 30));
42
+ }
43
+ }
44
+ if (n.children && depth < 3) { // deep walk but capped
45
+ for (var ci = 0; ci < n.children.length; ci++) walk(n.children[ci], depth + 1);
46
+ }
47
+ }
48
+
49
+ walk(node, 0);
50
+
51
+ function collectSections(n, depth) {
52
+ if (depth > maxDepth) return null;
53
+ var s = {
54
+ name: n.name,
55
+ type: n.type,
56
+ size: n.width && n.height ? Math.round(n.width) + 'x' + Math.round(n.height) : null,
57
+ childrenCount: n.children ? n.children.length : 0,
58
+ };
59
+ if (n.children && n.children.length > 0 && depth < maxDepth) {
60
+ var items = [];
61
+ for (var i = 0; i < n.children.length && i < 20; i++) {
62
+ items.push(collectSections(n.children[i], depth + 1));
63
+ }
64
+ s.children = items;
65
+ }
66
+ return s;
67
+ }
68
+
69
+ // Resolve variable names (best-effort)
70
+ var dominantColors = [];
71
+ var colorEntries = Object.keys(colorUsage).map(function (k) { return { role: k, usage: colorUsage[k] }; });
72
+ colorEntries.sort(function (a, b) { return b.usage - a.usage; });
73
+ for (var ci2 = 0; ci2 < Math.min(colorEntries.length, 5); ci2++) {
74
+ dominantColors.push({ variableId: colorEntries[ci2].role, usageCount: colorEntries[ci2].usage, bound: true });
75
+ }
76
+ if (unboundHexCount > 0) {
77
+ dominantColors.push({ variableId: '<hardcoded>', usageCount: unboundHexCount, bound: false });
78
+ }
79
+
80
+ var textRoles = [];
81
+ var trKeys = Object.keys(textRoleCount);
82
+ for (var tk = 0; tk < trKeys.length; tk++) {
83
+ textRoles.push({ styleId: trKeys[tk], count: textRoleCount[trKeys[tk]].count, samples: textRoleCount[trKeys[tk]].samples });
84
+ }
85
+
86
+ return {
87
+ nodeId: node.id,
88
+ name: node.name,
89
+ type: node.type,
90
+ dimensions: { width: Math.round(node.width || 0), height: Math.round(node.height || 0) },
91
+ layoutMode: node.layoutMode || 'NONE',
92
+ childrenCount: node.children ? node.children.length : 0,
93
+ totalNodes: totalNodes,
94
+ sections: collectSections(node, 0),
95
+ dominantColors: dominantColors,
96
+ textRoles: textRoles.slice(0, 10),
97
+ hint: "v1.9.5 summary: Metadata ozeti, gorsel yok. Layout karari icin yeter. Pixel dogrulama gerekirse returnMode='file' veya 'regions'.",
98
+ };
99
+ }
10
100
 
11
101
  // Console log buffer for figma_get_console_logs (no CDP)
12
102
  var __consoleLogBuffer = [];
@@ -2391,6 +2481,9 @@ figma.ui.onmessage = async (msg) => {
2391
2481
  // Pass 2: Promise.all on getMainComponentAsync for all instances in parallel
2392
2482
  // This cuts 100+ node validation from ~60s to ~5s.
2393
2483
 
2484
+ // v1.9.4: detailed param — if true, collect hardcoded samples + overflow + primitive fallbacks.
2485
+ var detailed = msg.detailed === true;
2486
+
2394
2487
  // Iterative tree walk (stack-safe for deep trees up to 10,000+ nodes)
2395
2488
  var metrics = {
2396
2489
  totalNodes: 0,
@@ -2400,11 +2493,33 @@ figma.ui.onmessage = async (msg) => {
2400
2493
  libraryInstanceNodes: 0,
2401
2494
  stylableNodes: 0,
2402
2495
  nodesWithBindings: 0,
2496
+ // v1.9.4: extra coverage metrics
2497
+ boundFills: 0,
2498
+ unboundSolidFills: 0,
2499
+ paddingsTotal: 0,
2500
+ paddingsBound: 0,
2501
+ itemSpacingTotal: 0,
2502
+ itemSpacingBound: 0,
2503
+ radiusTotal: 0,
2504
+ radiusBound: 0,
2505
+ textsTotal: 0,
2506
+ textsWithStyle: 0,
2507
+ textsHardcodedFontSize: 0,
2508
+ textColorsTotal: 0,
2509
+ textColorsBound: 0,
2510
+ strokesTotal: 0,
2511
+ strokesBound: 0,
2403
2512
  };
2404
2513
  var violations = [];
2405
2514
  var maxViolations = 20; // cap to keep response small
2406
2515
  var instanceNodesList = []; // v1.8.2: collect for batch resolve
2407
2516
 
2517
+ // v1.9.4: detailed mode samples (capped small)
2518
+ var hardcodedHexSamples = [];
2519
+ var hardcodedFontSizeSamples = [];
2520
+ var primitiveFallbacks = [];
2521
+ var maxSamples = 8;
2522
+
2408
2523
  var walkStack = [rootNode];
2409
2524
  var visited = 0;
2410
2525
  var maxVisit = 5000; // safety cap
@@ -2479,6 +2594,95 @@ figma.ui.onmessage = async (msg) => {
2479
2594
  }
2480
2595
  }
2481
2596
 
2597
+ // v1.9.4: Granular bind coverage — fills/strokes/paddings/radius/itemSpacing/text
2598
+ // Separate from stylableNodes/nodesWithBindings (which is coarse grained).
2599
+ try {
2600
+ if (Array.isArray(node.fills) && node.type !== 'INSTANCE') {
2601
+ for (var fi = 0; fi < node.fills.length; fi++) {
2602
+ var fl = node.fills[fi];
2603
+ if (fl && fl.visible !== false && fl.type === 'SOLID') {
2604
+ var fbv = node.boundVariables && node.boundVariables.fills;
2605
+ var fillBound = !!(fbv && (Array.isArray(fbv) ? fbv[fi] : true));
2606
+ if (fillBound) metrics.boundFills++;
2607
+ else {
2608
+ metrics.unboundSolidFills++;
2609
+ if (detailed && hardcodedHexSamples.length < maxSamples) {
2610
+ var c = fl.color;
2611
+ var hex = '#' + [c.r, c.g, c.b].map(function (v) { return Math.round(v * 255).toString(16).padStart(2, '0'); }).join('');
2612
+ hardcodedHexSamples.push({ node: node.name, type: node.type, hex: hex });
2613
+ }
2614
+ }
2615
+ }
2616
+ }
2617
+ }
2618
+ if (Array.isArray(node.strokes) && node.type !== 'INSTANCE') {
2619
+ for (var si = 0; si < node.strokes.length; si++) {
2620
+ var st = node.strokes[si];
2621
+ if (st && st.type === 'SOLID') {
2622
+ metrics.strokesTotal++;
2623
+ var sbv = node.boundVariables && node.boundVariables.strokes;
2624
+ if (sbv && (Array.isArray(sbv) ? sbv[si] : true)) metrics.strokesBound++;
2625
+ }
2626
+ }
2627
+ }
2628
+ } catch (e) { /* skip */ }
2629
+
2630
+ if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
2631
+ try {
2632
+ var padProps = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'];
2633
+ for (var pi = 0; pi < padProps.length; pi++) {
2634
+ var pp = padProps[pi];
2635
+ if (typeof node[pp] === 'number' && node[pp] > 0) {
2636
+ metrics.paddingsTotal++;
2637
+ if (node.boundVariables && node.boundVariables[pp]) metrics.paddingsBound++;
2638
+ }
2639
+ }
2640
+ if (typeof node.itemSpacing === 'number' && node.itemSpacing > 0) {
2641
+ metrics.itemSpacingTotal++;
2642
+ if (node.boundVariables && node.boundVariables.itemSpacing) metrics.itemSpacingBound++;
2643
+ }
2644
+ var radProps = ['cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius'];
2645
+ for (var ri = 0; ri < radProps.length; ri++) {
2646
+ var rp = radProps[ri];
2647
+ if (typeof node[rp] === 'number' && node[rp] > 0) {
2648
+ metrics.radiusTotal++;
2649
+ if (node.boundVariables && node.boundVariables[rp]) metrics.radiusBound++;
2650
+ }
2651
+ }
2652
+ // v1.9.4 detailed: flag primitive frames that *should* have been components
2653
+ if (detailed && node.type === 'FRAME' && node !== rootNode && primitiveFallbacks.length < maxSamples && node.children && node.children.length >= 2) {
2654
+ var onlyPrimitives = true;
2655
+ for (var cpi = 0; cpi < node.children.length; cpi++) {
2656
+ if (node.children[cpi].type === 'INSTANCE') { onlyPrimitives = false; break; }
2657
+ }
2658
+ if (onlyPrimitives) primitiveFallbacks.push({ name: node.name, childCount: node.children.length });
2659
+ }
2660
+ } catch (e) { /* skip */ }
2661
+ }
2662
+
2663
+ if (node.type === 'TEXT') {
2664
+ try {
2665
+ metrics.textsTotal++;
2666
+ if (node.textStyleId && typeof node.textStyleId === 'string' && node.textStyleId !== '') {
2667
+ metrics.textsWithStyle++;
2668
+ } else {
2669
+ metrics.textsHardcodedFontSize++;
2670
+ if (detailed && hardcodedFontSizeSamples.length < maxSamples) {
2671
+ hardcodedFontSizeSamples.push({
2672
+ text: String(node.characters || '').slice(0, 40),
2673
+ fontSize: node.fontSize,
2674
+ fontName: node.fontName,
2675
+ });
2676
+ }
2677
+ }
2678
+ if (Array.isArray(node.fills) && node.fills.length > 0) {
2679
+ metrics.textColorsTotal++;
2680
+ var tbv = node.boundVariables && node.boundVariables.fills;
2681
+ if (tbv && (Array.isArray(tbv) ? tbv[0] : true)) metrics.textColorsBound++;
2682
+ }
2683
+ } catch (e) { /* skip */ }
2684
+ }
2685
+
2482
2686
  // Push children
2483
2687
  if (node.children) {
2484
2688
  for (var ci = 0; ci < node.children.length; ci++) {
@@ -2543,7 +2747,50 @@ figma.ui.onmessage = async (msg) => {
2543
2747
  : 'Score ' + score + '/100 is below minimum ' + minScore + '. Consider deleting this screen and rebuilding using ' +
2544
2748
  'DS components (figma_instantiate_component) and token bindings (figma_bind_variable).';
2545
2749
 
2546
- figma.ui.postMessage({
2750
+ // v1.9.4: granular bind coverage percentages
2751
+ function pct(b, t) { return t > 0 ? Math.round((b / t) * 100) : 100; }
2752
+ var fillsTotal = metrics.boundFills + metrics.unboundSolidFills;
2753
+ var coverage = {
2754
+ fills: { bound: metrics.boundFills, total: fillsTotal, pct: pct(metrics.boundFills, fillsTotal) },
2755
+ paddings: { bound: metrics.paddingsBound, total: metrics.paddingsTotal, pct: pct(metrics.paddingsBound, metrics.paddingsTotal) },
2756
+ itemSpacing: { bound: metrics.itemSpacingBound, total: metrics.itemSpacingTotal, pct: pct(metrics.itemSpacingBound, metrics.itemSpacingTotal) },
2757
+ radius: { bound: metrics.radiusBound, total: metrics.radiusTotal, pct: pct(metrics.radiusBound, metrics.radiusTotal) },
2758
+ textStyle: { withStyle: metrics.textsWithStyle, hardcoded: metrics.textsHardcodedFontSize, total: metrics.textsTotal, pct: pct(metrics.textsWithStyle, metrics.textsTotal) },
2759
+ textColor: { bound: metrics.textColorsBound, total: metrics.textColorsTotal, pct: pct(metrics.textColorsBound, metrics.textColorsTotal) },
2760
+ strokes: { bound: metrics.strokesBound, total: metrics.strokesTotal, pct: pct(metrics.strokesBound, metrics.strokesTotal) },
2761
+ };
2762
+
2763
+ // v1.9.4: overflow check (root level auto-layout containers)
2764
+ var overflow = null;
2765
+ try {
2766
+ if (rootNode.layoutMode === 'VERTICAL' && rootNode.children) {
2767
+ var contentH = 0;
2768
+ for (var chi = 0; chi < rootNode.children.length; chi++) contentH += (rootNode.children[chi].height || 0);
2769
+ if (rootNode.itemSpacing) contentH += rootNode.itemSpacing * Math.max(0, rootNode.children.length - 1);
2770
+ contentH += (rootNode.paddingTop || 0) + (rootNode.paddingBottom || 0);
2771
+ overflow = {
2772
+ axis: 'VERTICAL',
2773
+ frameSize: Math.round(rootNode.height || 0),
2774
+ contentSize: Math.round(contentH),
2775
+ overflowPx: Math.round(contentH - (rootNode.height || 0)),
2776
+ clipsContent: !!rootNode.clipsContent,
2777
+ };
2778
+ } else if (rootNode.layoutMode === 'HORIZONTAL' && rootNode.children) {
2779
+ var contentW = 0;
2780
+ for (var cwi = 0; cwi < rootNode.children.length; cwi++) contentW += (rootNode.children[cwi].width || 0);
2781
+ if (rootNode.itemSpacing) contentW += rootNode.itemSpacing * Math.max(0, rootNode.children.length - 1);
2782
+ contentW += (rootNode.paddingLeft || 0) + (rootNode.paddingRight || 0);
2783
+ overflow = {
2784
+ axis: 'HORIZONTAL',
2785
+ frameSize: Math.round(rootNode.width || 0),
2786
+ contentSize: Math.round(contentW),
2787
+ overflowPx: Math.round(contentW - (rootNode.width || 0)),
2788
+ clipsContent: !!rootNode.clipsContent,
2789
+ };
2790
+ }
2791
+ } catch (e) { overflow = null; }
2792
+
2793
+ var response = {
2547
2794
  type: 'VALIDATE_SCREEN_RESULT',
2548
2795
  requestId: msg.requestId,
2549
2796
  success: true,
@@ -2561,12 +2808,25 @@ figma.ui.onmessage = async (msg) => {
2561
2808
  nodesWithBindings: metrics.nodesWithBindings,
2562
2809
  totalStylable: metrics.stylableNodes,
2563
2810
  },
2811
+ coverage: coverage, // v1.9.4: granular coverage
2812
+ overflow: overflow, // v1.9.4: root auto-layout overflow
2564
2813
  violations: violations,
2565
2814
  violationCount: violations.length,
2566
2815
  violationCapped: violations.length >= maxViolations,
2567
2816
  totalNodesWalked: visited,
2568
2817
  recommendation: recommendation,
2569
- });
2818
+ };
2819
+
2820
+ // v1.9.4: detailed mode — hardcoded samples + primitive fallbacks
2821
+ if (detailed) {
2822
+ response.samples = {
2823
+ hardcodedHex: hardcodedHexSamples,
2824
+ hardcodedFontSize: hardcodedFontSizeSamples,
2825
+ primitiveFrames: primitiveFallbacks,
2826
+ };
2827
+ }
2828
+
2829
+ figma.ui.postMessage(response);
2570
2830
  } catch (error) {
2571
2831
  var errorMsg = error && error.message ? error.message : String(error);
2572
2832
  console.error('🌉 [F-MCP] Validate screen error:', errorMsg);
@@ -2815,64 +3075,140 @@ figma.ui.onmessage = async (msg) => {
2815
3075
  // ============================================================================
2816
3076
  else if (msg.type === 'CAPTURE_SCREENSHOT') {
2817
3077
  try {
2818
- console.log('🌉 [F-MCP ATezer Bridge] Capturing screenshot for node:', msg.nodeId);
3078
+ console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Capturing screenshot for node:', msg.nodeId, 'returnMode:', msg.returnMode);
2819
3079
 
2820
3080
  var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
2821
3081
  if (!node) {
2822
3082
  throw new Error('Node not found: ' + msg.nodeId);
2823
3083
  }
2824
3084
 
2825
- // Verify node supports export
2826
- if (!('exportAsync' in node)) {
2827
- throw new Error('Node type ' + node.type + ' does not support export');
2828
- }
2829
-
2830
- // v1.8.0: Default JPG@1x q70 for context safety. Plugin reads msg.format,
2831
- // msg.scale, msg.jpegQuality from the request payload (passed through
2832
- // captureScreenshot connector → bridge → here).
3085
+ // v1.8.0: Default JPG@1x q70 for context safety.
2833
3086
  var format = (msg.format || 'JPG').toUpperCase();
2834
3087
  var scale = msg.scale != null ? msg.scale : 1;
2835
3088
  var jpegQuality = msg.jpegQuality != null ? msg.jpegQuality : 70;
3089
+ // v1.9.5: returnMode (default 'file' which server translates to base64 + save)
3090
+ var returnMode = msg.returnMode || 'base64'; // plugin default stays base64; server wraps to file
3091
+ var regionStrategy = msg.regionStrategy || 'children';
3092
+ var maxRegions = msg.maxRegions != null ? msg.maxRegions : 8;
2836
3093
 
2837
- var exportSettings = {
2838
- format: format,
2839
- constraint: { type: 'SCALE', value: scale }
2840
- };
2841
- // JPG quality (Figma Plugin API supports 'quality' for ExportSettingsImage)
2842
- if (format === 'JPG') {
2843
- exportSettings.quality = jpegQuality;
3094
+ function buildExportSettings() {
3095
+ var s = { format: format, constraint: { type: 'SCALE', value: scale } };
3096
+ if (format === 'JPG') s.quality = jpegQuality;
3097
+ return s;
2844
3098
  }
2845
3099
 
2846
- // Export the node
2847
- var bytes = await node.exportAsync(exportSettings);
3100
+ // ---------- v1.9.5 MODE: summary ----------
3101
+ // No export. Just metadata.
3102
+ if (returnMode === 'summary') {
3103
+ var rootSummary = await buildNodeSummary(node, /*depth*/ 2);
3104
+ figma.ui.postMessage({
3105
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3106
+ requestId: msg.requestId,
3107
+ success: true,
3108
+ mode: 'summary',
3109
+ summary: rootSummary,
3110
+ });
3111
+ return;
3112
+ }
2848
3113
 
2849
- // Convert to base64
2850
- var base64 = figma.base64Encode(bytes);
3114
+ // ---------- v1.9.5 MODE: regions (children strategy) ----------
3115
+ if (returnMode === 'regions' && regionStrategy === 'children') {
3116
+ if (!node.children || node.children.length === 0) {
3117
+ figma.ui.postMessage({
3118
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3119
+ requestId: msg.requestId,
3120
+ success: false,
3121
+ error: "Node has no children for regions mode. Use 'file' or 'summary' instead.",
3122
+ });
3123
+ return;
3124
+ }
3125
+ var regions = [];
3126
+ var children = node.children.slice(0, maxRegions);
3127
+ for (var ri = 0; ri < children.length; ri++) {
3128
+ var child = children[ri];
3129
+ if (!('exportAsync' in child)) continue;
3130
+ try {
3131
+ var cbytes = await child.exportAsync(buildExportSettings());
3132
+ var cbase64 = figma.base64Encode(cbytes);
3133
+ regions.push({
3134
+ name: child.name,
3135
+ nodeId: child.id,
3136
+ type: child.type,
3137
+ image: {
3138
+ base64: cbase64,
3139
+ format: format,
3140
+ scale: scale,
3141
+ byteLength: cbytes.length,
3142
+ width: child.width,
3143
+ height: child.height,
3144
+ },
3145
+ });
3146
+ } catch (childErr) {
3147
+ regions.push({
3148
+ name: child.name,
3149
+ nodeId: child.id,
3150
+ error: childErr && childErr.message ? childErr.message : String(childErr),
3151
+ });
3152
+ }
3153
+ }
3154
+ figma.ui.postMessage({
3155
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3156
+ requestId: msg.requestId,
3157
+ success: true,
3158
+ mode: 'regions',
3159
+ regionStrategy: 'children',
3160
+ regions: regions,
3161
+ totalRegions: regions.length,
3162
+ rootSummary: {
3163
+ nodeId: node.id,
3164
+ name: node.name,
3165
+ dimensions: { width: node.width, height: node.height },
3166
+ childrenCount: node.children.length,
3167
+ capped: node.children.length > maxRegions,
3168
+ },
3169
+ });
3170
+ return;
3171
+ }
2851
3172
 
2852
- // Get node bounds for context
2853
- var bounds = null;
2854
- if ('absoluteBoundingBox' in node) {
2855
- bounds = node.absoluteBoundingBox;
3173
+ // ---------- v1.9.5 MODE: regions (slices strategy) — NOT SUPPORTED YET ----------
3174
+ if (returnMode === 'regions' && regionStrategy === 'slices') {
3175
+ // Figma exportAsync does not support crop regions natively.
3176
+ // Fallback: return a warning + single full image export.
3177
+ figma.ui.postMessage({
3178
+ type: 'CAPTURE_SCREENSHOT_RESULT',
3179
+ requestId: msg.requestId,
3180
+ success: false,
3181
+ error: "regionStrategy='slices' not supported in v1.9.5. Use regionStrategy='children' or returnMode='summary'.",
3182
+ });
3183
+ return;
3184
+ }
3185
+
3186
+ // ---------- DEFAULT: full single export (base64 or file — server post-processes) ----------
3187
+ if (!('exportAsync' in node)) {
3188
+ throw new Error('Node type ' + node.type + ' does not support export');
2856
3189
  }
2857
3190
 
2858
- console.log('🌉 [F-MCP ATezer Bridge] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
3191
+ var bytes = await node.exportAsync(buildExportSettings());
3192
+ var base64 = figma.base64Encode(bytes);
3193
+ var bounds = ('absoluteBoundingBox' in node) ? node.absoluteBoundingBox : null;
3194
+
3195
+ console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
2859
3196
 
2860
3197
  figma.ui.postMessage({
2861
3198
  type: 'CAPTURE_SCREENSHOT_RESULT',
2862
3199
  requestId: msg.requestId,
2863
3200
  success: true,
3201
+ mode: returnMode, // 'file' or 'base64' (server post-processes 'file' mode)
2864
3202
  image: {
2865
3203
  base64: base64,
2866
3204
  format: format,
2867
3205
  scale: scale,
2868
3206
  jpegQuality: format === 'JPG' ? jpegQuality : null,
2869
3207
  byteLength: bytes.length,
2870
- node: {
2871
- id: node.id,
2872
- name: node.name,
2873
- type: node.type
2874
- },
2875
- bounds: bounds
3208
+ width: node.width,
3209
+ height: node.height,
3210
+ node: { id: node.id, name: node.name, type: node.type },
3211
+ bounds: bounds,
2876
3212
  }
2877
3213
  });
2878
3214
 
@@ -424,7 +424,7 @@
424
424
  window.__figmaFileName = null;
425
425
  // v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
426
426
  // Keep in sync with package.json, src/core/version.ts, f-mcp-plugin/code.js.
427
- var FMCP_PLUGIN_VERSION = '1.9.2';
427
+ var FMCP_PLUGIN_VERSION = '1.9.5';
428
428
 
429
429
  // v1.9.2+ Startup log — Figma plugin iframe'de yeni kod yüklendiğini doğrulamak için.
430
430
  // Console'da bu log görünüyorsa plugin güncel. Görünmüyorsa iframe eski kopyayı tutuyor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atezer/figma-mcp-bridge",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "F-MCP ATezer: MCP server and Figma plugin bridge for Claude/Cursor. No REST token required.",
5
5
  "type": "module",
6
6
  "main": "dist/local-plugin-only.js",
@@ -119,6 +119,54 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
119
119
 
120
120
  Hardcoded `node.fills = [{ type: "SOLID", color: {...} }]` YASAK.
121
121
 
122
+ 10a. **Inline Bind Verification (v1.9.4, ZORUNLU).** Her execute sonunda oluşturulan node'ları tara. Bind eksikse `throw` atılır — execute başarısız sayılır, Claude retry eder. Şablon:
123
+
124
+ ```js
125
+ // Execute'un sonunda, createdNodes listesini tarayıp bind kontrolü yap:
126
+ function assertBound(node) {
127
+ // Fill bind
128
+ if (Array.isArray(node.fills)) {
129
+ for (var i = 0; i < node.fills.length; i++) {
130
+ var f = node.fills[i];
131
+ if (f && f.visible !== false && f.type === "SOLID") {
132
+ var fbv = node.boundVariables && node.boundVariables.fills;
133
+ var bound = fbv && (Array.isArray(fbv) ? fbv[i] : true);
134
+ if (!bound) throw new Error("UNBOUND_FILL: " + node.name + " — setBoundVariableForPaint cagrisi eksik");
135
+ }
136
+ }
137
+ }
138
+ // Padding/itemSpacing/radius bind (auto-layout frame)
139
+ if (node.type === "FRAME" || node.type === "COMPONENT") {
140
+ var padProps = ["paddingTop","paddingBottom","paddingLeft","paddingRight"];
141
+ for (var j = 0; j < padProps.length; j++) {
142
+ var p = padProps[j];
143
+ if (typeof node[p] === "number" && node[p] > 0 && !(node.boundVariables && node.boundVariables[p])) {
144
+ throw new Error("UNBOUND_PADDING: " + node.name + "." + p + "=" + node[p] + " — setBoundVariable cagrisi eksik");
145
+ }
146
+ }
147
+ if (typeof node.itemSpacing === "number" && node.itemSpacing > 0 && !(node.boundVariables && node.boundVariables.itemSpacing)) {
148
+ throw new Error("UNBOUND_ITEMSPACING: " + node.name + " — setBoundVariable('itemSpacing', v) cagrisi eksik");
149
+ }
150
+ var radProps = ["cornerRadius","topLeftRadius","topRightRadius","bottomLeftRadius","bottomRightRadius"];
151
+ for (var k = 0; k < radProps.length; k++) {
152
+ var r = radProps[k];
153
+ if (typeof node[r] === "number" && node[r] > 0 && !(node.boundVariables && node.boundVariables[r])) {
154
+ throw new Error("UNBOUND_RADIUS: " + node.name + "." + r + "=" + node[r]);
155
+ }
156
+ }
157
+ }
158
+ // Text style
159
+ if (node.type === "TEXT" && !(node.textStyleId && typeof node.textStyleId === "string" && node.textStyleId !== "")) {
160
+ throw new Error("UNBOUND_TEXTSTYLE: '" + (node.characters||"").slice(0,30) + "' — setTextStyleIdAsync(style.id) cagrisi eksik");
161
+ }
162
+ }
163
+
164
+ // Tüm oluşturulan node'ları doğrula:
165
+ for (var n = 0; n < createdNodes.length; n++) assertBound(createdNodes[n]);
166
+ ```
167
+
168
+ Bu kontrol atlanırsa `figma_scan_ds_compliance` final gate'te sonuç zaten BLOCKING olur — ama inline check erkendedir ve context'i az yer. **v1.9.4 önerisi:** Her mega-step sonunda bu assertion bloğunu execute'un sonuna koy.
169
+
122
170
  11. **appendChild sıralaması kritik.** ÖNCE `parent.appendChild(child)`, SONRA `child.layoutSizingHorizontal = "FILL"` / `layoutPositioning = "ABSOLUTE"`:
123
171
  ```js
124
172
  parent.appendChild(child); // ÖNCE
@@ -136,6 +184,26 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
136
184
 
137
185
  16. **Her Promise'i `await` et.**
138
186
 
187
+ 17. **v1.9.5 Concise Query Rule — tek satır boyut/metadata sorguları.**
188
+
189
+ Boyut, renk, layout sorgulamak için **20+ satır JS YAZMA**. Tek satır yeter:
190
+
191
+ ```js
192
+ // İYİ — tek satır, minimal context
193
+ return await figma.getNodeByIdAsync(id).then(n => ({ w: n.width, h: n.height, children: n.children?.length }));
194
+
195
+ // İYİ — toplu (birden fazla node)
196
+ return Promise.all(ids.map(async id => {
197
+ const n = await figma.getNodeByIdAsync(id);
198
+ return { id, name: n?.name, size: n ? [n.width, n.height] : null };
199
+ }));
200
+
201
+ // KÖTÜ — 30 satır "defensive" kod boyut sorgusu için
202
+ // await figma.loadAllPagesAsync(); for (var pg of ...) { if (...) ... }
203
+ ```
204
+
205
+ **Kural:** Tek nodeId için size → 1 satır. Multi-node metadata → 1 Promise.all. Complex traversal sadece üretim mantığında gerekli — keşifte değil.
206
+
139
207
  ## 2. Sayfa Kuralları
140
208
 
141
209
  ```js
@@ -226,6 +226,15 @@ FOR each input in required_inputs:
226
226
 
227
227
  Missing list'teki tüm soruları **TEK** `AskUserQuestion` çağrısında toplayıp sor. Max 4 soru/çağrı (AskUserQuestion limiti). 5+ soru gerekiyorsa 2 ayrı çağrı yap, ama ÖNCE kritik 4'ü sor, sonra kalanları.
228
228
 
229
+ ### v1.9.5 Elicitation Kuralı (SERT)
230
+
231
+ - **Maks 1 `AskUserQuestion` çağrısı** tüm oturum boyunca. Tekrar soru sormak yasak — state'ten veya context'ten çıkar.
232
+ - **"devam et" / "tamam" / "ok" / "yap" sonrası soru YASAK.** Kullanıcı onay verdi → üretime geç.
233
+ - User prompt'u anlamlıysa (spesifik ekran adı, boyut, DS veriliyorsa) **hiç sorma**, direkt planla ve göster.
234
+ - **"Sen seç"** cevabı alırsan: mantıklı default kullan (iPhone 17, mobile, single variant, active DS).
235
+ - Sorulan her soru ≤30 kelime — uzun elicitation yasak.
236
+ - Soruyu sormanın context maliyeti ≥ yanıtın değeri mi? Değilse sorma.
237
+
229
238
  **Örnek — generate-figma-screen:**
230
239
 
231
240
  ```
@@ -144,6 +144,35 @@ Benchmark/görselden DEĞER alma YASAK. Sadece NİYET: layout yönü, hiyerarşi
144
144
  2. **Content** — DS instance yerleşimi → screenshot → onay
145
145
  3. **Polish** — spacing, states, edge cases → son screenshot → audit
146
146
 
147
+ ### v1.9.5 Discovery Budget Rule (SERT)
148
+
149
+ - **Maks 3 discovery çağrısı** (figma_get_*, figma_search_*, figma_execute read-only) sonra plan sun.
150
+ - Plan kullanıcıya 1-2 cümle + varsa özet: "Şu ekran/section'ları oluşturacağım: [liste]. Onay veriyor musun?"
151
+ - Kullanıcı onay verdikten sonra **mutation** aşamasına geç (figma_execute createFrame/setFills/setBoundVariable) — discovery counter reset olur.
152
+ - Plugin 8 çağrıdan sonra `_warnings: ["DISCOVERY_BUDGET_WARNING..."]` döner — görünce üretime geçmek zorundasın.
153
+ - 12 çağrıdan sonra `_DISCOVERY_BUDGET_EXCEEDED_BLOCKING: true` döner — **skip edilemez**, plan sun veya dur.
154
+
155
+ ### v1.9.5 Screenshot Method Selection (KARAR AĞACI)
156
+
157
+ Ekran yakalama isteği olduğunda şu ağacı takip et:
158
+
159
+ ```
160
+ İhtiyaç ne?
161
+ ├── "Planlama yapacağım, layout anlamak istiyorum" → returnMode: "summary" (metadata, screenshotsuz)
162
+ ├── "Kullanıcıya son halini göstereyim" → returnMode: "file" (1 screenshot dosyaya)
163
+ ├── "Büyük/scroll'lu ekran, bölümleri görmek istiyorum" → returnMode: "regions", regionStrategy: "children"
164
+ ├── "Üretim sonrası hızlı validation" → returnMode: "file"
165
+ ├── "Spesifik region (örn Hero)" → single-node file veya regions children maxRegions=3
166
+ └── "Base64 context'te gerekli (nadiren)" → returnMode: "base64" (explicit, _warning ile)
167
+ ```
168
+
169
+ **Budget farkındalığı:**
170
+ - Bir oturumda >3 farklı nodeId için screenshot → 4. için zorunlu `summary` veya `regions`
171
+ - Response'da `_warnings: ["CONTEXT_NEAR_LIMIT"]` veya `"DISCOVERY_BUDGET_..."` görürsen → sonraki screenshot zorunlu `summary`
172
+ - Pixel-perfect değil, **region-perfect** düşün: "Hero'yu göreyim, gerekirse Actions'ı da" — hepsi birden değil
173
+
174
+ **Kritik:** Screenshot YASAK değil. "Yasak" yerine **doğru yöntem** kullan. Her mode'un kullanım amacı var.
175
+
147
176
  ### Error Recovery
148
177
 
149
178
  | Hata | Aksiyon |