@atezer/figma-mcp-bridge 1.9.4 → 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.4';
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 = [];
@@ -2985,64 +3075,140 @@ figma.ui.onmessage = async (msg) => {
2985
3075
  // ============================================================================
2986
3076
  else if (msg.type === 'CAPTURE_SCREENSHOT') {
2987
3077
  try {
2988
- 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);
2989
3079
 
2990
3080
  var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
2991
3081
  if (!node) {
2992
3082
  throw new Error('Node not found: ' + msg.nodeId);
2993
3083
  }
2994
3084
 
2995
- // Verify node supports export
2996
- if (!('exportAsync' in node)) {
2997
- throw new Error('Node type ' + node.type + ' does not support export');
2998
- }
2999
-
3000
- // v1.8.0: Default JPG@1x q70 for context safety. Plugin reads msg.format,
3001
- // msg.scale, msg.jpegQuality from the request payload (passed through
3002
- // captureScreenshot connector → bridge → here).
3085
+ // v1.8.0: Default JPG@1x q70 for context safety.
3003
3086
  var format = (msg.format || 'JPG').toUpperCase();
3004
3087
  var scale = msg.scale != null ? msg.scale : 1;
3005
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;
3006
3093
 
3007
- var exportSettings = {
3008
- format: format,
3009
- constraint: { type: 'SCALE', value: scale }
3010
- };
3011
- // JPG quality (Figma Plugin API supports 'quality' for ExportSettingsImage)
3012
- if (format === 'JPG') {
3013
- 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;
3014
3098
  }
3015
3099
 
3016
- // Export the node
3017
- 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
+ }
3018
3113
 
3019
- // Convert to base64
3020
- 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
+ }
3021
3172
 
3022
- // Get node bounds for context
3023
- var bounds = null;
3024
- if ('absoluteBoundingBox' in node) {
3025
- 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;
3026
3184
  }
3027
3185
 
3028
- console.log('🌉 [F-MCP ATezer Bridge] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
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');
3189
+ }
3190
+
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)');
3029
3196
 
3030
3197
  figma.ui.postMessage({
3031
3198
  type: 'CAPTURE_SCREENSHOT_RESULT',
3032
3199
  requestId: msg.requestId,
3033
3200
  success: true,
3201
+ mode: returnMode, // 'file' or 'base64' (server post-processes 'file' mode)
3034
3202
  image: {
3035
3203
  base64: base64,
3036
3204
  format: format,
3037
3205
  scale: scale,
3038
3206
  jpegQuality: format === 'JPG' ? jpegQuality : null,
3039
3207
  byteLength: bytes.length,
3040
- node: {
3041
- id: node.id,
3042
- name: node.name,
3043
- type: node.type
3044
- },
3045
- bounds: bounds
3208
+ width: node.width,
3209
+ height: node.height,
3210
+ node: { id: node.id, name: node.name, type: node.type },
3211
+ bounds: bounds,
3046
3212
  }
3047
3213
  });
3048
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.4';
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.4",
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",
@@ -184,6 +184,26 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
184
184
 
185
185
  16. **Her Promise'i `await` et.**
186
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
+
187
207
  ## 2. Sayfa Kuralları
188
208
 
189
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 |