@atezer/figma-mcp-bridge 1.7.23 → 1.7.24

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 (77) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +1 -1
  3. package/dist/core/config.d.ts +1 -5
  4. package/dist/core/config.d.ts.map +1 -1
  5. package/dist/core/config.js +11 -111
  6. package/dist/core/config.js.map +1 -1
  7. package/dist/core/plugin-bridge-server.d.ts.map +1 -1
  8. package/dist/core/plugin-bridge-server.js +1 -2
  9. package/dist/core/plugin-bridge-server.js.map +1 -1
  10. package/dist/core/types/index.d.ts +2 -98
  11. package/dist/core/types/index.d.ts.map +1 -1
  12. package/dist/local-plugin-only.d.ts.map +1 -1
  13. package/dist/local-plugin-only.js +14 -13
  14. package/dist/local-plugin-only.js.map +1 -1
  15. package/f-mcp-plugin/README.md +8 -15
  16. package/f-mcp-plugin/manifest.json +1 -3
  17. package/package.json +8 -31
  18. package/dist/browser/base.d.ts +0 -50
  19. package/dist/browser/base.d.ts.map +0 -1
  20. package/dist/browser/base.js +0 -6
  21. package/dist/browser/base.js.map +0 -1
  22. package/dist/browser/local.d.ts +0 -81
  23. package/dist/browser/local.d.ts.map +0 -1
  24. package/dist/browser/local.js +0 -283
  25. package/dist/browser/local.js.map +0 -1
  26. package/dist/core/console-monitor.d.ts +0 -82
  27. package/dist/core/console-monitor.d.ts.map +0 -1
  28. package/dist/core/console-monitor.js +0 -428
  29. package/dist/core/console-monitor.js.map +0 -1
  30. package/dist/core/design-system-manifest.d.ts +0 -272
  31. package/dist/core/design-system-manifest.d.ts.map +0 -1
  32. package/dist/core/design-system-manifest.js +0 -261
  33. package/dist/core/design-system-manifest.js.map +0 -1
  34. package/dist/core/enrichment/enrichment-service.d.ts +0 -52
  35. package/dist/core/enrichment/enrichment-service.d.ts.map +0 -1
  36. package/dist/core/enrichment/enrichment-service.js +0 -272
  37. package/dist/core/enrichment/enrichment-service.js.map +0 -1
  38. package/dist/core/enrichment/index.d.ts +0 -8
  39. package/dist/core/enrichment/index.d.ts.map +0 -1
  40. package/dist/core/enrichment/index.js +0 -8
  41. package/dist/core/enrichment/index.js.map +0 -1
  42. package/dist/core/enrichment/relationship-mapper.d.ts +0 -106
  43. package/dist/core/enrichment/relationship-mapper.d.ts.map +0 -1
  44. package/dist/core/enrichment/relationship-mapper.js +0 -352
  45. package/dist/core/enrichment/relationship-mapper.js.map +0 -1
  46. package/dist/core/enrichment/style-resolver.d.ts +0 -80
  47. package/dist/core/enrichment/style-resolver.d.ts.map +0 -1
  48. package/dist/core/enrichment/style-resolver.js +0 -327
  49. package/dist/core/enrichment/style-resolver.js.map +0 -1
  50. package/dist/core/figma-api.d.ts +0 -137
  51. package/dist/core/figma-api.d.ts.map +0 -1
  52. package/dist/core/figma-api.js +0 -274
  53. package/dist/core/figma-api.js.map +0 -1
  54. package/dist/core/figma-desktop-connector.d.ts +0 -242
  55. package/dist/core/figma-desktop-connector.d.ts.map +0 -1
  56. package/dist/core/figma-desktop-connector.js +0 -1042
  57. package/dist/core/figma-desktop-connector.js.map +0 -1
  58. package/dist/core/figma-reconstruction-spec.d.ts +0 -162
  59. package/dist/core/figma-reconstruction-spec.d.ts.map +0 -1
  60. package/dist/core/figma-reconstruction-spec.js +0 -387
  61. package/dist/core/figma-reconstruction-spec.js.map +0 -1
  62. package/dist/core/figma-tools.d.ts +0 -21
  63. package/dist/core/figma-tools.d.ts.map +0 -1
  64. package/dist/core/figma-tools.js +0 -2920
  65. package/dist/core/figma-tools.js.map +0 -1
  66. package/dist/core/snippet-injector.d.ts +0 -24
  67. package/dist/core/snippet-injector.d.ts.map +0 -1
  68. package/dist/core/snippet-injector.js +0 -97
  69. package/dist/core/snippet-injector.js.map +0 -1
  70. package/dist/core/types/enriched.d.ts +0 -213
  71. package/dist/core/types/enriched.d.ts.map +0 -1
  72. package/dist/core/types/enriched.js +0 -6
  73. package/dist/core/types/enriched.js.map +0 -1
  74. package/dist/local.d.ts +0 -73
  75. package/dist/local.d.ts.map +0 -1
  76. package/dist/local.js +0 -2605
  77. package/dist/local.js.map +0 -1
@@ -1,2920 +0,0 @@
1
- /**
2
- * Figma API MCP Tools
3
- * MCP tool definitions for Figma REST API data extraction
4
- */
5
- import { z } from "zod";
6
- import { extractFileKey, formatVariables, formatComponentData } from "./figma-api.js";
7
- import { createChildLogger } from "./logger.js";
8
- import { EnrichmentService } from "./enrichment/index.js";
9
- import { SnippetInjector } from "./snippet-injector.js";
10
- import { extractNodeSpec, validateReconstructionSpec, listVariants } from "./figma-reconstruction-spec.js";
11
- const logger = createChildLogger({ component: "figma-tools" });
12
- // Initialize enrichment service
13
- const enrichmentService = new EnrichmentService(logger);
14
- // Initialize snippet injector
15
- const snippetInjector = new SnippetInjector();
16
- // ============================================================================
17
- // Cache Management & Data Processing Helpers
18
- // ============================================================================
19
- /**
20
- * Cache configuration
21
- */
22
- const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
23
- const MAX_CACHE_ENTRIES = 10; // LRU eviction
24
- /**
25
- * Check if cache entry is still valid based on TTL
26
- */
27
- function isCacheValid(timestamp, ttlMs = CACHE_TTL_MS) {
28
- return Date.now() - timestamp < ttlMs;
29
- }
30
- /**
31
- * Rough token estimation for response size checking
32
- * Approximation: 1 token ≈ 4 characters for JSON
33
- */
34
- function estimateTokens(data) {
35
- const jsonString = JSON.stringify(data);
36
- return Math.ceil(jsonString.length / 4);
37
- }
38
- /**
39
- * Response size thresholds for adaptive verbosity
40
- * Based on typical Claude Desktop context window limits
41
- */
42
- const RESPONSE_SIZE_THRESHOLDS = {
43
- // Conservative thresholds to leave room for conversation context
44
- IDEAL_SIZE_KB: 100, // Target size for optimal performance
45
- WARNING_SIZE_KB: 200, // Start considering compression
46
- CRITICAL_SIZE_KB: 500, // Must compress to avoid context exhaustion
47
- MAX_SIZE_KB: 1000, // Absolute maximum before emergency compression
48
- };
49
- /**
50
- * Calculate JSON string size in KB
51
- */
52
- function calculateSizeKB(data) {
53
- const jsonString = JSON.stringify(data);
54
- return jsonString.length / 1024;
55
- }
56
- /**
57
- * Generic adaptive response wrapper - automatically compresses responses that exceed size thresholds
58
- * Can be used by any tool to prevent context window exhaustion
59
- *
60
- * @param responseData - The response data to potentially compress
61
- * @param options - Configuration options for compression behavior
62
- * @returns Response content array with optional AI instruction
63
- */
64
- function adaptiveResponse(responseData, options) {
65
- const sizeKB = calculateSizeKB(responseData);
66
- // No compression needed
67
- if (sizeKB <= RESPONSE_SIZE_THRESHOLDS.IDEAL_SIZE_KB) {
68
- return {
69
- content: [
70
- {
71
- type: "text",
72
- text: JSON.stringify(responseData),
73
- },
74
- ],
75
- };
76
- }
77
- // Determine compression level and message
78
- let compressionLevel = "info";
79
- let aiInstruction = "";
80
- let shouldCompress = false;
81
- if (sizeKB > RESPONSE_SIZE_THRESHOLDS.MAX_SIZE_KB) {
82
- compressionLevel = "emergency";
83
- shouldCompress = true;
84
- aiInstruction =
85
- `⚠️ RESPONSE AUTO-COMPRESSED: The ${options.toolName} response was automatically reduced because the full response would be ${sizeKB.toFixed(0)}KB, which would exhaust Claude Desktop's context window.\n\n`;
86
- }
87
- else if (sizeKB > RESPONSE_SIZE_THRESHOLDS.CRITICAL_SIZE_KB) {
88
- compressionLevel = "critical";
89
- shouldCompress = true;
90
- aiInstruction =
91
- `⚠️ RESPONSE AUTO-COMPRESSED: The ${options.toolName} response was automatically reduced because it would be ${sizeKB.toFixed(0)}KB, risking context window exhaustion.\n\n`;
92
- }
93
- else if (sizeKB > RESPONSE_SIZE_THRESHOLDS.WARNING_SIZE_KB) {
94
- compressionLevel = "warning";
95
- shouldCompress = true;
96
- aiInstruction =
97
- `ℹ️ RESPONSE OPTIMIZED: The ${options.toolName} response was automatically reduced because it would be ${sizeKB.toFixed(0)}KB.\n\n`;
98
- }
99
- // Map compression level to verbosity level
100
- const verbosityMap = {
101
- "info": "standard",
102
- "warning": "summary",
103
- "critical": "summary",
104
- "emergency": "inventory"
105
- };
106
- // If compression needed, apply callback to reduce data
107
- let finalData = responseData;
108
- if (shouldCompress && options.compressionCallback) {
109
- const targetVerbosity = verbosityMap[compressionLevel] || "summary";
110
- finalData = options.compressionCallback(targetVerbosity);
111
- // Add compression metadata
112
- finalData.compression = {
113
- originalSizeKB: Math.round(sizeKB),
114
- finalSizeKB: Math.round(calculateSizeKB(finalData)),
115
- compressionLevel,
116
- };
117
- logger.info({
118
- tool: options.toolName,
119
- originalSizeKB: sizeKB.toFixed(2),
120
- finalSizeKB: calculateSizeKB(finalData).toFixed(2),
121
- compressionLevel,
122
- }, "Response compressed to prevent context exhaustion");
123
- }
124
- // Build AI instruction with suggested actions
125
- if (shouldCompress) {
126
- if (options.suggestedActions && options.suggestedActions.length > 0) {
127
- aiInstruction += `To get more detail:\n`;
128
- options.suggestedActions.forEach(action => {
129
- aiInstruction += `• ${action}\n`;
130
- });
131
- }
132
- }
133
- // Build response content
134
- const content = [
135
- {
136
- type: "text",
137
- text: JSON.stringify(finalData),
138
- },
139
- ];
140
- // Add AI instruction as separate content block if needed
141
- if (aiInstruction) {
142
- content.unshift({
143
- type: "text",
144
- text: aiInstruction.trim(),
145
- });
146
- }
147
- return { content };
148
- }
149
- /**
150
- * Adaptive verbosity system - automatically downgrades verbosity based on response size
151
- * Returns adjusted verbosity level and compression info for AI instructions
152
- *
153
- * @deprecated Use adaptiveResponse instead for more flexible compression
154
- */
155
- function adaptiveVerbosity(data, requestedVerbosity) {
156
- const sizeKB = calculateSizeKB(data);
157
- // No adjustment needed - response is within ideal size
158
- if (sizeKB <= RESPONSE_SIZE_THRESHOLDS.IDEAL_SIZE_KB) {
159
- return {
160
- adjustedVerbosity: requestedVerbosity,
161
- sizeKB,
162
- wasCompressed: false,
163
- };
164
- }
165
- // Determine appropriate verbosity based on size
166
- let adjustedVerbosity = requestedVerbosity;
167
- let compressionReason = "";
168
- let aiInstruction = "";
169
- if (sizeKB > RESPONSE_SIZE_THRESHOLDS.MAX_SIZE_KB) {
170
- // Emergency: Force inventory mode
171
- adjustedVerbosity = "inventory";
172
- compressionReason = `Response size (${sizeKB.toFixed(0)}KB) exceeds maximum threshold (${RESPONSE_SIZE_THRESHOLDS.MAX_SIZE_KB}KB)`;
173
- aiInstruction =
174
- `⚠️ RESPONSE AUTO-COMPRESSED: The response was automatically reduced to 'inventory' verbosity (names/IDs only) because the full response would be ${sizeKB.toFixed(0)}KB, which would exhaust Claude Desktop's context window.\n\n` +
175
- `To get more detail:\n` +
176
- `• Use format='filtered' with collection/namePattern/mode filters to narrow the scope\n` +
177
- `• Use pagination (page=1, pageSize=20) to retrieve data in smaller chunks\n` +
178
- `• Use returnAsLinks=true to get resource_link references instead of full data\n\n` +
179
- `Current response contains variable/collection names and IDs only.`;
180
- }
181
- else if (sizeKB > RESPONSE_SIZE_THRESHOLDS.CRITICAL_SIZE_KB) {
182
- // Critical: Downgrade to summary if higher was requested
183
- if (requestedVerbosity === "full" || requestedVerbosity === "standard") {
184
- adjustedVerbosity = "summary";
185
- compressionReason = `Response size (${sizeKB.toFixed(0)}KB) exceeds critical threshold (${RESPONSE_SIZE_THRESHOLDS.CRITICAL_SIZE_KB}KB)`;
186
- aiInstruction =
187
- `⚠️ RESPONSE AUTO-COMPRESSED: The response was automatically reduced to 'summary' verbosity because the ${requestedVerbosity} response would be ${sizeKB.toFixed(0)}KB, risking context window exhaustion.\n\n` +
188
- `To get more detail, use filtering options:\n` +
189
- `• format='filtered' with collection='CollectionName' to focus on specific collections\n` +
190
- `• namePattern='color' to filter by variable name\n` +
191
- `• mode='Light' to filter by mode\n` +
192
- `• pagination with smaller pageSize values\n\n` +
193
- `Current response includes variable names, types, and mode information.`;
194
- }
195
- }
196
- else if (sizeKB > RESPONSE_SIZE_THRESHOLDS.WARNING_SIZE_KB) {
197
- // Warning: Downgrade full to standard
198
- if (requestedVerbosity === "full") {
199
- adjustedVerbosity = "standard";
200
- compressionReason = `Response size (${sizeKB.toFixed(0)}KB) exceeds warning threshold (${RESPONSE_SIZE_THRESHOLDS.WARNING_SIZE_KB}KB)`;
201
- aiInstruction =
202
- `ℹ️ RESPONSE OPTIMIZED: The response was automatically reduced to 'standard' verbosity because the full response would be ${sizeKB.toFixed(0)}KB.\n\n` +
203
- `This response includes essential variable properties. For specific details, use filtering:\n` +
204
- `• format='filtered' with collection/namePattern/mode filters\n` +
205
- `• Request verbosity='full' with specific filters to get complete data for a subset`;
206
- }
207
- }
208
- const wasCompressed = adjustedVerbosity !== requestedVerbosity;
209
- if (wasCompressed) {
210
- logger.info({
211
- originalVerbosity: requestedVerbosity,
212
- adjustedVerbosity,
213
- sizeKB: sizeKB.toFixed(2),
214
- threshold: compressionReason,
215
- }, "Adaptive compression applied");
216
- }
217
- return {
218
- adjustedVerbosity,
219
- sizeKB,
220
- wasCompressed,
221
- compressionReason: wasCompressed ? compressionReason : undefined,
222
- aiInstruction: wasCompressed ? aiInstruction : undefined,
223
- };
224
- }
225
- /**
226
- * Generate compact summary of variables data (~2K tokens)
227
- * Returns high-level overview with counts and names
228
- */
229
- function generateSummary(data) {
230
- const summary = {
231
- fileKey: data.fileKey,
232
- timestamp: data.timestamp,
233
- source: data.source || 'cache',
234
- overview: {
235
- total_variables: data.variables?.length || 0,
236
- total_collections: data.variableCollections?.length || 0,
237
- },
238
- collections: data.variableCollections?.map((c) => ({
239
- id: c.id,
240
- name: c.name,
241
- modes: c.modes?.map((m) => ({ id: m.modeId, name: m.name })),
242
- variable_count: c.variableIds?.length || 0,
243
- })) || [],
244
- variables_by_type: {},
245
- variable_names: [],
246
- };
247
- // Count variables by type
248
- const typeCount = {};
249
- const names = [];
250
- data.variables?.forEach((v) => {
251
- typeCount[v.resolvedType] = (typeCount[v.resolvedType] || 0) + 1;
252
- names.push(v.name);
253
- });
254
- summary.variables_by_type = typeCount;
255
- summary.variable_names = names;
256
- return summary;
257
- }
258
- /**
259
- * Apply filters to variables data
260
- */
261
- function applyFilters(data, filters, verbosity = "standard") {
262
- let filteredVariables = [...(data.variables || [])];
263
- let filteredCollections = [...(data.variableCollections || [])];
264
- // Filter by collection name or ID
265
- if (filters.collection) {
266
- const collectionFilter = filters.collection.toLowerCase();
267
- filteredCollections = filteredCollections.filter((c) => c.name?.toLowerCase().includes(collectionFilter) ||
268
- c.id === filters.collection);
269
- const collectionIds = new Set(filteredCollections.map((c) => c.id));
270
- filteredVariables = filteredVariables.filter((v) => collectionIds.has(v.variableCollectionId));
271
- }
272
- // Filter by variable name pattern (regex or substring)
273
- if (filters.namePattern) {
274
- try {
275
- const regex = new RegExp(filters.namePattern, 'i');
276
- filteredVariables = filteredVariables.filter((v) => regex.test(v.name));
277
- }
278
- catch (e) {
279
- // If regex fails, fall back to substring match
280
- const pattern = filters.namePattern.toLowerCase();
281
- filteredVariables = filteredVariables.filter((v) => v.name?.toLowerCase().includes(pattern));
282
- }
283
- }
284
- // Find target mode ID if mode filter specified (needed for both filtering and transformation)
285
- let targetModeId = null;
286
- let targetModeName = null;
287
- if (filters.mode) {
288
- const modeFilter = filters.mode.toLowerCase();
289
- // Try direct mode ID match first
290
- if (data.variableCollections || filteredCollections.length > 0) {
291
- for (const collection of filteredCollections) {
292
- if (collection.modes) {
293
- const mode = collection.modes.find((m) => m.modeId === filters.mode ||
294
- m.name?.toLowerCase().includes(modeFilter));
295
- if (mode) {
296
- targetModeId = mode.modeId;
297
- targetModeName = mode.name;
298
- break;
299
- }
300
- }
301
- }
302
- }
303
- }
304
- // Filter by mode name or ID
305
- if (filters.mode) {
306
- filteredVariables = filteredVariables.filter((v) => {
307
- // Check if variable has values for the specified mode
308
- if (v.valuesByMode) {
309
- // Try to match by mode ID directly
310
- if (v.valuesByMode[filters.mode]) {
311
- return true;
312
- }
313
- // Try using resolved targetModeId
314
- if (targetModeId && v.valuesByMode[targetModeId]) {
315
- return true;
316
- }
317
- // Try to match by mode name through collections
318
- const collection = filteredCollections.find((c) => c.id === v.variableCollectionId);
319
- if (collection?.modes) {
320
- const mode = collection.modes.find((m) => m.name?.toLowerCase().includes(filters.mode.toLowerCase()) || m.modeId === filters.mode);
321
- return mode && v.valuesByMode[mode.modeId];
322
- }
323
- }
324
- return false;
325
- });
326
- }
327
- // Transform valuesByMode based on verbosity level
328
- // This is critical for reducing response size with multi-mode variables
329
- if (verbosity !== "full") {
330
- filteredVariables = filteredVariables.map((v) => {
331
- const variable = { ...v };
332
- // Use original collections array for lookup, not filtered, since we need mode metadata
333
- // Handle both variableCollections and collections property names
334
- const collections = data.variableCollections || data.collections || [];
335
- const collection = collections.find((c) => c.id === v.variableCollectionId);
336
- if (verbosity === "inventory") {
337
- // Inventory: Remove valuesByMode entirely, add mode count
338
- delete variable.valuesByMode;
339
- if (collection?.modes) {
340
- variable.modeCount = collection.modes.length;
341
- }
342
- }
343
- else if (verbosity === "summary") {
344
- // Summary: Replace valuesByMode with mode names array
345
- if (variable.valuesByMode && collection?.modes) {
346
- variable.modeNames = collection.modes.map((m) => m.name);
347
- variable.modeCount = collection.modes.length;
348
- }
349
- delete variable.valuesByMode;
350
- }
351
- else if (verbosity === "standard") {
352
- // Standard: If mode parameter specified, filter to that mode only
353
- if (targetModeId && variable.valuesByMode) {
354
- const singleModeValue = variable.valuesByMode[targetModeId];
355
- variable.valuesByMode = { [targetModeId]: singleModeValue };
356
- variable.selectedMode = {
357
- modeId: targetModeId,
358
- modeName: targetModeName,
359
- };
360
- }
361
- // If no mode specified, keep all valuesByMode but add metadata for context
362
- else if (variable.valuesByMode && collection?.modes) {
363
- variable.modeMetadata = collection.modes.map((m) => ({
364
- modeId: m.modeId,
365
- modeName: m.name,
366
- }));
367
- }
368
- }
369
- return variable;
370
- });
371
- // Apply field-level filtering based on verbosity
372
- if (verbosity === "inventory") {
373
- filteredVariables = filteredVariables.map((v) => ({
374
- id: v.id,
375
- name: v.name,
376
- resolvedType: v.resolvedType,
377
- variableCollectionId: v.variableCollectionId,
378
- ...(v.modeCount && { modeCount: v.modeCount }),
379
- }));
380
- }
381
- else if (verbosity === "summary") {
382
- filteredVariables = filteredVariables.map((v) => ({
383
- id: v.id,
384
- name: v.name,
385
- resolvedType: v.resolvedType,
386
- variableCollectionId: v.variableCollectionId,
387
- ...(v.modeNames && { modeNames: v.modeNames }),
388
- ...(v.modeCount && { modeCount: v.modeCount }),
389
- }));
390
- }
391
- else if (verbosity === "standard") {
392
- filteredVariables = filteredVariables.map((v) => ({
393
- id: v.id,
394
- name: v.name,
395
- resolvedType: v.resolvedType,
396
- valuesByMode: v.valuesByMode,
397
- description: v.description,
398
- variableCollectionId: v.variableCollectionId,
399
- ...(v.scopes && { scopes: v.scopes }),
400
- ...(v.selectedMode && { selectedMode: v.selectedMode }),
401
- ...(v.modeMetadata && { modeMetadata: v.modeMetadata }),
402
- }));
403
- }
404
- // For "full" verbosity, return all fields (no filtering)
405
- }
406
- // IMPORTANT: Only return filtered data, not the entire original data object
407
- // The ...data spread was including massive metadata that bloated responses
408
- return {
409
- variables: filteredVariables,
410
- variableCollections: filteredCollections,
411
- };
412
- }
413
- /**
414
- * Apply pagination to variables
415
- */
416
- function paginateVariables(data, page = 1, pageSize = 50) {
417
- const variables = data.variables || [];
418
- const totalVariables = variables.length;
419
- const totalPages = Math.ceil(totalVariables / pageSize);
420
- // Validate page number
421
- const currentPage = Math.max(1, Math.min(page, totalPages || 1));
422
- // Calculate pagination
423
- const startIndex = (currentPage - 1) * pageSize;
424
- const endIndex = startIndex + pageSize;
425
- const paginatedVariables = variables.slice(startIndex, endIndex);
426
- return {
427
- data: {
428
- ...data,
429
- variables: paginatedVariables,
430
- },
431
- pagination: {
432
- currentPage,
433
- pageSize,
434
- totalVariables,
435
- totalPages,
436
- hasNextPage: currentPage < totalPages,
437
- hasPrevPage: currentPage > 1,
438
- },
439
- };
440
- }
441
- /**
442
- * Manage LRU cache eviction
443
- */
444
- function evictOldestCacheEntry(cache) {
445
- if (cache.size >= MAX_CACHE_ENTRIES) {
446
- // Find oldest entry
447
- let oldestKey = null;
448
- let oldestTime = Infinity;
449
- for (const [key, entry] of cache.entries()) {
450
- if (entry.timestamp < oldestTime) {
451
- oldestTime = entry.timestamp;
452
- oldestKey = key;
453
- }
454
- }
455
- if (oldestKey) {
456
- cache.delete(oldestKey);
457
- logger.info({ evictedKey: oldestKey }, 'Evicted oldest cache entry (LRU)');
458
- }
459
- }
460
- }
461
- /**
462
- * Resolve variable aliases to their final values for all modes
463
- * @param variables Array of variables to resolve
464
- * @param allVariablesMap Map of all variables by ID for lookup
465
- * @param collectionsMap Map of collections by ID for mode info
466
- * @returns Variables with added resolvedValuesByMode field
467
- */
468
- function resolveVariableAliases(variables, allVariablesMap, collectionsMap) {
469
- // Helper to format color value to hex
470
- const formatColorToHex = (color) => {
471
- if (typeof color === 'string')
472
- return color;
473
- if (color && typeof color.r === 'number' && typeof color.g === 'number' && typeof color.b === 'number') {
474
- const r = Math.round(color.r * 255);
475
- const g = Math.round(color.g * 255);
476
- const b = Math.round(color.b * 255);
477
- const a = typeof color.a === 'number' ? color.a : 1;
478
- if (a < 1) {
479
- const aHex = Math.round(a * 255).toString(16).padStart(2, '0');
480
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}${aHex}`.toUpperCase();
481
- }
482
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
483
- }
484
- return null;
485
- };
486
- // Helper to get mode ID from a mode object (handles both 'modeId' and 'id' properties)
487
- const getModeId = (mode) => {
488
- return mode?.modeId || mode?.id || null;
489
- };
490
- // Helper to get default mode ID from a collection
491
- const getDefaultModeId = (collection, variable) => {
492
- // Try explicit defaultModeId first
493
- if (collection?.defaultModeId) {
494
- return collection.defaultModeId;
495
- }
496
- // Try first mode's ID
497
- if (collection?.modes?.length > 0) {
498
- return getModeId(collection.modes[0]);
499
- }
500
- // Fallback to first key in valuesByMode
501
- const modeKeys = Object.keys(variable?.valuesByMode || {});
502
- return modeKeys.length > 0 ? modeKeys[0] : null;
503
- };
504
- // Helper to resolve a single value, following alias chains
505
- const resolveValue = (value, resolvedType, visited = new Set(), depth = 0) => {
506
- if (depth > 10) {
507
- logger.warn({ depth }, 'Max alias resolution depth reached');
508
- return { resolved: null, aliasChain: Array.from(visited) };
509
- }
510
- // Check if this is an alias
511
- if (value && typeof value === 'object' && value.type === 'VARIABLE_ALIAS') {
512
- const targetId = value.id;
513
- // Prevent circular references
514
- if (visited.has(targetId)) {
515
- logger.warn({ targetId, visited: Array.from(visited) }, 'Circular alias reference detected');
516
- return { resolved: null, aliasChain: Array.from(visited) };
517
- }
518
- visited.add(targetId);
519
- const targetVar = allVariablesMap.get(targetId);
520
- if (!targetVar) {
521
- logger.debug({ targetId }, 'Target variable not found in map');
522
- return { resolved: null, aliasChain: Array.from(visited) };
523
- }
524
- // Get the target's collection to find its default mode
525
- const targetCollection = collectionsMap.get(targetVar.variableCollectionId);
526
- const targetModeId = getDefaultModeId(targetCollection, targetVar);
527
- if (!targetModeId) {
528
- logger.debug({ targetId, collectionId: targetVar.variableCollectionId }, 'Could not determine target mode ID');
529
- return { resolved: null, aliasChain: Array.from(visited) };
530
- }
531
- const targetValue = targetVar.valuesByMode?.[targetModeId];
532
- if (targetValue === undefined) {
533
- logger.debug({ targetId, targetModeId, availableModes: Object.keys(targetVar.valuesByMode || {}) }, 'Target value not found for mode');
534
- return { resolved: null, aliasChain: Array.from(visited) };
535
- }
536
- // Recursively resolve
537
- const result = resolveValue(targetValue, targetVar.resolvedType, visited, depth + 1);
538
- return {
539
- resolved: result.resolved,
540
- aliasChain: [targetVar.name, ...(result.aliasChain || [])]
541
- };
542
- }
543
- // Not an alias - format the value based on type
544
- if (resolvedType === 'COLOR') {
545
- return { resolved: formatColorToHex(value) };
546
- }
547
- return { resolved: value };
548
- };
549
- // Process each variable
550
- return variables.map(variable => {
551
- const collection = collectionsMap.get(variable.variableCollectionId);
552
- const modes = collection?.modes || [];
553
- const resolvedValuesByMode = {};
554
- for (const mode of modes) {
555
- const modeId = getModeId(mode);
556
- if (!modeId)
557
- continue;
558
- const rawValue = variable.valuesByMode?.[modeId];
559
- if (rawValue === undefined)
560
- continue;
561
- const { resolved, aliasChain } = resolveValue(rawValue, variable.resolvedType, new Set());
562
- const modeName = mode.name || modeId;
563
- resolvedValuesByMode[modeName] = {
564
- value: resolved,
565
- ...(aliasChain && aliasChain.length > 0 && { aliasTo: aliasChain[0] })
566
- };
567
- }
568
- return {
569
- ...variable,
570
- resolvedValuesByMode
571
- };
572
- });
573
- }
574
- /**
575
- * Register Figma API tools with the MCP server
576
- */
577
- export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getConsoleMonitor, getBrowserManager, ensureInitialized, variablesCache, getDesktopConnector) {
578
- // Tool 8: Get File Data (General Purpose)
579
- // NOTE: For specific use cases, consider using specialized tools:
580
- // - figma_get_component_for_development: For UI component implementation
581
- // - figma_get_file_for_plugin: For plugin development
582
- server.registerTool("figma_get_file_data", {
583
- description: "Get full file structure and document tree. WARNING: Can consume large amounts of tokens. NOT recommended for component descriptions (use figma_get_component instead). Best for understanding file structure or finding component nodeIds. Start with verbosity='summary' and depth=1 for initial exploration.",
584
- inputSchema: {
585
- fileUrl: z
586
- .string()
587
- .url()
588
- .optional()
589
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
590
- depth: z
591
- .number()
592
- .min(0)
593
- .max(3)
594
- .optional()
595
- .default(1)
596
- .describe("How many levels of children to include (default: 1, max: 3). Start with 1 to prevent context exhaustion. Use 0 for full tree only when absolutely necessary."),
597
- verbosity: z
598
- .enum(["summary", "standard", "full"])
599
- .optional()
600
- .default("summary")
601
- .describe("Controls payload size: 'summary' (IDs/names/types only, ~90% smaller - RECOMMENDED), 'standard' (essential properties, ~50% smaller), 'full' (everything). Default: summary for token efficiency."),
602
- nodeIds: z
603
- .array(z.string())
604
- .optional()
605
- .describe("Specific node IDs to retrieve (optional)"),
606
- enrich: z
607
- .boolean()
608
- .optional()
609
- .describe("Set to true when user asks for: file statistics, health metrics, design system audit, or quality analysis. Adds statistics, health scores, and audit summaries. Default: false"),
610
- },
611
- annotations: { readOnlyHint: true },
612
- }, async ({ fileUrl, depth, nodeIds, enrich, verbosity }) => {
613
- try {
614
- // Initialize API client (required for file data - no F-MCP ATezer Bridge alternative)
615
- let api;
616
- try {
617
- api = await getFigmaAPI();
618
- }
619
- catch (apiError) {
620
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
621
- throw new Error(`Cannot retrieve file data. REST API authentication required.\n` +
622
- `Error: ${errorMessage}\n\n` +
623
- `To fix:\n` +
624
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable\n` +
625
- `2. Cloud mode: Authenticate via OAuth\n\n` +
626
- `Note: figma_get_file_data requires REST API access. ` +
627
- `For component-specific data, use figma_get_component which has F-MCP ATezer Bridge fallback.`);
628
- }
629
- // Use provided URL or current URL from browser
630
- const url = fileUrl || getCurrentUrl();
631
- if (!url) {
632
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
633
- }
634
- const fileKey = extractFileKey(url);
635
- if (!fileKey) {
636
- throw new Error(`Invalid Figma URL: ${url}`);
637
- }
638
- logger.info({ fileKey, depth, nodeIds, enrich, verbosity }, "Fetching file data");
639
- const fileData = await api.getFile(fileKey, {
640
- depth,
641
- ids: nodeIds,
642
- });
643
- // Apply verbosity filtering to reduce payload size
644
- const filterNode = (node, level) => {
645
- if (!node)
646
- return node;
647
- if (level === "summary") {
648
- // Summary: Only IDs, names, types (~90% reduction)
649
- return {
650
- id: node.id,
651
- name: node.name,
652
- type: node.type,
653
- ...(node.children && {
654
- children: node.children.map((child) => filterNode(child, level))
655
- }),
656
- };
657
- }
658
- if (level === "standard") {
659
- // Standard: Essential properties for plugin development (~50% reduction)
660
- const filtered = {
661
- id: node.id,
662
- name: node.name,
663
- type: node.type,
664
- visible: node.visible,
665
- locked: node.locked,
666
- };
667
- // Include bounds for layout calculations
668
- if (node.absoluteBoundingBox)
669
- filtered.absoluteBoundingBox = node.absoluteBoundingBox;
670
- if (node.size)
671
- filtered.size = node.size;
672
- // Include component/instance info for plugin work
673
- if (node.componentId)
674
- filtered.componentId = node.componentId;
675
- if (node.componentPropertyReferences)
676
- filtered.componentPropertyReferences = node.componentPropertyReferences;
677
- // Include basic styling (but not full details)
678
- if (node.fills && node.fills.length > 0) {
679
- filtered.fills = node.fills.map((fill) => ({
680
- type: fill.type,
681
- visible: fill.visible,
682
- ...(fill.color && { color: fill.color }),
683
- }));
684
- }
685
- // Include plugin data if present
686
- if (node.pluginData)
687
- filtered.pluginData = node.pluginData;
688
- if (node.sharedPluginData)
689
- filtered.sharedPluginData = node.sharedPluginData;
690
- // Recursively filter children
691
- if (node.children) {
692
- filtered.children = node.children.map((child) => filterNode(child, level));
693
- }
694
- return filtered;
695
- }
696
- // Full: Return everything
697
- return node;
698
- };
699
- const filteredDocument = verbosity !== "full"
700
- ? filterNode(fileData.document, verbosity || "standard")
701
- : fileData.document;
702
- let response = {
703
- fileKey,
704
- name: fileData.name,
705
- lastModified: fileData.lastModified,
706
- version: fileData.version,
707
- document: filteredDocument,
708
- components: fileData.components
709
- ? Object.keys(fileData.components).length
710
- : 0,
711
- styles: fileData.styles
712
- ? Object.keys(fileData.styles).length
713
- : 0,
714
- verbosity: verbosity || "standard",
715
- ...(nodeIds && {
716
- requestedNodes: nodeIds,
717
- nodes: fileData.nodes,
718
- }),
719
- };
720
- // Apply enrichment if requested
721
- if (enrich) {
722
- const enrichmentOptions = {
723
- enrich: true,
724
- include_usage: true,
725
- };
726
- response = await enrichmentService.enrichFileData({ ...response, ...fileData }, enrichmentOptions);
727
- }
728
- const finalResponse = {
729
- ...response,
730
- enriched: enrich || false,
731
- };
732
- // Use adaptive response to prevent context exhaustion
733
- return adaptiveResponse(finalResponse, {
734
- toolName: "figma_get_file_data",
735
- compressionCallback: (adjustedLevel) => {
736
- // Re-apply node filtering with lower verbosity
737
- const level = adjustedLevel;
738
- const refiltered = {
739
- ...finalResponse,
740
- document: verbosity !== "full"
741
- ? filterNode(fileData.document, level)
742
- : fileData.document,
743
- verbosity: level,
744
- };
745
- return refiltered;
746
- },
747
- suggestedActions: [
748
- "Use verbosity='summary' with depth=1 for initial exploration",
749
- "Use verbosity='standard' for essential properties",
750
- "Request specific nodeIds to narrow the scope",
751
- "Reduce depth parameter (max 3, recommend 1-2)",
752
- ],
753
- });
754
- }
755
- catch (error) {
756
- logger.error({ error }, "Failed to get file data");
757
- const errorMessage = error instanceof Error ? error.message : String(error);
758
- return {
759
- content: [
760
- {
761
- type: "text",
762
- text: JSON.stringify({
763
- error: errorMessage,
764
- message: "Failed to retrieve Figma file data",
765
- hint: "Make sure FIGMA_ACCESS_TOKEN is configured and the file is accessible",
766
- }, null, 2),
767
- },
768
- ],
769
- isError: true,
770
- };
771
- }
772
- });
773
- /**
774
- * Tool 9: Get Variables (Design Tokens)
775
- *
776
- * WORKFLOW:
777
- * - Primary: Attempts to fetch variables via Figma REST API (requires Enterprise plan)
778
- * - Fallback: On 403 error, provides console-based extraction snippet
779
- *
780
- * TWO-CALL PATTERN (when API unavailable):
781
- * 1. First call: Returns snippet + instructions (useConsoleFallback: true, default)
782
- * 2. User runs snippet in Figma plugin console
783
- * 3. Second call: Parses captured data (parseFromConsole: true)
784
- *
785
- * IMPORTANT: Snippet requires Figma Plugin API context, not browser DevTools console.
786
- */
787
- server.registerTool("figma_get_variables", {
788
- description: "Extract design tokens and variables from a Figma file with code export support (CSS, Tailwind, TypeScript, Sass). Use when user asks for: design system tokens, variables, color/spacing values, theme data, or code exports. Handles multi-mode variables (Light/Dark themes). NOT for component metadata (use figma_get_component). Supports filtering by collection/mode/name and verbosity control to prevent token exhaustion. Enterprise plan required for Variables API; automatically falls back to Styles API or console-based extraction if unavailable.",
789
- inputSchema: {
790
- fileUrl: z
791
- .string()
792
- .url()
793
- .optional()
794
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
795
- includePublished: z
796
- .boolean()
797
- .optional()
798
- .default(true)
799
- .describe("Include published variables from libraries"),
800
- verbosity: z
801
- .enum(["inventory", "summary", "standard", "full"])
802
- .optional()
803
- .default("standard")
804
- .describe("Controls payload size: 'inventory' (names/IDs only, ~95% smaller, use with filtered), 'summary' (names/values only, ~80% smaller), 'standard' (essential properties, ~45% smaller), 'full' (everything). Default: standard"),
805
- enrich: z
806
- .boolean()
807
- .optional()
808
- .describe("Set to true when user asks for: CSS/Sass/Tailwind exports, code examples, design tokens, usage information, dependencies, or any export format. Adds resolved values, dependency graphs, and usage analysis. Default: false"),
809
- include_usage: z
810
- .boolean()
811
- .optional()
812
- .describe("Include usage in styles and components (requires enrich=true)"),
813
- include_dependencies: z
814
- .boolean()
815
- .optional()
816
- .describe("Include variable dependency graph (requires enrich=true)"),
817
- include_exports: z
818
- .boolean()
819
- .optional()
820
- .describe("Include export format examples (requires enrich=true)"),
821
- export_formats: z
822
- .array(z.enum(["css", "sass", "tailwind", "typescript", "json"]))
823
- .optional()
824
- .describe("Which code formats to generate examples for. Use when user mentions specific formats like 'CSS', 'Tailwind', 'SCSS', 'TypeScript', etc. Automatically enables enrichment."),
825
- format: z
826
- .enum(["summary", "filtered", "full"])
827
- .optional()
828
- .default("full")
829
- .describe("Response format: 'summary' (~2K tokens with overview and names only), 'filtered' (apply collection/name/mode filters), 'full' (complete dataset from cache or fetch). " +
830
- "Summary is recommended for initial exploration. Full format returns all data but may be auto-summarized if >25K tokens. Default: full"),
831
- collection: z
832
- .string()
833
- .optional()
834
- .describe("Filter variables by collection name or ID. Case-insensitive substring match. Only applies when format='filtered'. Example: 'Primitives' or 'VariableCollectionId:123'"),
835
- namePattern: z
836
- .string()
837
- .optional()
838
- .describe("Filter variables by name using regex pattern or substring. Case-insensitive. Only applies when format='filtered'. Example: 'color/brand' or '^typography'"),
839
- mode: z
840
- .string()
841
- .optional()
842
- .describe("Filter variables by mode name or ID. Only returns variables that have values for this mode. Only applies when format='filtered'. Example: 'Light' or 'Dark'"),
843
- returnAsLinks: z
844
- .boolean()
845
- .optional()
846
- .default(false)
847
- .describe("Return variables as resource_link references instead of full data. Drastically reduces payload size (100+ variables = ~20KB vs >1MB). Use with figma_get_variable_by_id to fetch specific variables. Recommended for large variable sets. Default: false"),
848
- refreshCache: z
849
- .boolean()
850
- .optional()
851
- .default(false)
852
- .describe("Force refresh cache by fetching fresh data from Figma. Use when data may have changed since last fetch. Default: false (use cached data if available and fresh)"),
853
- useConsoleFallback: z
854
- .boolean()
855
- .optional()
856
- .default(true)
857
- .describe("Enable automatic fallback to console-based extraction when REST API returns 403 (Figma Enterprise plan required). " +
858
- "When enabled, provides a JavaScript snippet that users run in Figma's plugin console. " +
859
- "This is STEP 1 of a two-call workflow. After receiving the snippet, instruct the user to run it, then call this tool again with parseFromConsole=true. " +
860
- "Default: true. Set to false only to disable the fallback entirely."),
861
- parseFromConsole: z
862
- .boolean()
863
- .optional()
864
- .default(false)
865
- .describe("Parse variables from console logs after user has executed the snippet. " +
866
- "This is STEP 2 of the two-call workflow. Set to true ONLY after: " +
867
- "(1) you received a console snippet from the first call, " +
868
- "(2) instructed the user to run it in Figma's PLUGIN console (Plugins → Development → Open Console or existing plugin), " +
869
- "(3) user confirmed they ran the snippet and saw '✅ Variables data captured!' message. " +
870
- "Default: false. Never set to true on the first call."),
871
- page: z
872
- .number()
873
- .int()
874
- .min(1)
875
- .optional()
876
- .default(1)
877
- .describe("Page number for paginated results (1-based). Use when response is too large (>1MB). Each page returns up to 50 variables."),
878
- pageSize: z
879
- .number()
880
- .int()
881
- .min(1)
882
- .max(100)
883
- .optional()
884
- .default(50)
885
- .describe("Number of variables per page (1-100). Default: 50. Smaller values reduce response size."),
886
- resolveAliases: z
887
- .boolean()
888
- .optional()
889
- .default(false)
890
- .describe("Automatically resolve variable aliases to their final values (hex colors, numbers, etc.). " +
891
- "When true, each variable will include a 'resolvedValuesByMode' field with the actual values " +
892
- "instead of just alias references. Useful for getting color hex values without manual resolution. " +
893
- "Default: false."),
894
- },
895
- annotations: { readOnlyHint: true },
896
- }, async ({ fileUrl, includePublished, verbosity, enrich, include_usage, include_dependencies, include_exports, export_formats, format, collection, namePattern, mode, returnAsLinks, refreshCache, useConsoleFallback, parseFromConsole, page, pageSize, resolveAliases }) => {
897
- // Extract fileKey outside try block so it's available in catch block
898
- const url = fileUrl || getCurrentUrl();
899
- if (!url) {
900
- return {
901
- content: [
902
- {
903
- type: "text",
904
- text: JSON.stringify({
905
- error: "No Figma file URL provided",
906
- message: "Either pass fileUrl parameter or call figma_navigate first."
907
- }, null, 2),
908
- },
909
- ],
910
- isError: true,
911
- };
912
- }
913
- const fileKey = extractFileKey(url);
914
- if (!fileKey) {
915
- return {
916
- content: [
917
- {
918
- type: "text",
919
- text: JSON.stringify({
920
- error: `Invalid Figma URL: ${url}`,
921
- message: "Could not extract file key from URL"
922
- }, null, 2),
923
- },
924
- ],
925
- isError: true,
926
- };
927
- }
928
- try {
929
- // =====================================================================
930
- // CACHE-FIRST LOGIC: Check if we have cached data before fetching
931
- // =====================================================================
932
- let cachedData = null;
933
- let shouldFetch = true;
934
- if (variablesCache && !parseFromConsole) {
935
- const cacheEntry = variablesCache.get(fileKey);
936
- if (cacheEntry) {
937
- const isValid = isCacheValid(cacheEntry.timestamp);
938
- if (isValid && !refreshCache) {
939
- // Cache hit! Use cached data
940
- cachedData = cacheEntry.data;
941
- shouldFetch = false;
942
- logger.info({
943
- fileKey,
944
- cacheAge: Date.now() - cacheEntry.timestamp,
945
- variableCount: cachedData.variables?.length,
946
- }, 'Using cached variables data');
947
- }
948
- else if (!isValid) {
949
- logger.info({ fileKey, cacheAge: Date.now() - cacheEntry.timestamp }, 'Cache expired, will refresh');
950
- }
951
- else if (refreshCache) {
952
- logger.info({ fileKey }, 'Refresh cache requested, will fetch fresh data');
953
- }
954
- }
955
- else {
956
- logger.info({ fileKey }, 'No cache entry found, will fetch data');
957
- }
958
- }
959
- // If we have cached data, skip fetching and jump to formatting
960
- if (cachedData && !shouldFetch) {
961
- // Apply format logic based on user request
962
- let responseData = cachedData;
963
- let paginationInfo = null;
964
- if (format === 'summary') {
965
- // Return compact summary
966
- responseData = generateSummary(cachedData);
967
- logger.info({ fileKey, estimatedTokens: estimateTokens(responseData) }, 'Generated summary from cache');
968
- }
969
- else if (format === 'filtered') {
970
- // Apply filters with verbosity-aware valuesByMode transformation
971
- responseData = applyFilters(cachedData, {
972
- collection,
973
- namePattern,
974
- mode,
975
- }, verbosity || 'standard');
976
- // ALWAYS apply pagination for filtered results to prevent 1MB limit
977
- // Default to page 1, pageSize 50 if not specified
978
- const paginated = paginateVariables(responseData, page || 1, pageSize || 50);
979
- responseData = paginated.data;
980
- paginationInfo = paginated.pagination;
981
- // Apply verbosity filtering to minimize payload size
982
- // For filtered results, default to "inventory" for maximum size reduction
983
- const effectiveVerbosity = verbosity || "inventory";
984
- // CRITICAL FIX: Only include collections referenced by paginated variables
985
- const referencedCollectionIds = new Set(responseData.variables.map((v) => v.variableCollectionId));
986
- responseData.variableCollections = responseData.variableCollections.filter((c) => referencedCollectionIds.has(c.id));
987
- // Filter variables to minimal needed fields
988
- responseData.variables = responseData.variables.map((v) => {
989
- if (effectiveVerbosity === "inventory") {
990
- // Ultra-minimal: just names and IDs for inventory purposes
991
- // If mode filter is specified, include only that mode's value
992
- const result = {
993
- id: v.id,
994
- name: v.name,
995
- collectionId: v.variableCollectionId,
996
- };
997
- // If mode filter specified, include just that single mode's value
998
- if (mode && v.valuesByMode) {
999
- // Find the mode ID from the collection
1000
- const collection = responseData.variableCollections.find((c) => c.id === v.variableCollectionId);
1001
- if (collection?.modes) {
1002
- const modeObj = collection.modes.find((m) => m.name?.toLowerCase().includes(mode.toLowerCase()) || m.modeId === mode);
1003
- if (modeObj && v.valuesByMode[modeObj.modeId]) {
1004
- result.value = v.valuesByMode[modeObj.modeId];
1005
- result.mode = modeObj.name;
1006
- }
1007
- }
1008
- }
1009
- return result;
1010
- }
1011
- if (effectiveVerbosity === "summary") {
1012
- return {
1013
- id: v.id,
1014
- name: v.name,
1015
- resolvedType: v.resolvedType,
1016
- valuesByMode: v.valuesByMode,
1017
- variableCollectionId: v.variableCollectionId,
1018
- // Include modeNames and modeCount added by applyFilters
1019
- ...(v.modeNames && { modeNames: v.modeNames }),
1020
- ...(v.modeCount && { modeCount: v.modeCount }),
1021
- };
1022
- }
1023
- if (effectiveVerbosity === "standard") {
1024
- return {
1025
- id: v.id,
1026
- name: v.name,
1027
- resolvedType: v.resolvedType,
1028
- valuesByMode: v.valuesByMode,
1029
- description: v.description,
1030
- variableCollectionId: v.variableCollectionId,
1031
- };
1032
- }
1033
- return v; // full
1034
- });
1035
- // Filter collections to remove massive variableIds arrays
1036
- responseData.variableCollections = responseData.variableCollections.map((c) => {
1037
- if (effectiveVerbosity === "inventory") {
1038
- // Ultra-minimal: just ID and name, mode names only (no full mode objects)
1039
- return {
1040
- id: c.id,
1041
- name: c.name,
1042
- modeNames: c.modes?.map((m) => m.name) || [],
1043
- };
1044
- }
1045
- if (effectiveVerbosity === "summary") {
1046
- return {
1047
- id: c.id,
1048
- name: c.name,
1049
- modes: c.modes, // Keep modes for user to understand mode structure
1050
- };
1051
- }
1052
- if (effectiveVerbosity === "standard") {
1053
- return {
1054
- id: c.id,
1055
- name: c.name,
1056
- modes: c.modes,
1057
- defaultModeId: c.defaultModeId,
1058
- };
1059
- }
1060
- // For full, remove variableIds array to reduce size
1061
- const { variableIds, ...rest } = c;
1062
- return rest;
1063
- });
1064
- logger.info({
1065
- fileKey,
1066
- originalCount: cachedData.variables?.length,
1067
- filteredCount: paginationInfo.totalVariables,
1068
- returnedCount: responseData.variables?.length,
1069
- page: paginationInfo.currentPage,
1070
- totalPages: paginationInfo.totalPages,
1071
- verbosity: effectiveVerbosity,
1072
- }, 'Applied filters, pagination, and verbosity filtering to cached data');
1073
- // Apply alias resolution if requested
1074
- if (resolveAliases && responseData.variables?.length > 0) {
1075
- // Build maps from ALL cached variables (not just filtered) for resolution
1076
- const allVariablesMap = new Map();
1077
- const collectionsMap = new Map();
1078
- for (const v of cachedData.variables || []) {
1079
- allVariablesMap.set(v.id, v);
1080
- }
1081
- for (const c of cachedData.variableCollections || []) {
1082
- collectionsMap.set(c.id, c);
1083
- }
1084
- responseData.variables = resolveVariableAliases(responseData.variables, allVariablesMap, collectionsMap);
1085
- logger.info({ fileKey, resolvedCount: responseData.variables.length }, 'Applied alias resolution to filtered variables');
1086
- }
1087
- }
1088
- else {
1089
- // format === 'full'
1090
- // Check if we need to auto-summarize
1091
- const estimatedTokens = estimateTokens(responseData);
1092
- if (estimatedTokens > 25000) {
1093
- logger.warn({ fileKey, estimatedTokens }, 'Full data exceeds MCP token limit (25K), auto-summarizing. Use format=summary or format=filtered to get specific data.');
1094
- const summary = generateSummary(responseData);
1095
- return {
1096
- content: [
1097
- {
1098
- type: "text",
1099
- text: JSON.stringify({
1100
- fileKey,
1101
- source: 'cache_auto_summarized',
1102
- warning: 'Full dataset exceeds MCP token limit (25,000 tokens)',
1103
- suggestion: 'Use format="summary" for overview or format="filtered" with collection/namePattern/mode filters to get specific variables',
1104
- estimatedTokens,
1105
- summary,
1106
- }, null, 2),
1107
- },
1108
- ],
1109
- };
1110
- }
1111
- }
1112
- // Apply alias resolution for 'full' format if not already applied (filtered format handles it above)
1113
- if (resolveAliases && format !== 'filtered' && responseData.variables?.length > 0) {
1114
- // Build maps from ALL cached variables for resolution
1115
- const allVariablesMap = new Map();
1116
- const collectionsMap = new Map();
1117
- for (const v of cachedData.variables || []) {
1118
- allVariablesMap.set(v.id, v);
1119
- }
1120
- for (const c of cachedData.variableCollections || []) {
1121
- collectionsMap.set(c.id, c);
1122
- }
1123
- responseData.variables = resolveVariableAliases(responseData.variables, allVariablesMap, collectionsMap);
1124
- logger.info({ fileKey, resolvedCount: responseData.variables.length, format }, 'Applied alias resolution to variables (full/summary format)');
1125
- }
1126
- // Return cached/processed data
1127
- // If returnAsLinks=true, return resource_link references instead of full data
1128
- if (returnAsLinks) {
1129
- const summary = {
1130
- fileKey,
1131
- source: 'cache',
1132
- totalVariables: responseData.variables?.length || 0,
1133
- totalCollections: responseData.variableCollections?.length || 0,
1134
- ...(paginationInfo && { pagination: paginationInfo }),
1135
- };
1136
- // Build resource_link content for each variable
1137
- const content = [
1138
- {
1139
- type: "text",
1140
- text: JSON.stringify(summary),
1141
- },
1142
- ];
1143
- // Add resource_link for each variable (minimal overhead ~150 bytes each)
1144
- responseData.variables?.forEach((v) => {
1145
- content.push({
1146
- type: "resource_link",
1147
- uri: `figma://variable/${v.id}`,
1148
- name: v.name || v.id,
1149
- description: `${v.resolvedType || 'VARIABLE'} from ${fileKey}`,
1150
- });
1151
- });
1152
- logger.info({
1153
- fileKey,
1154
- format: 'resource_links',
1155
- variableCount: responseData.variables?.length || 0,
1156
- linkCount: content.length - 1, // -1 for summary text
1157
- estimatedSizeKB: (content.length * 150) / 1024,
1158
- }, `Returning variables as resource_links`);
1159
- return { content };
1160
- }
1161
- // Default: return full data
1162
- const responsePayload = {
1163
- fileKey,
1164
- source: 'cache',
1165
- format: format || 'full',
1166
- timestamp: cachedData.timestamp,
1167
- data: responseData,
1168
- ...(paginationInfo && { pagination: paginationInfo }),
1169
- };
1170
- // Remove pretty printing to reduce payload size by 30-40%
1171
- const responseText = JSON.stringify(responsePayload);
1172
- const responseSizeBytes = Buffer.byteLength(responseText, 'utf8');
1173
- const responseSizeMB = (responseSizeBytes / (1024 * 1024)).toFixed(2);
1174
- logger.info({
1175
- fileKey,
1176
- format: format || 'full',
1177
- verbosity: verbosity || 'standard',
1178
- variableCount: responseData.variables?.length || 0,
1179
- collectionCount: responseData.variableCollections?.length || 0,
1180
- responseSizeBytes,
1181
- responseSizeMB: `${responseSizeMB} MB`,
1182
- isUnder1MB: responseSizeBytes < 1024 * 1024,
1183
- }, `Response size check: ${responseSizeMB} MB`);
1184
- return {
1185
- content: [
1186
- {
1187
- type: "text",
1188
- text: responseText,
1189
- },
1190
- ],
1191
- };
1192
- }
1193
- // =====================================================================
1194
- // FETCH LOGIC: No cache or cache invalid/refresh requested
1195
- // =====================================================================
1196
- // Check if REST API token is available (determines priority)
1197
- const hasToken = !!process.env.FIGMA_ACCESS_TOKEN;
1198
- let restApiSucceeded = false;
1199
- // PRIORITY LOGIC:
1200
- // 1. If token exists → Try REST API FIRST (enterprise users)
1201
- // 2. If no token OR REST API fails → Try F-MCP ATezer Bridge as fallback
1202
- logger.info({ hasToken }, "Authentication method detection");
1203
- // Try REST API first if token is available
1204
- if (hasToken && !parseFromConsole) {
1205
- try {
1206
- logger.info({ fileKey, includePublished, verbosity, enrich }, "Fetching variables via REST API (priority: token detected)");
1207
- const api = await getFigmaAPI();
1208
- const { local, published } = await api.getAllVariables(fileKey);
1209
- let localFormatted = formatVariables(local);
1210
- let publishedFormatted = includePublished
1211
- ? formatVariables(published)
1212
- : null;
1213
- // DEBUG: Check if valuesByMode exists before filtering
1214
- if (localFormatted.variables[0]) {
1215
- logger.info({
1216
- hasValuesByMode: !!localFormatted.variables[0].valuesByMode,
1217
- variableKeys: Object.keys(localFormatted.variables[0]),
1218
- collectionCount: localFormatted.collections?.length,
1219
- }, 'Variable structure before filtering');
1220
- }
1221
- // Apply collection/name/mode filtering if format is 'filtered'
1222
- if (format === 'filtered') {
1223
- // Create properly structured data for applyFilters
1224
- const dataToFilter = {
1225
- variables: localFormatted.variables,
1226
- variableCollections: localFormatted.collections,
1227
- };
1228
- const filteredLocal = applyFilters(dataToFilter, { collection, namePattern, mode }, verbosity || "standard");
1229
- localFormatted = {
1230
- summary: localFormatted.summary,
1231
- collections: filteredLocal.variableCollections,
1232
- variables: filteredLocal.variables,
1233
- };
1234
- // Also filter published if included
1235
- if (includePublished && publishedFormatted) {
1236
- const dataToFilterPublished = {
1237
- variables: publishedFormatted.variables,
1238
- variableCollections: publishedFormatted.collections,
1239
- };
1240
- const filteredPublished = applyFilters(dataToFilterPublished, { collection, namePattern, mode }, verbosity || "standard");
1241
- publishedFormatted = {
1242
- summary: publishedFormatted.summary,
1243
- collections: filteredPublished.variableCollections,
1244
- variables: filteredPublished.variables,
1245
- };
1246
- }
1247
- }
1248
- // Apply verbosity filtering after collection/name/mode filters
1249
- if (verbosity && verbosity !== 'full') {
1250
- const verbosityFiltered = applyFilters({
1251
- variables: localFormatted.variables,
1252
- variableCollections: localFormatted.collections,
1253
- }, {}, verbosity);
1254
- localFormatted = {
1255
- ...localFormatted,
1256
- collections: verbosityFiltered.variableCollections,
1257
- variables: verbosityFiltered.variables,
1258
- };
1259
- if (includePublished && publishedFormatted) {
1260
- const verbosityFilteredPublished = applyFilters({
1261
- variables: publishedFormatted.variables,
1262
- variableCollections: publishedFormatted.collections,
1263
- }, {}, verbosity);
1264
- publishedFormatted = {
1265
- ...publishedFormatted,
1266
- collections: verbosityFilteredPublished.variableCollections,
1267
- variables: verbosityFilteredPublished.variables,
1268
- };
1269
- }
1270
- }
1271
- // Apply pagination if requested
1272
- let paginationInfo;
1273
- if (pageSize) {
1274
- const startIdx = (page - 1) * pageSize;
1275
- const endIdx = startIdx + pageSize;
1276
- const totalVars = localFormatted.variables.length;
1277
- paginationInfo = {
1278
- page,
1279
- pageSize,
1280
- totalItems: totalVars,
1281
- totalPages: Math.ceil(totalVars / pageSize),
1282
- hasNextPage: endIdx < totalVars,
1283
- hasPrevPage: page > 1,
1284
- };
1285
- localFormatted.variables = localFormatted.variables.slice(startIdx, endIdx);
1286
- if (includePublished && publishedFormatted) {
1287
- publishedFormatted.variables = publishedFormatted.variables.slice(startIdx, endIdx);
1288
- }
1289
- }
1290
- // Cache the successful REST API response
1291
- const dataForCache = {
1292
- fileKey,
1293
- local: {
1294
- summary: localFormatted.summary,
1295
- collections: localFormatted.collections,
1296
- variables: localFormatted.variables,
1297
- },
1298
- ...(includePublished &&
1299
- publishedFormatted && {
1300
- published: {
1301
- summary: publishedFormatted.summary,
1302
- collections: publishedFormatted.collections,
1303
- variables: publishedFormatted.variables,
1304
- },
1305
- }),
1306
- verbosity: verbosity || "standard",
1307
- enriched: enrich || false,
1308
- timestamp: Date.now(),
1309
- source: "rest_api",
1310
- };
1311
- if (variablesCache) {
1312
- variablesCache.set(fileKey, { data: dataForCache, timestamp: Date.now() });
1313
- logger.info({ fileKey }, "Cached REST API variables");
1314
- }
1315
- // Apply alias resolution if requested (REST API format has local.variables)
1316
- if (resolveAliases && localFormatted.variables?.length > 0) {
1317
- // Build maps from local variables and collections
1318
- const allVariablesMap = new Map();
1319
- const collectionsMap = new Map();
1320
- for (const v of localFormatted.variables || []) {
1321
- allVariablesMap.set(v.id, v);
1322
- }
1323
- for (const c of localFormatted.collections || []) {
1324
- collectionsMap.set(c.id, c);
1325
- }
1326
- // Also include published variables if available
1327
- if (publishedFormatted?.variables) {
1328
- for (const v of publishedFormatted.variables) {
1329
- allVariablesMap.set(v.id, v);
1330
- }
1331
- }
1332
- if (publishedFormatted?.collections) {
1333
- for (const c of publishedFormatted.collections) {
1334
- collectionsMap.set(c.id, c);
1335
- }
1336
- }
1337
- localFormatted.variables = resolveVariableAliases(localFormatted.variables, allVariablesMap, collectionsMap);
1338
- if (publishedFormatted?.variables) {
1339
- publishedFormatted.variables = resolveVariableAliases(publishedFormatted.variables, allVariablesMap, collectionsMap);
1340
- }
1341
- logger.info({ fileKey, resolvedCount: localFormatted.variables.length }, 'Applied alias resolution to REST API variables');
1342
- }
1343
- // Handle resource_links format
1344
- if (returnAsLinks) {
1345
- const content = [
1346
- {
1347
- type: "text",
1348
- text: `Variables for file ${fileKey} (${localFormatted.variables.length} variables). Use figma_get_variable_by_id to fetch specific variables:\n\n`,
1349
- },
1350
- ];
1351
- for (const variable of localFormatted.variables) {
1352
- content.push({
1353
- type: "resource",
1354
- resource: {
1355
- uri: `figma://variable/${fileKey}/${variable.id}`,
1356
- mimeType: "application/json",
1357
- text: `${variable.name} (${variable.resolvedType})`,
1358
- },
1359
- });
1360
- }
1361
- logger.info({
1362
- fileKey,
1363
- format: 'resource_links',
1364
- variableCount: localFormatted.variables.length,
1365
- linkCount: content.length - 1,
1366
- }, `Returning REST API variables as resource_links`);
1367
- return { content };
1368
- }
1369
- // Build initial response data
1370
- const responseData = {
1371
- fileKey,
1372
- local: {
1373
- summary: localFormatted.summary,
1374
- collections: localFormatted.collections,
1375
- variables: localFormatted.variables,
1376
- },
1377
- ...(includePublished &&
1378
- publishedFormatted && {
1379
- published: {
1380
- summary: publishedFormatted.summary,
1381
- collections: publishedFormatted.collections,
1382
- variables: publishedFormatted.variables,
1383
- },
1384
- }),
1385
- verbosity: verbosity || "standard",
1386
- enriched: enrich || false,
1387
- ...(paginationInfo && { pagination: paginationInfo }),
1388
- };
1389
- // Mark REST API as successful
1390
- restApiSucceeded = true;
1391
- logger.info({ fileKey }, "REST API fetch successful, skipping F-MCP ATezer Bridge");
1392
- // Use adaptive response to prevent context exhaustion
1393
- return adaptiveResponse(responseData, {
1394
- toolName: "figma_get_variables",
1395
- compressionCallback: (adjustedLevel) => {
1396
- // Re-apply filters with adjusted verbosity
1397
- const level = adjustedLevel;
1398
- const refiltered = applyFilters({
1399
- variables: localFormatted.variables,
1400
- variableCollections: localFormatted.collections,
1401
- }, { collection, namePattern, mode }, level);
1402
- return {
1403
- ...responseData,
1404
- local: {
1405
- ...responseData.local,
1406
- variables: refiltered.variables,
1407
- collections: refiltered.variableCollections,
1408
- },
1409
- verbosity: level,
1410
- };
1411
- },
1412
- suggestedActions: [
1413
- "Use verbosity='inventory' or 'summary' for large variable sets",
1414
- "Apply filters: collection, namePattern, or mode parameters",
1415
- "Use pagination with pageSize parameter (default 50, max 100)",
1416
- "Use returnAsLinks=true to get resource_link references instead of full data",
1417
- ],
1418
- });
1419
- }
1420
- catch (restError) {
1421
- const errorMessage = restError instanceof Error ? restError.message : String(restError);
1422
- logger.warn({ error: errorMessage }, "REST API failed, will try F-MCP ATezer Bridge fallback");
1423
- // Don't throw - fall through to F-MCP ATezer Bridge
1424
- }
1425
- }
1426
- // FALLBACK: Try Desktop connection (when no token available OR as secondary fallback)
1427
- // Ensure browser manager is initialized
1428
- if (ensureInitialized && !parseFromConsole && (!hasToken || !restApiSucceeded)) {
1429
- logger.info("Calling ensureInitialized to initialize browser manager");
1430
- await ensureInitialized();
1431
- }
1432
- const browserManager = getBrowserManager?.();
1433
- logger.info({ hasBrowserManager: !!browserManager, parseFromConsole, hasToken, restApiSucceeded }, "Desktop connection check");
1434
- // Debug: Log why Desktop connection might be skipped
1435
- if (!browserManager) {
1436
- logger.error("Desktop connection skipped: browserManager is not available");
1437
- }
1438
- else if (parseFromConsole) {
1439
- logger.info("Desktop connection skipped: parseFromConsole is true");
1440
- }
1441
- else if (restApiSucceeded) {
1442
- logger.info("Desktop connection skipped: REST API already succeeded");
1443
- }
1444
- if (browserManager && !parseFromConsole && (!hasToken || !restApiSucceeded)) {
1445
- try {
1446
- logger.info({ fileKey }, "Attempting to get variables via Desktop connection");
1447
- // Import and use the Desktop connector
1448
- const { FigmaDesktopConnector } = await import('./figma-desktop-connector.js');
1449
- const page = await browserManager.getPage();
1450
- logger.info("Got page from browser manager");
1451
- // Log to browser console for MCP capture
1452
- await page.evaluate(() => {
1453
- console.log('[FIGMA_TOOLS] 🚀 Got page from browser manager, creating Desktop connector...');
1454
- });
1455
- const connector = new FigmaDesktopConnector(page);
1456
- await page.evaluate(() => {
1457
- console.log('[FIGMA_TOOLS] ✅ Desktop connector created, initializing...');
1458
- });
1459
- await connector.initialize();
1460
- logger.info("Desktop connector initialized, calling getVariablesFromPluginUI...");
1461
- await page.evaluate(() => {
1462
- console.log('[FIGMA_TOOLS] ✅ Desktop connector initialized, calling getVariablesFromPluginUI...');
1463
- });
1464
- const desktopResult = await connector.getVariablesFromPluginUI(fileKey);
1465
- if (desktopResult.success && desktopResult.variables) {
1466
- logger.info({
1467
- variableCount: desktopResult.variables.length,
1468
- collectionCount: desktopResult.variableCollections?.length
1469
- }, "Successfully retrieved variables via Desktop connection!");
1470
- // Prepare data for caching (using the raw data, not enriched)
1471
- const dataForCache = {
1472
- fileKey,
1473
- source: "desktop_connection",
1474
- timestamp: desktopResult.timestamp || Date.now(),
1475
- variables: desktopResult.variables,
1476
- variableCollections: desktopResult.variableCollections,
1477
- };
1478
- // Store in cache with LRU eviction
1479
- if (variablesCache) {
1480
- evictOldestCacheEntry(variablesCache);
1481
- variablesCache.set(fileKey, {
1482
- data: dataForCache,
1483
- timestamp: Date.now(),
1484
- });
1485
- logger.info({ fileKey, cacheSize: variablesCache.size }, 'Stored variables in cache');
1486
- }
1487
- // Apply format logic
1488
- let responseData = dataForCache;
1489
- if (format === 'summary') {
1490
- responseData = generateSummary(dataForCache);
1491
- logger.info({ fileKey, estimatedTokens: estimateTokens(responseData) }, 'Generated summary from fetched data');
1492
- }
1493
- else if (format === 'filtered') {
1494
- // Apply filters with verbosity-aware valuesByMode transformation
1495
- responseData = applyFilters(dataForCache, {
1496
- collection,
1497
- namePattern,
1498
- mode,
1499
- }, verbosity || 'standard');
1500
- logger.info({
1501
- fileKey,
1502
- originalCount: dataForCache.variables?.length,
1503
- filteredCount: responseData.variables?.length,
1504
- }, 'Applied filters to fetched data');
1505
- // Apply pagination (CRITICAL - was missing!)
1506
- let paginationInfo = null;
1507
- const paginated = paginateVariables(responseData, page || 1, pageSize || 50);
1508
- responseData = paginated.data;
1509
- paginationInfo = paginated.pagination;
1510
- // Apply verbosity filtering (CRITICAL - was missing!)
1511
- const effectiveVerbosity = verbosity || "inventory";
1512
- // Only include collections referenced by paginated variables
1513
- const referencedCollectionIds = new Set(responseData.variables.map((v) => v.variableCollectionId));
1514
- responseData.variableCollections = responseData.variableCollections.filter((c) => referencedCollectionIds.has(c.id));
1515
- // Filter variables by verbosity
1516
- responseData.variables = responseData.variables.map((v) => {
1517
- if (effectiveVerbosity === "inventory") {
1518
- return {
1519
- id: v.id,
1520
- name: v.name,
1521
- collectionId: v.variableCollectionId,
1522
- };
1523
- }
1524
- if (effectiveVerbosity === "summary") {
1525
- return {
1526
- id: v.id,
1527
- name: v.name,
1528
- resolvedType: v.resolvedType,
1529
- valuesByMode: v.valuesByMode,
1530
- variableCollectionId: v.variableCollectionId,
1531
- };
1532
- }
1533
- return v; // standard/full
1534
- });
1535
- // Filter collections by verbosity
1536
- responseData.variableCollections = responseData.variableCollections.map((c) => {
1537
- if (effectiveVerbosity === "inventory") {
1538
- return {
1539
- id: c.id,
1540
- name: c.name,
1541
- modeNames: c.modes?.map((m) => m.name) || [],
1542
- };
1543
- }
1544
- if (effectiveVerbosity === "summary") {
1545
- return {
1546
- id: c.id,
1547
- name: c.name,
1548
- modes: c.modes,
1549
- };
1550
- }
1551
- return c; // standard/full
1552
- });
1553
- }
1554
- else {
1555
- // format === 'full'
1556
- // Check if we need to auto-summarize
1557
- const estimatedTokens = estimateTokens(responseData);
1558
- if (estimatedTokens > 25000) {
1559
- logger.warn({ fileKey, estimatedTokens }, 'Full data exceeds MCP token limit (25K), auto-summarizing. Use format=summary or format=filtered to get specific data.');
1560
- const summary = generateSummary(responseData);
1561
- return {
1562
- content: [
1563
- {
1564
- type: "text",
1565
- text: JSON.stringify({
1566
- fileKey,
1567
- source: 'desktop_connection_auto_summarized',
1568
- warning: 'Full dataset exceeds MCP token limit (25,000 tokens)',
1569
- suggestion: 'Use format="summary" for overview or format="filtered" with collection/namePattern/mode filters to get specific variables',
1570
- estimatedTokens,
1571
- summary,
1572
- }),
1573
- },
1574
- ],
1575
- };
1576
- }
1577
- }
1578
- // Apply alias resolution if requested
1579
- if (resolveAliases && responseData.variables?.length > 0) {
1580
- // Build maps from ALL variables for resolution
1581
- const allVariablesMap = new Map();
1582
- const collectionsMap = new Map();
1583
- for (const v of dataForCache.variables || []) {
1584
- allVariablesMap.set(v.id, v);
1585
- }
1586
- for (const c of dataForCache.variableCollections || []) {
1587
- collectionsMap.set(c.id, c);
1588
- }
1589
- responseData.variables = resolveVariableAliases(responseData.variables, allVariablesMap, collectionsMap);
1590
- logger.info({ fileKey, resolvedCount: responseData.variables.length }, 'Applied alias resolution to Desktop variables');
1591
- }
1592
- // If returnAsLinks=true, return resource_link references
1593
- if (returnAsLinks) {
1594
- const summary = {
1595
- fileKey,
1596
- source: 'desktop_connection',
1597
- totalVariables: responseData.variables?.length || 0,
1598
- totalCollections: responseData.variableCollections?.length || 0,
1599
- };
1600
- const content = [
1601
- {
1602
- type: "text",
1603
- text: JSON.stringify(summary),
1604
- },
1605
- ];
1606
- // Add resource_link for each variable
1607
- responseData.variables?.forEach((v) => {
1608
- content.push({
1609
- type: "resource_link",
1610
- uri: `figma://variable/${v.id}`,
1611
- name: v.name || v.id,
1612
- description: `${v.resolvedType || 'VARIABLE'} from ${fileKey}`,
1613
- });
1614
- });
1615
- logger.info({
1616
- fileKey,
1617
- format: 'resource_links',
1618
- variableCount: responseData.variables?.length || 0,
1619
- linkCount: content.length - 1,
1620
- }, `Returning Desktop variables as resource_links`);
1621
- return { content };
1622
- }
1623
- // Default: return full data (removed pretty printing)
1624
- return {
1625
- content: [
1626
- {
1627
- type: "text",
1628
- text: JSON.stringify({
1629
- fileKey,
1630
- source: "desktop_connection",
1631
- format: format || 'full',
1632
- timestamp: dataForCache.timestamp,
1633
- data: responseData,
1634
- cached: true,
1635
- }),
1636
- },
1637
- ],
1638
- };
1639
- }
1640
- }
1641
- catch (desktopError) {
1642
- const errorMessage = desktopError instanceof Error ? desktopError.message : String(desktopError);
1643
- const errorStack = desktopError instanceof Error ? desktopError.stack : undefined;
1644
- logger.error({
1645
- error: desktopError,
1646
- message: errorMessage,
1647
- stack: errorStack
1648
- }, "Desktop connection failed, falling back to other methods");
1649
- // Try to log to browser console if we have access to page
1650
- try {
1651
- if (browserManager) {
1652
- const page = await browserManager.getPage();
1653
- await page.evaluate((msg, stack) => {
1654
- console.error('[FIGMA_TOOLS] ❌ Desktop connection failed:', msg);
1655
- if (stack) {
1656
- console.error('[FIGMA_TOOLS] Stack trace:', stack);
1657
- }
1658
- }, errorMessage, errorStack);
1659
- }
1660
- }
1661
- catch (logError) {
1662
- // Ignore logging errors
1663
- }
1664
- // Continue to try other methods
1665
- }
1666
- }
1667
- // FALLBACK: Parse from console logs if requested
1668
- if (parseFromConsole) {
1669
- const consoleMonitor = getConsoleMonitor?.();
1670
- if (!consoleMonitor) {
1671
- throw new Error("Console monitoring not available. Make sure browser is connected to Figma.");
1672
- }
1673
- logger.info({ fileKey }, "Parsing variables from console logs");
1674
- // Get recent logs
1675
- const logs = consoleMonitor.getLogs({ count: 100, level: "log" });
1676
- const varLog = snippetInjector.findVariablesLog(logs);
1677
- if (!varLog) {
1678
- throw new Error("No variables found in console logs.\n\n" +
1679
- "Did you run the snippet in Figma's plugin console? Here's the correct workflow:\n\n" +
1680
- "1. Call figma_get_variables() without parameters (you may have already done this)\n" +
1681
- "2. Copy the provided snippet\n" +
1682
- "3. Open Figma Desktop → Plugins → Development → Open Console\n" +
1683
- "4. Paste and run the snippet in the PLUGIN console (not browser DevTools)\n" +
1684
- "5. Wait for '✅ Variables data captured!' confirmation\n" +
1685
- "6. Then call figma_get_variables({ parseFromConsole: true })\n\n" +
1686
- "Note: The browser console won't work - you need a plugin console for the figma.variables API.");
1687
- }
1688
- // Parse variables from log
1689
- const parsedData = snippetInjector.parseVariablesFromLog(varLog);
1690
- if (!parsedData) {
1691
- throw new Error("Failed to parse variables from console log");
1692
- }
1693
- return {
1694
- content: [
1695
- {
1696
- type: "text",
1697
- text: JSON.stringify({
1698
- fileKey,
1699
- source: "console_capture",
1700
- local: {
1701
- summary: {
1702
- total_variables: parsedData.variables.length,
1703
- total_collections: parsedData.variableCollections.length,
1704
- },
1705
- collections: parsedData.variableCollections,
1706
- variables: parsedData.variables,
1707
- },
1708
- timestamp: parsedData.timestamp,
1709
- enriched: false,
1710
- }, null, 2),
1711
- },
1712
- ],
1713
- };
1714
- }
1715
- // No more fallback options available
1716
- throw new Error(`Cannot retrieve variables. All methods failed.\n\n` +
1717
- `Tried methods:\n` +
1718
- `${hasToken ? '✗ REST API (failed)\n' : ''}` +
1719
- `✗ F-MCP ATezer Bridge (failed or not available)\n` +
1720
- `\nTo fix:\n` +
1721
- `1. If you have FIGMA_ACCESS_TOKEN: Check your token permissions\n` +
1722
- `2. Install and run the F-MCP ATezer Bridge\n` +
1723
- `3. Alternative: Use parseFromConsole=true with console snippet workflow`);
1724
- }
1725
- catch (error) {
1726
- logger.error({ error }, "Failed to get variables");
1727
- const errorMessage = error instanceof Error ? error.message : String(error);
1728
- // FIXED: Jump directly to Styles API (fast) instead of full file data (slow)
1729
- if (errorMessage.includes("403")) {
1730
- try {
1731
- logger.info({ fileKey }, "Variables API requires Enterprise, falling back to Styles API");
1732
- let api;
1733
- try {
1734
- api = await getFigmaAPI();
1735
- }
1736
- catch (apiError) {
1737
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
1738
- throw new Error(`Cannot retrieve variables or styles. REST API authentication required for both.\n` +
1739
- `Error: ${errorMessage}\n\n` +
1740
- `To fix:\n` +
1741
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable\n` +
1742
- `2. Cloud mode: Authenticate via OAuth`);
1743
- }
1744
- // Use the Styles API directly - much faster than getFile!
1745
- const stylesData = await api.getStyles(fileKey);
1746
- // Format the styles data similar to variables
1747
- const formattedStyles = {
1748
- summary: {
1749
- total_styles: stylesData.meta?.styles?.length || 0,
1750
- message: "Variables API requires Enterprise. Here are your design styles instead.",
1751
- note: "These are Figma Styles (not Variables). Styles are the traditional way to store design tokens in Figma."
1752
- },
1753
- styles: stylesData.meta?.styles || []
1754
- };
1755
- logger.info({ styleCount: formattedStyles.summary.total_styles }, "Successfully retrieved styles as fallback!");
1756
- return {
1757
- content: [
1758
- {
1759
- type: "text",
1760
- text: JSON.stringify({
1761
- fileKey,
1762
- source: "styles_api",
1763
- message: "Variables API requires an Enterprise plan. Retrieved your design system styles instead.",
1764
- data: formattedStyles,
1765
- fallback_method: true,
1766
- }, null, 2),
1767
- },
1768
- ],
1769
- };
1770
- }
1771
- catch (styleError) {
1772
- logger.warn({ error: styleError }, "Style extraction failed");
1773
- // Return a simple error message without the console snippet
1774
- return {
1775
- content: [
1776
- {
1777
- type: "text",
1778
- text: JSON.stringify({
1779
- error: "Unable to extract variables or styles from this file",
1780
- message: "The Variables API requires an Enterprise plan, and the automatic style extraction encountered an error.",
1781
- possibleReasons: [
1782
- "The file may be private or require additional permissions",
1783
- "The file structure may not contain extractable styles",
1784
- "There may be a network or authentication issue"
1785
- ],
1786
- suggestion: "Please ensure the file is accessible and try again, or check if your token has the necessary permissions.",
1787
- technical: styleError instanceof Error ? styleError.message : String(styleError)
1788
- }, null, 2),
1789
- },
1790
- ],
1791
- };
1792
- }
1793
- }
1794
- // Standard error response
1795
- return {
1796
- content: [
1797
- {
1798
- type: "text",
1799
- text: JSON.stringify({
1800
- error: errorMessage,
1801
- message: "Failed to retrieve Figma variables",
1802
- hint: errorMessage.includes("403")
1803
- ? "Variables API requires Enterprise plan. Set useConsoleFallback=true for alternative method."
1804
- : "Make sure FIGMA_ACCESS_TOKEN is configured and has appropriate permissions",
1805
- }, null, 2),
1806
- },
1807
- ],
1808
- isError: true,
1809
- };
1810
- }
1811
- });
1812
- // Tool 10: Get Component Data
1813
- server.registerTool("figma_get_component", {
1814
- description: "Get component metadata or reconstruction specification. Two export formats: (1) 'metadata' (default) - comprehensive documentation with properties, variants, and design tokens for style guides and references, (2) 'reconstruction' - node tree specification compatible with Figma Component Reconstructor plugin for programmatic component creation. IMPORTANT: For local/unpublished components with metadata format, ensure the F-MCP ATezer Bridge is running (Right-click in Figma → Plugins → Development → F-MCP ATezer Bridge) to get complete description data.",
1815
- inputSchema: {
1816
- fileUrl: z
1817
- .string()
1818
- .url()
1819
- .optional()
1820
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
1821
- nodeId: z
1822
- .string()
1823
- .describe("Component node ID (e.g., '123:456')"),
1824
- format: z
1825
- .enum(["metadata", "reconstruction"])
1826
- .optional()
1827
- .default("metadata")
1828
- .describe("Export format: 'metadata' (default) for comprehensive documentation, 'reconstruction' for node tree specification compatible with Figma Component Reconstructor plugin"),
1829
- enrich: z
1830
- .boolean()
1831
- .optional()
1832
- .describe("Set to true when user asks for: design token coverage, hardcoded value analysis, or component quality metrics. Adds token coverage analysis and hardcoded value detection. Default: false. Only applicable for metadata format."),
1833
- },
1834
- annotations: { readOnlyHint: true },
1835
- }, async ({ fileUrl, nodeId, format = "metadata", enrich }) => {
1836
- try {
1837
- const url = fileUrl || getCurrentUrl();
1838
- if (!url) {
1839
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
1840
- }
1841
- const fileKey = extractFileKey(url);
1842
- if (!fileKey) {
1843
- throw new Error(`Invalid Figma URL: ${url}`);
1844
- }
1845
- logger.info({ fileKey, nodeId, format, enrich }, "Fetching component data");
1846
- // PRIORITY 1: Try F-MCP ATezer Bridge plugin UI first (has reliable description field!)
1847
- if (getBrowserManager && ensureInitialized) {
1848
- try {
1849
- logger.info({ nodeId }, "Attempting to get component via F-MCP ATezer Bridge plugin UI");
1850
- await ensureInitialized();
1851
- const browserManager = getBrowserManager();
1852
- if (!browserManager) {
1853
- throw new Error("Browser manager not available after initialization");
1854
- }
1855
- const { FigmaDesktopConnector } = await import('./figma-desktop-connector.js');
1856
- const page = await browserManager.getPage();
1857
- const connector = new FigmaDesktopConnector(page);
1858
- await connector.initialize();
1859
- const desktopResult = await connector.getComponentFromPluginUI(nodeId);
1860
- if (desktopResult.success && desktopResult.component) {
1861
- logger.info({
1862
- componentName: desktopResult.component.name,
1863
- hasDescription: !!desktopResult.component.description,
1864
- hasDescriptionMarkdown: !!desktopResult.component.descriptionMarkdown,
1865
- annotationsCount: desktopResult.component.annotations?.length || 0
1866
- }, "Successfully retrieved component via F-MCP ATezer Bridge plugin UI!");
1867
- // Handle reconstruction format
1868
- if (format === "reconstruction") {
1869
- const reconstructionSpec = extractNodeSpec(desktopResult.component);
1870
- const validation = validateReconstructionSpec(reconstructionSpec);
1871
- if (!validation.valid) {
1872
- logger.warn({ errors: validation.errors }, "Reconstruction spec validation warnings");
1873
- }
1874
- // Check if this is a COMPONENT_SET - plugin cannot create these
1875
- if (reconstructionSpec.type === 'COMPONENT_SET') {
1876
- const variants = listVariants(desktopResult.component);
1877
- return {
1878
- content: [
1879
- {
1880
- type: "text",
1881
- text: JSON.stringify({
1882
- error: "COMPONENT_SET_NOT_SUPPORTED",
1883
- message: "The Figma Component Reconstructor plugin cannot create COMPONENT_SET nodes (variant containers). Please select a specific variant component instead.",
1884
- componentName: reconstructionSpec.name,
1885
- availableVariants: variants,
1886
- instructions: [
1887
- "1. In Figma, expand the component set to see individual variants",
1888
- "2. Select the specific variant you want to reconstruct",
1889
- "3. Copy the node ID of that variant",
1890
- "4. Use figma_get_component with that variant's node ID"
1891
- ],
1892
- note: "COMPONENT_SET is automatically created by Figma when you have variants. The plugin can only create individual COMPONENT nodes."
1893
- }, null, 2),
1894
- },
1895
- ],
1896
- };
1897
- }
1898
- // Return spec directly for plugin compatibility
1899
- // Plugin expects name, type, etc. at root level
1900
- return {
1901
- content: [
1902
- {
1903
- type: "text",
1904
- text: JSON.stringify(reconstructionSpec, null, 2),
1905
- },
1906
- ],
1907
- };
1908
- }
1909
- // Handle metadata format (original behavior)
1910
- let formatted = desktopResult.component;
1911
- // Apply enrichment if requested
1912
- if (enrich) {
1913
- const enrichmentOptions = {
1914
- enrich: true,
1915
- include_usage: true,
1916
- };
1917
- formatted = await enrichmentService.enrichComponent(formatted, fileKey, enrichmentOptions);
1918
- }
1919
- return {
1920
- content: [
1921
- {
1922
- type: "text",
1923
- text: JSON.stringify({
1924
- fileKey,
1925
- nodeId,
1926
- component: formatted,
1927
- source: "desktop_bridge_plugin",
1928
- enriched: enrich || false,
1929
- note: "Retrieved via F-MCP ATezer Bridge plugin - description fields and annotations are reliable and current"
1930
- }, null, 2),
1931
- },
1932
- ],
1933
- };
1934
- }
1935
- }
1936
- catch (desktopError) {
1937
- logger.warn({ error: desktopError, nodeId }, "F-MCP ATezer Bridge plugin failed, falling back to REST API");
1938
- }
1939
- }
1940
- // FALLBACK: Use REST API (may have missing/outdated description)
1941
- logger.info({ nodeId }, "Using REST API fallback");
1942
- // Initialize API client (may throw if no token available)
1943
- let api;
1944
- try {
1945
- api = await getFigmaAPI();
1946
- }
1947
- catch (apiError) {
1948
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
1949
- throw new Error(`Cannot retrieve component data. Both F-MCP ATezer Bridge and REST API are unavailable.\n` +
1950
- `F-MCP ATezer Bridge: ${getBrowserManager && ensureInitialized ? 'Failed (see logs above)' : 'Not available (local mode only)'}\n` +
1951
- `REST API: ${errorMessage}\n\n` +
1952
- `To fix:\n` +
1953
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable, OR ensure F-MCP ATezer Bridge is running\n` +
1954
- `2. Cloud mode: Authenticate via OAuth\n` +
1955
- `3. Make sure figma_navigate was called to initialize browser connection`);
1956
- }
1957
- const componentData = await api.getComponentData(fileKey, nodeId);
1958
- if (!componentData) {
1959
- throw new Error(`Component not found: ${nodeId}`);
1960
- }
1961
- // Handle reconstruction format
1962
- if (format === "reconstruction") {
1963
- const reconstructionSpec = extractNodeSpec(componentData.document);
1964
- const validation = validateReconstructionSpec(reconstructionSpec);
1965
- if (!validation.valid) {
1966
- logger.warn({ errors: validation.errors }, "Reconstruction spec validation warnings");
1967
- }
1968
- // Check if this is a COMPONENT_SET - plugin cannot create these
1969
- if (reconstructionSpec.type === 'COMPONENT_SET') {
1970
- const variants = listVariants(componentData.document);
1971
- return {
1972
- content: [
1973
- {
1974
- type: "text",
1975
- text: JSON.stringify({
1976
- error: "COMPONENT_SET_NOT_SUPPORTED",
1977
- message: "The Figma Component Reconstructor plugin cannot create COMPONENT_SET nodes (variant containers). Please select a specific variant component instead.",
1978
- componentName: reconstructionSpec.name,
1979
- availableVariants: variants,
1980
- instructions: [
1981
- "1. In Figma, expand the component set to see individual variants",
1982
- "2. Select the specific variant you want to reconstruct",
1983
- "3. Copy the node ID of that variant",
1984
- "4. Use figma_get_component with that variant's node ID"
1985
- ],
1986
- note: "COMPONENT_SET is automatically created by Figma when you have variants. The plugin can only create individual COMPONENT nodes."
1987
- }, null, 2),
1988
- },
1989
- ],
1990
- };
1991
- }
1992
- // Return spec directly for plugin compatibility
1993
- // Plugin expects name, type, etc. at root level
1994
- return {
1995
- content: [
1996
- {
1997
- type: "text",
1998
- text: JSON.stringify(reconstructionSpec, null, 2),
1999
- },
2000
- ],
2001
- };
2002
- }
2003
- // Handle metadata format (original behavior)
2004
- let formatted = formatComponentData(componentData.document);
2005
- // Apply enrichment if requested
2006
- if (enrich) {
2007
- const enrichmentOptions = {
2008
- enrich: true,
2009
- include_usage: true,
2010
- };
2011
- formatted = await enrichmentService.enrichComponent(formatted, fileKey, enrichmentOptions);
2012
- }
2013
- return {
2014
- content: [
2015
- {
2016
- type: "text",
2017
- text: JSON.stringify({
2018
- fileKey,
2019
- nodeId,
2020
- component: formatted,
2021
- source: "rest_api",
2022
- enriched: enrich || false,
2023
- warning: "Retrieved via REST API - description field may be missing due to known Figma API bug",
2024
- action_required: formatted.description || formatted.descriptionMarkdown ? null : "To get reliable component descriptions, run the F-MCP ATezer Bridge plugin in Figma Desktop: Right-click → Plugins → Development → F-MCP ATezer Bridge, then try again."
2025
- }, null, 2),
2026
- },
2027
- ],
2028
- };
2029
- }
2030
- catch (error) {
2031
- logger.error({ error }, "Failed to get component");
2032
- const errorMessage = error instanceof Error ? error.message : String(error);
2033
- return {
2034
- content: [
2035
- {
2036
- type: "text",
2037
- text: JSON.stringify({
2038
- error: errorMessage,
2039
- message: "Failed to retrieve component data",
2040
- hint: "Make sure the node ID is correct and the file is accessible",
2041
- }, null, 2),
2042
- },
2043
- ],
2044
- isError: true,
2045
- };
2046
- }
2047
- });
2048
- // Tool 11: Get Styles
2049
- server.registerTool("figma_get_styles", {
2050
- description: "Get all styles (color, text, effects, grids) from a Figma file with optional code exports. Use when user asks for: text styles, color palette, design system styles, typography, or style documentation. Returns organized style definitions with resolved values. NOT for design tokens/variables (use figma_get_variables). Set enrich=true for CSS/Tailwind/Sass code examples. Supports verbosity control to manage payload size.",
2051
- inputSchema: {
2052
- fileUrl: z
2053
- .string()
2054
- .url()
2055
- .optional()
2056
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
2057
- verbosity: z
2058
- .enum(["summary", "standard", "full"])
2059
- .optional()
2060
- .default("standard")
2061
- .describe("Controls payload size: 'summary' (names/types only, ~85% smaller), 'standard' (essential properties, ~40% smaller), 'full' (everything). Default: standard"),
2062
- enrich: z
2063
- .boolean()
2064
- .optional()
2065
- .describe("Set to true when user asks for: CSS/Sass/Tailwind code, export formats, usage information, code examples, or design system exports. Adds resolved values, usage analysis, and export format examples. Default: false for backward compatibility"),
2066
- include_usage: z
2067
- .boolean()
2068
- .optional()
2069
- .describe("Include component usage information (requires enrich=true)"),
2070
- include_exports: z
2071
- .boolean()
2072
- .optional()
2073
- .describe("Include export format examples (requires enrich=true)"),
2074
- export_formats: z
2075
- .array(z.enum(["css", "sass", "tailwind", "typescript", "json"]))
2076
- .optional()
2077
- .describe("Which code formats to generate examples for. Use when user mentions specific formats like 'CSS', 'Tailwind', 'SCSS', 'TypeScript', etc. Automatically enables enrichment. Default: all formats"),
2078
- },
2079
- annotations: { readOnlyHint: true },
2080
- }, async ({ fileUrl, verbosity, enrich, include_usage, include_exports, export_formats }) => {
2081
- try {
2082
- let api;
2083
- try {
2084
- api = await getFigmaAPI();
2085
- }
2086
- catch (apiError) {
2087
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
2088
- throw new Error(`Cannot retrieve styles. REST API authentication required.\n` +
2089
- `Error: ${errorMessage}\n\n` +
2090
- `To fix:\n` +
2091
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable\n` +
2092
- `2. Cloud mode: Authenticate via OAuth`);
2093
- }
2094
- const url = fileUrl || getCurrentUrl();
2095
- if (!url) {
2096
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2097
- }
2098
- const fileKey = extractFileKey(url);
2099
- if (!fileKey) {
2100
- throw new Error(`Invalid Figma URL: ${url}`);
2101
- }
2102
- logger.info({ fileKey, verbosity, enrich }, "Fetching styles");
2103
- // Get styles via REST API
2104
- const stylesData = await api.getStyles(fileKey);
2105
- let styles = stylesData.meta?.styles || [];
2106
- logger.info({ styleCount: styles.length }, "Successfully retrieved styles via REST API");
2107
- // Apply verbosity filtering
2108
- const filterStyle = (style, level) => {
2109
- if (!style)
2110
- return style;
2111
- if (level === "summary") {
2112
- // Summary: Only key, name, type (~85% reduction)
2113
- return {
2114
- key: style.key,
2115
- name: style.name,
2116
- style_type: style.style_type,
2117
- };
2118
- }
2119
- if (level === "standard") {
2120
- // Standard: Essential properties (~40% reduction)
2121
- return {
2122
- key: style.key,
2123
- name: style.name,
2124
- description: style.description,
2125
- style_type: style.style_type,
2126
- ...(style.remote && { remote: style.remote }),
2127
- };
2128
- }
2129
- // Full: Return everything
2130
- return style;
2131
- };
2132
- if (verbosity !== "full") {
2133
- styles = styles.map((style) => filterStyle(style, verbosity || "standard"));
2134
- }
2135
- // Apply enrichment if requested
2136
- if (enrich) {
2137
- const enrichmentOptions = {
2138
- enrich: true,
2139
- include_usage: include_usage !== false,
2140
- include_exports: include_exports !== false,
2141
- export_formats: export_formats || [
2142
- "css",
2143
- "sass",
2144
- "tailwind",
2145
- "typescript",
2146
- "json",
2147
- ],
2148
- };
2149
- styles = await enrichmentService.enrichStyles(styles, fileKey, enrichmentOptions);
2150
- }
2151
- const finalResponse = {
2152
- fileKey,
2153
- styles,
2154
- totalStyles: styles.length,
2155
- verbosity: verbosity || "standard",
2156
- enriched: enrich || false,
2157
- };
2158
- // Use adaptive response to prevent context exhaustion
2159
- return adaptiveResponse(finalResponse, {
2160
- toolName: "figma_get_styles",
2161
- compressionCallback: (adjustedLevel) => {
2162
- // Re-apply style filtering with lower verbosity
2163
- const level = adjustedLevel;
2164
- const refilteredStyles = verbosity !== "full"
2165
- ? styles.map((style) => filterStyle(style, level))
2166
- : styles;
2167
- return {
2168
- ...finalResponse,
2169
- styles: refilteredStyles,
2170
- verbosity: level,
2171
- };
2172
- },
2173
- suggestedActions: [
2174
- "Use verbosity='summary' for style names and types only",
2175
- "Use verbosity='standard' for essential style properties",
2176
- "Filter to specific style types if needed",
2177
- ],
2178
- });
2179
- }
2180
- catch (error) {
2181
- logger.error({ error }, "Failed to get styles");
2182
- const errorMessage = error instanceof Error ? error.message : String(error);
2183
- return {
2184
- content: [
2185
- {
2186
- type: "text",
2187
- text: JSON.stringify({
2188
- error: errorMessage,
2189
- message: "Failed to retrieve styles",
2190
- }, null, 2),
2191
- },
2192
- ],
2193
- isError: true,
2194
- };
2195
- }
2196
- });
2197
- // Tool 12: Get Component Image (Visual Reference)
2198
- server.registerTool("figma_get_component_image", {
2199
- description: "Render a specific component or node as an image (PNG, JPG, SVG, PDF). Returns image URL valid for 30 days. Use when user asks for: component screenshot, visual preview, rendered output, or 'show me'. NOT for component metadata/properties (use figma_get_component). NOT for getting code/layout data (use figma_get_component_for_development). Best for: visual references, design review, documentation.",
2200
- inputSchema: {
2201
- fileUrl: z
2202
- .string()
2203
- .url()
2204
- .optional()
2205
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called. If not provided, ask the user to share their Figma file URL (they can copy it from Figma Desktop via right-click → 'Copy link')."),
2206
- nodeId: z
2207
- .string()
2208
- .describe("Component node ID to render as image (e.g., '695:313')"),
2209
- scale: z
2210
- .number()
2211
- .min(0.01)
2212
- .max(4)
2213
- .optional()
2214
- .default(2)
2215
- .describe("Image scale factor (0.01-4, default: 2 for high quality)"),
2216
- format: z
2217
- .enum(["png", "jpg", "svg", "pdf"])
2218
- .optional()
2219
- .default("png")
2220
- .describe("Image format (default: png)"),
2221
- },
2222
- annotations: { readOnlyHint: true },
2223
- }, async ({ fileUrl, nodeId, scale, format }) => {
2224
- try {
2225
- let api;
2226
- try {
2227
- api = await getFigmaAPI();
2228
- }
2229
- catch (apiError) {
2230
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
2231
- throw new Error(`Cannot render component image. REST API authentication required.\n` +
2232
- `Error: ${errorMessage}\n\n` +
2233
- `To fix:\n` +
2234
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable\n` +
2235
- `2. Cloud mode: Authenticate via OAuth\n\n` +
2236
- `Note: For component screenshots, figma_take_screenshot may work as browser-based alternative ` +
2237
- `if you've called figma_navigate first.`);
2238
- }
2239
- const url = fileUrl || getCurrentUrl();
2240
- if (!url) {
2241
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2242
- }
2243
- const fileKey = extractFileKey(url);
2244
- if (!fileKey) {
2245
- throw new Error(`Invalid Figma URL: ${url}`);
2246
- }
2247
- logger.info({ fileKey, nodeId, scale, format }, "Rendering component image");
2248
- // First, fetch the node to check if it's a COMPONENT_SET
2249
- const fileData = await api.getFile(fileKey, { ids: [nodeId] });
2250
- const node = fileData.nodes[nodeId]?.document;
2251
- if (!node) {
2252
- throw new Error(`Node ${nodeId} not found in file ${fileKey}. Please verify the node ID is correct.`);
2253
- }
2254
- // Check if this is a COMPONENT_SET - cannot be rendered as image
2255
- if (node.type === 'COMPONENT_SET') {
2256
- const variants = listVariants(node);
2257
- return {
2258
- content: [
2259
- {
2260
- type: "text",
2261
- text: JSON.stringify({
2262
- error: "COMPONENT_SET_NOT_RENDERABLE",
2263
- message: "Node is a COMPONENT_SET which cannot be rendered. Please use a specific variant component ID instead.",
2264
- componentName: node.name,
2265
- availableVariants: variants,
2266
- instructions: [
2267
- "1. In Figma, expand the component set to see individual variants",
2268
- "2. Select the specific variant you want to render",
2269
- "3. Copy the node ID of that variant",
2270
- "4. Use figma_get_component_image with that variant's node ID"
2271
- ],
2272
- note: "COMPONENT_SET is a container for variants. Only individual variant components can be rendered as images."
2273
- }, null, 2),
2274
- },
2275
- ],
2276
- };
2277
- }
2278
- // Call the new getImages method
2279
- const result = await api.getImages(fileKey, nodeId, {
2280
- scale,
2281
- format,
2282
- contents_only: true,
2283
- });
2284
- const imageUrl = result.images[nodeId];
2285
- if (!imageUrl) {
2286
- throw new Error(`Failed to render image for node ${nodeId}. The node may not exist or may not be renderable.`);
2287
- }
2288
- return {
2289
- content: [
2290
- {
2291
- type: "text",
2292
- text: JSON.stringify({
2293
- fileKey,
2294
- nodeId,
2295
- imageUrl,
2296
- scale,
2297
- format,
2298
- expiresIn: "30 days",
2299
- note: "Use this image as visual reference for component development. Image URLs expire after 30 days.",
2300
- }, null, 2),
2301
- },
2302
- ],
2303
- };
2304
- }
2305
- catch (error) {
2306
- logger.error({ error }, "Failed to render component image");
2307
- const errorMessage = error instanceof Error ? error.message : String(error);
2308
- return {
2309
- content: [
2310
- {
2311
- type: "text",
2312
- text: JSON.stringify({
2313
- error: errorMessage,
2314
- message: "Failed to render component image",
2315
- hint: "Make sure the node ID is correct and the component is renderable",
2316
- }, null, 2),
2317
- },
2318
- ],
2319
- isError: true,
2320
- };
2321
- }
2322
- });
2323
- // Tool 13: Get Component for Development (UI Implementation)
2324
- server.registerTool("figma_get_component_for_development", {
2325
- description: "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.",
2326
- inputSchema: {
2327
- fileUrl: z
2328
- .string()
2329
- .url()
2330
- .optional()
2331
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called."),
2332
- nodeId: z
2333
- .string()
2334
- .describe("Component node ID to get data for (e.g., '695:313')"),
2335
- includeImage: z
2336
- .boolean()
2337
- .optional()
2338
- .default(true)
2339
- .describe("Include rendered image for visual reference (default: true)"),
2340
- },
2341
- annotations: { readOnlyHint: true },
2342
- }, async ({ fileUrl, nodeId, includeImage }) => {
2343
- try {
2344
- let api;
2345
- try {
2346
- api = await getFigmaAPI();
2347
- }
2348
- catch (apiError) {
2349
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
2350
- throw new Error(`Cannot retrieve component for development. REST API authentication required.\n` +
2351
- `Error: ${errorMessage}\n\n` +
2352
- `To fix:\n` +
2353
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable\n` +
2354
- `2. Cloud mode: Authenticate via OAuth\n\n` +
2355
- `Note: For component metadata, figma_get_component has F-MCP ATezer Bridge fallback ` +
2356
- `that works without token (requires figma_navigate first).`);
2357
- }
2358
- const url = fileUrl || getCurrentUrl();
2359
- if (!url) {
2360
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2361
- }
2362
- const fileKey = extractFileKey(url);
2363
- if (!fileKey) {
2364
- throw new Error(`Invalid Figma URL: ${url}`);
2365
- }
2366
- logger.info({ fileKey, nodeId, includeImage }, "Fetching component for development");
2367
- // Get node data with depth for children
2368
- const nodeData = await api.getNodes(fileKey, [nodeId], { depth: 2 });
2369
- const node = nodeData.nodes?.[nodeId]?.document;
2370
- if (!node) {
2371
- throw new Error(`Component not found: ${nodeId}`);
2372
- }
2373
- // Filter to visual/layout properties only
2374
- const filterForDevelopment = (n) => {
2375
- if (!n)
2376
- return n;
2377
- const result = {
2378
- id: n.id,
2379
- name: n.name,
2380
- type: n.type,
2381
- description: n.description,
2382
- descriptionMarkdown: n.descriptionMarkdown,
2383
- };
2384
- // Layout & positioning
2385
- if (n.absoluteBoundingBox)
2386
- result.absoluteBoundingBox = n.absoluteBoundingBox;
2387
- if (n.relativeTransform)
2388
- result.relativeTransform = n.relativeTransform;
2389
- if (n.size)
2390
- result.size = n.size;
2391
- if (n.constraints)
2392
- result.constraints = n.constraints;
2393
- if (n.layoutAlign)
2394
- result.layoutAlign = n.layoutAlign;
2395
- if (n.layoutGrow)
2396
- result.layoutGrow = n.layoutGrow;
2397
- if (n.layoutPositioning)
2398
- result.layoutPositioning = n.layoutPositioning;
2399
- // Auto-layout
2400
- if (n.layoutMode)
2401
- result.layoutMode = n.layoutMode;
2402
- if (n.primaryAxisSizingMode)
2403
- result.primaryAxisSizingMode = n.primaryAxisSizingMode;
2404
- if (n.counterAxisSizingMode)
2405
- result.counterAxisSizingMode = n.counterAxisSizingMode;
2406
- if (n.primaryAxisAlignItems)
2407
- result.primaryAxisAlignItems = n.primaryAxisAlignItems;
2408
- if (n.counterAxisAlignItems)
2409
- result.counterAxisAlignItems = n.counterAxisAlignItems;
2410
- if (n.paddingLeft !== undefined)
2411
- result.paddingLeft = n.paddingLeft;
2412
- if (n.paddingRight !== undefined)
2413
- result.paddingRight = n.paddingRight;
2414
- if (n.paddingTop !== undefined)
2415
- result.paddingTop = n.paddingTop;
2416
- if (n.paddingBottom !== undefined)
2417
- result.paddingBottom = n.paddingBottom;
2418
- if (n.itemSpacing !== undefined)
2419
- result.itemSpacing = n.itemSpacing;
2420
- if (n.itemReverseZIndex)
2421
- result.itemReverseZIndex = n.itemReverseZIndex;
2422
- if (n.strokesIncludedInLayout)
2423
- result.strokesIncludedInLayout = n.strokesIncludedInLayout;
2424
- // Visual properties
2425
- if (n.fills)
2426
- result.fills = n.fills;
2427
- if (n.strokes)
2428
- result.strokes = n.strokes;
2429
- if (n.strokeWeight !== undefined)
2430
- result.strokeWeight = n.strokeWeight;
2431
- if (n.strokeAlign)
2432
- result.strokeAlign = n.strokeAlign;
2433
- if (n.strokeCap)
2434
- result.strokeCap = n.strokeCap;
2435
- if (n.strokeJoin)
2436
- result.strokeJoin = n.strokeJoin;
2437
- if (n.dashPattern)
2438
- result.dashPattern = n.dashPattern;
2439
- if (n.cornerRadius !== undefined)
2440
- result.cornerRadius = n.cornerRadius;
2441
- if (n.rectangleCornerRadii)
2442
- result.rectangleCornerRadii = n.rectangleCornerRadii;
2443
- if (n.effects)
2444
- result.effects = n.effects;
2445
- if (n.opacity !== undefined)
2446
- result.opacity = n.opacity;
2447
- if (n.blendMode)
2448
- result.blendMode = n.blendMode;
2449
- if (n.isMask)
2450
- result.isMask = n.isMask;
2451
- if (n.clipsContent)
2452
- result.clipsContent = n.clipsContent;
2453
- // Typography
2454
- if (n.characters)
2455
- result.characters = n.characters;
2456
- if (n.style)
2457
- result.style = n.style;
2458
- if (n.characterStyleOverrides)
2459
- result.characterStyleOverrides = n.characterStyleOverrides;
2460
- if (n.styleOverrideTable)
2461
- result.styleOverrideTable = n.styleOverrideTable;
2462
- // Component properties & variants
2463
- if (n.componentProperties)
2464
- result.componentProperties = n.componentProperties;
2465
- if (n.componentPropertyDefinitions)
2466
- result.componentPropertyDefinitions = n.componentPropertyDefinitions;
2467
- if (n.variantProperties)
2468
- result.variantProperties = n.variantProperties;
2469
- if (n.componentId)
2470
- result.componentId = n.componentId;
2471
- // State
2472
- if (n.visible !== undefined)
2473
- result.visible = n.visible;
2474
- if (n.locked)
2475
- result.locked = n.locked;
2476
- // Recursively process children
2477
- if (n.children) {
2478
- result.children = n.children.map((child) => filterForDevelopment(child));
2479
- }
2480
- return result;
2481
- };
2482
- const componentData = filterForDevelopment(node);
2483
- // Get image if requested
2484
- let imageUrl = null;
2485
- if (includeImage) {
2486
- try {
2487
- const imageResult = await api.getImages(fileKey, nodeId, {
2488
- scale: 2,
2489
- format: "png",
2490
- contents_only: true,
2491
- });
2492
- imageUrl = imageResult.images[nodeId];
2493
- }
2494
- catch (error) {
2495
- logger.warn({ error }, "Failed to render component image, continuing without it");
2496
- }
2497
- }
2498
- // Build response with component data and image URL
2499
- return {
2500
- content: [
2501
- {
2502
- type: "text",
2503
- text: JSON.stringify({
2504
- fileKey,
2505
- nodeId,
2506
- imageUrl,
2507
- component: componentData,
2508
- metadata: {
2509
- purpose: "component_development",
2510
- note: imageUrl
2511
- ? "Image URL provided above (valid for 30 days). Full component data optimized for UI implementation."
2512
- : "Full component data optimized for UI implementation.",
2513
- },
2514
- }, null, 2),
2515
- },
2516
- ],
2517
- };
2518
- }
2519
- catch (error) {
2520
- logger.error({ error }, "Failed to get component for development");
2521
- const errorMessage = error instanceof Error ? error.message : String(error);
2522
- return {
2523
- content: [
2524
- {
2525
- type: "text",
2526
- text: JSON.stringify({
2527
- error: errorMessage,
2528
- message: "Failed to retrieve component development data",
2529
- }, null, 2),
2530
- },
2531
- ],
2532
- isError: true,
2533
- };
2534
- }
2535
- });
2536
- // Tool 14: Get File for Plugin Development
2537
- server.registerTool("figma_get_file_for_plugin", {
2538
- description: "Get file data optimized for plugin development with filtered properties (IDs, structure, plugin data, component relationships). Excludes visual properties (fills, strokes, effects) to reduce payload. Use when user asks for: plugin development, file structure for manipulation, node IDs for plugin API. NOT for component descriptions (use figma_get_component). NOT for visual/styling data (use figma_get_component_for_development). Supports deeper tree traversal (max depth=5) than figma_get_file_data.",
2539
- inputSchema: {
2540
- fileUrl: z
2541
- .string()
2542
- .url()
2543
- .optional()
2544
- .describe("Figma file URL (e.g., https://figma.com/design/abc123). REQUIRED unless figma_navigate was already called."),
2545
- depth: z
2546
- .number()
2547
- .min(0)
2548
- .max(5)
2549
- .optional()
2550
- .default(2)
2551
- .describe("How many levels of children to include (default: 2, max: 5). Higher depths are safe here due to filtering."),
2552
- nodeIds: z
2553
- .array(z.string())
2554
- .optional()
2555
- .describe("Specific node IDs to retrieve (optional)"),
2556
- },
2557
- annotations: { readOnlyHint: true },
2558
- }, async ({ fileUrl, depth, nodeIds }) => {
2559
- try {
2560
- let api;
2561
- try {
2562
- api = await getFigmaAPI();
2563
- }
2564
- catch (apiError) {
2565
- const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
2566
- throw new Error(`Cannot retrieve file data for plugin development. REST API authentication required.\n` +
2567
- `Error: ${errorMessage}\n\n` +
2568
- `To fix:\n` +
2569
- `1. Local mode: Set FIGMA_ACCESS_TOKEN environment variable\n` +
2570
- `2. Cloud mode: Authenticate via OAuth`);
2571
- }
2572
- const url = fileUrl || getCurrentUrl();
2573
- if (!url) {
2574
- throw new Error("No Figma file URL provided. Either pass fileUrl parameter or call figma_navigate first.");
2575
- }
2576
- const fileKey = extractFileKey(url);
2577
- if (!fileKey) {
2578
- throw new Error(`Invalid Figma URL: ${url}`);
2579
- }
2580
- logger.info({ fileKey, depth, nodeIds }, "Fetching file data for plugin development");
2581
- const fileData = await api.getFile(fileKey, {
2582
- depth,
2583
- ids: nodeIds,
2584
- });
2585
- // Filter to plugin-relevant properties only
2586
- const filterForPlugin = (node) => {
2587
- if (!node)
2588
- return node;
2589
- const result = {
2590
- id: node.id,
2591
- name: node.name,
2592
- type: node.type,
2593
- description: node.description,
2594
- descriptionMarkdown: node.descriptionMarkdown,
2595
- };
2596
- // Navigation & structure
2597
- if (node.visible !== undefined)
2598
- result.visible = node.visible;
2599
- if (node.locked)
2600
- result.locked = node.locked;
2601
- if (node.removed)
2602
- result.removed = node.removed;
2603
- // Lightweight bounds (just position/size)
2604
- if (node.absoluteBoundingBox) {
2605
- result.bounds = {
2606
- x: node.absoluteBoundingBox.x,
2607
- y: node.absoluteBoundingBox.y,
2608
- width: node.absoluteBoundingBox.width,
2609
- height: node.absoluteBoundingBox.height,
2610
- };
2611
- }
2612
- // Plugin data (CRITICAL for plugins)
2613
- if (node.pluginData)
2614
- result.pluginData = node.pluginData;
2615
- if (node.sharedPluginData)
2616
- result.sharedPluginData = node.sharedPluginData;
2617
- // Component relationships (important for plugins)
2618
- if (node.componentId)
2619
- result.componentId = node.componentId;
2620
- if (node.mainComponent)
2621
- result.mainComponent = node.mainComponent;
2622
- if (node.componentPropertyReferences)
2623
- result.componentPropertyReferences = node.componentPropertyReferences;
2624
- if (node.instanceOf)
2625
- result.instanceOf = node.instanceOf;
2626
- if (node.exposedInstances)
2627
- result.exposedInstances = node.exposedInstances;
2628
- // Component properties (for manipulation)
2629
- if (node.componentProperties)
2630
- result.componentProperties = node.componentProperties;
2631
- // Characters for text nodes (plugins often need this)
2632
- if (node.characters !== undefined)
2633
- result.characters = node.characters;
2634
- // Recursively process children
2635
- if (node.children) {
2636
- result.children = node.children.map((child) => filterForPlugin(child));
2637
- }
2638
- return result;
2639
- };
2640
- const filteredDocument = filterForPlugin(fileData.document);
2641
- const finalResponse = {
2642
- fileKey,
2643
- name: fileData.name,
2644
- lastModified: fileData.lastModified,
2645
- version: fileData.version,
2646
- document: filteredDocument,
2647
- components: fileData.components
2648
- ? Object.keys(fileData.components).length
2649
- : 0,
2650
- styles: fileData.styles
2651
- ? Object.keys(fileData.styles).length
2652
- : 0,
2653
- ...(nodeIds && {
2654
- requestedNodes: nodeIds,
2655
- nodes: fileData.nodes,
2656
- }),
2657
- metadata: {
2658
- purpose: "plugin_development",
2659
- note: "Optimized for plugin development. Contains IDs, structure, plugin data, and component relationships.",
2660
- },
2661
- };
2662
- // Use adaptive response to prevent context exhaustion
2663
- return adaptiveResponse(finalResponse, {
2664
- toolName: "figma_get_file_for_plugin",
2665
- compressionCallback: (adjustedLevel) => {
2666
- // For plugin format, we can't reduce much without breaking functionality
2667
- // But we can strip some less critical metadata
2668
- const compressNode = (node) => {
2669
- const result = {
2670
- id: node.id,
2671
- name: node.name,
2672
- type: node.type,
2673
- };
2674
- // Keep only essential properties based on compression level
2675
- if (adjustedLevel !== "inventory") {
2676
- if (node.visible !== undefined)
2677
- result.visible = node.visible;
2678
- if (node.locked !== undefined)
2679
- result.locked = node.locked;
2680
- if (node.absoluteBoundingBox)
2681
- result.absoluteBoundingBox = node.absoluteBoundingBox;
2682
- if (node.pluginData)
2683
- result.pluginData = node.pluginData;
2684
- if (node.sharedPluginData)
2685
- result.sharedPluginData = node.sharedPluginData;
2686
- if (node.componentId)
2687
- result.componentId = node.componentId;
2688
- }
2689
- if (node.children) {
2690
- result.children = node.children.map(compressNode);
2691
- }
2692
- return result;
2693
- };
2694
- return {
2695
- ...finalResponse,
2696
- document: compressNode(filteredDocument),
2697
- metadata: {
2698
- ...finalResponse.metadata,
2699
- compressionApplied: adjustedLevel,
2700
- },
2701
- };
2702
- },
2703
- suggestedActions: [
2704
- "Reduce depth parameter (recommend 1-2)",
2705
- "Request specific nodeIds to narrow the scope",
2706
- "Filter to specific component types if possible",
2707
- ],
2708
- });
2709
- }
2710
- catch (error) {
2711
- logger.error({ error }, "Failed to get file for plugin");
2712
- const errorMessage = error instanceof Error ? error.message : String(error);
2713
- return {
2714
- content: [
2715
- {
2716
- type: "text",
2717
- text: JSON.stringify({
2718
- error: errorMessage,
2719
- message: "Failed to retrieve file data for plugin development",
2720
- }, null, 2),
2721
- },
2722
- ],
2723
- isError: true,
2724
- };
2725
- }
2726
- });
2727
- // Tool 15: Capture Screenshot via Plugin (F-MCP ATezer Bridge)
2728
- // This uses exportAsync() which reads the current plugin runtime state, not the cloud state
2729
- // Solves race condition where REST API screenshots show stale data after changes
2730
- server.registerTool("figma_capture_screenshot", {
2731
- description: "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 F-MCP ATezer Bridge connection (Figma Desktop with plugin running).",
2732
- inputSchema: {
2733
- nodeId: z
2734
- .string()
2735
- .optional()
2736
- .describe("ID of the node to capture (e.g., '1:234'). If not provided, captures the current page."),
2737
- format: z
2738
- .enum(["PNG", "JPG", "SVG"])
2739
- .optional()
2740
- .default("PNG")
2741
- .describe("Image format (default: PNG)"),
2742
- scale: z
2743
- .number()
2744
- .min(0.5)
2745
- .max(4)
2746
- .optional()
2747
- .default(2)
2748
- .describe("Scale factor (default: 2 for 2x resolution)"),
2749
- },
2750
- annotations: { readOnlyHint: true },
2751
- }, async ({ nodeId, format, scale }) => {
2752
- try {
2753
- // Prefer connector (works with Plugin Bridge WebSocket, no CDP needed)
2754
- const connector = getDesktopConnector ? await getDesktopConnector().catch(() => null) : null;
2755
- let result = null;
2756
- if (connector && typeof connector.captureScreenshot === "function") {
2757
- logger.info({ nodeId, format, scale }, "Capturing screenshot via connector (bridge or CDP)");
2758
- result = await connector.captureScreenshot(nodeId ?? null, { format, scale });
2759
- }
2760
- const browserManager = getBrowserManager?.();
2761
- if (!result && browserManager) {
2762
- // Fallback: CDP path with page/frames
2763
- if (ensureInitialized)
2764
- await ensureInitialized();
2765
- const page = await browserManager.getPage();
2766
- logger.info({ nodeId, format, scale }, "Capturing screenshot via F-MCP ATezer Bridge (CDP)");
2767
- const frames = page.frames();
2768
- for (const frame of frames) {
2769
- try {
2770
- const hasFunction = await frame.evaluate('typeof window.captureScreenshot === "function"');
2771
- if (hasFunction) {
2772
- result = await frame.evaluate(`window.captureScreenshot(${JSON.stringify(nodeId)}, ${JSON.stringify({ format, scale })})`);
2773
- break;
2774
- }
2775
- }
2776
- catch {
2777
- continue;
2778
- }
2779
- }
2780
- }
2781
- if (!result) {
2782
- throw new Error("F-MCP ATezer Bridge not available. Open Figma, run the 'F-MCP ATezer Bridge' plugin (Plugins → Development). " +
2783
- "No debug port needed when plugin connects via WebSocket.");
2784
- }
2785
- if (!result.success) {
2786
- throw new Error(result.error || "Screenshot capture failed");
2787
- }
2788
- return {
2789
- content: [
2790
- {
2791
- type: "text",
2792
- text: JSON.stringify({
2793
- success: true,
2794
- image: {
2795
- format: result.image.format,
2796
- scale: result.image.scale,
2797
- byteLength: result.image.byteLength,
2798
- node: result.image.node,
2799
- bounds: result.image.bounds,
2800
- // Base64 data is included but may be large
2801
- base64: result.image.base64,
2802
- },
2803
- metadata: {
2804
- source: "plugin_export_async",
2805
- note: "Screenshot captured from plugin runtime state (guaranteed current). Use base64 data to verify visual changes.",
2806
- },
2807
- }, null, 2),
2808
- },
2809
- ],
2810
- };
2811
- }
2812
- catch (error) {
2813
- logger.error({ error }, "Failed to capture screenshot");
2814
- const errorMessage = error instanceof Error ? error.message : String(error);
2815
- return {
2816
- content: [
2817
- {
2818
- type: "text",
2819
- text: JSON.stringify({
2820
- error: errorMessage,
2821
- message: "Failed to capture screenshot via F-MCP ATezer Bridge",
2822
- suggestion: "Ensure Figma Desktop is open with the plugin running",
2823
- }, null, 2),
2824
- },
2825
- ],
2826
- isError: true,
2827
- };
2828
- }
2829
- });
2830
- // Tool 16: Set Instance Properties (F-MCP ATezer Bridge)
2831
- // Updates component properties on an instance using setProperties()
2832
- // This is the correct way to update TEXT/BOOLEAN/VARIANT properties on component instances
2833
- server.registerTool("figma_set_instance_properties", {
2834
- description: "Update component properties on a component instance. IMPORTANT: Use this tool instead of trying to edit text nodes directly when working with component instances. Components often expose TEXT, BOOLEAN, INSTANCE_SWAP, and VARIANT properties that control their content. Direct text node editing may fail silently if the component uses properties. This tool handles the #nodeId suffix pattern automatically. Requires F-MCP ATezer Bridge connection.",
2835
- inputSchema: {
2836
- nodeId: z
2837
- .string()
2838
- .describe("ID of the INSTANCE node to update (e.g., '1:234'). Must be a component instance, not a regular frame."),
2839
- properties: z
2840
- .record(z.string(), z.union([z.string(), z.boolean()]))
2841
- .describe("Properties to set. Keys are property names (e.g., 'Label', 'Show Icon', 'Size'). " +
2842
- "Values are strings for TEXT/VARIANT properties, booleans for BOOLEAN properties. " +
2843
- "The tool automatically handles the #nodeId suffix for TEXT/BOOLEAN/INSTANCE_SWAP properties."),
2844
- },
2845
- annotations: { destructiveHint: true },
2846
- }, async ({ nodeId, properties }) => {
2847
- try {
2848
- let result = null;
2849
- const connector = getDesktopConnector ? await getDesktopConnector().catch(() => null) : null;
2850
- if (connector && typeof connector.setInstanceProperties === "function") {
2851
- logger.info({ nodeId, properties: Object.keys(properties) }, "Setting instance properties via connector");
2852
- result = await connector.setInstanceProperties(nodeId, properties);
2853
- }
2854
- const browserManagerSet = getBrowserManager?.();
2855
- if (!result && browserManagerSet) {
2856
- if (ensureInitialized)
2857
- await ensureInitialized();
2858
- const page = await browserManagerSet.getPage();
2859
- logger.info({ nodeId, properties: Object.keys(properties) }, "Setting instance properties via F-MCP ATezer Bridge (CDP)");
2860
- const frames = page.frames();
2861
- for (const frame of frames) {
2862
- try {
2863
- const hasFunction = await frame.evaluate('typeof window.setInstanceProperties === "function"');
2864
- if (hasFunction) {
2865
- result = await frame.evaluate(`window.setInstanceProperties(${JSON.stringify(nodeId)}, ${JSON.stringify(properties)})`);
2866
- break;
2867
- }
2868
- }
2869
- catch {
2870
- continue;
2871
- }
2872
- }
2873
- }
2874
- if (!result) {
2875
- throw new Error("F-MCP ATezer Bridge not available. Open Figma, run the 'F-MCP ATezer Bridge' plugin (Plugins → Development).");
2876
- }
2877
- if (!result.success) {
2878
- throw new Error(result.error || "Failed to set instance properties");
2879
- }
2880
- return {
2881
- content: [
2882
- {
2883
- type: "text",
2884
- text: JSON.stringify({
2885
- success: true,
2886
- instance: result.instance,
2887
- metadata: {
2888
- note: "Instance properties updated successfully. Use figma_capture_screenshot to verify visual changes.",
2889
- },
2890
- }, null, 2),
2891
- },
2892
- ],
2893
- };
2894
- }
2895
- catch (error) {
2896
- logger.error({ error }, "Failed to set instance properties");
2897
- const errorMessage = error instanceof Error ? error.message : String(error);
2898
- return {
2899
- content: [
2900
- {
2901
- type: "text",
2902
- text: JSON.stringify({
2903
- error: errorMessage,
2904
- message: "Failed to set instance properties via F-MCP ATezer Bridge",
2905
- suggestions: [
2906
- "Verify the node is a component INSTANCE (not a regular frame)",
2907
- "Check available properties with figma_get_component first",
2908
- "Ensure property names match exactly (case-sensitive)",
2909
- "For TEXT properties, provide string values",
2910
- "For BOOLEAN properties, provide true/false",
2911
- ],
2912
- }, null, 2),
2913
- },
2914
- ],
2915
- isError: true,
2916
- };
2917
- }
2918
- });
2919
- }
2920
- //# sourceMappingURL=figma-tools.js.map