@muloka/figma-console-mcp 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +3 -1
  2. package/dist/core/port-discovery.d.ts +9 -0
  3. package/dist/core/port-discovery.d.ts.map +1 -1
  4. package/dist/core/port-discovery.js +40 -14
  5. package/dist/core/port-discovery.js.map +1 -1
  6. package/dist/core/websocket-connector.d.ts +2 -1
  7. package/dist/core/websocket-connector.d.ts.map +1 -1
  8. package/dist/core/websocket-connector.js +17 -2
  9. package/dist/core/websocket-connector.js.map +1 -1
  10. package/dist/core/websocket-server.d.ts +8 -1
  11. package/dist/core/websocket-server.d.ts.map +1 -1
  12. package/dist/core/websocket-server.js +18 -2
  13. package/dist/core/websocket-server.js.map +1 -1
  14. package/dist/core/write-tools.d.ts +3 -1
  15. package/dist/core/write-tools.d.ts.map +1 -1
  16. package/dist/core/write-tools.js +34 -14
  17. package/dist/core/write-tools.js.map +1 -1
  18. package/dist/local.d.ts +2 -1
  19. package/dist/local.d.ts.map +1 -1
  20. package/dist/local.js +10 -14
  21. package/dist/local.js.map +1 -1
  22. package/package.json +1 -1
  23. package/dist/apps/design-system-dashboard/mcp-app.html +0 -521
  24. package/dist/apps/token-browser/mcp-app.html +0 -410
  25. package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +0 -277
  26. package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +0 -357
  27. package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +0 -341
  28. package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +0 -230
  29. package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +0 -92
  30. package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +0 -308
  31. package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +0 -349
  32. package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +0 -40
  33. package/dist/cloudflare/apps/design-system-dashboard/server.js +0 -159
  34. package/dist/cloudflare/apps/token-browser/server.js +0 -136
  35. package/dist/cloudflare/browser-manager.js +0 -157
  36. package/dist/cloudflare/core/accessibility-tools.js +0 -361
  37. package/dist/cloudflare/core/annotation-tools.js +0 -230
  38. package/dist/cloudflare/core/cloud-websocket-connector.js +0 -419
  39. package/dist/cloudflare/core/cloud-websocket-relay.js +0 -198
  40. package/dist/cloudflare/core/comment-tools.js +0 -292
  41. package/dist/cloudflare/core/config.js +0 -153
  42. package/dist/cloudflare/core/console-monitor.js +0 -427
  43. package/dist/cloudflare/core/deep-component-tools.js +0 -128
  44. package/dist/cloudflare/core/design-code-tools.js +0 -2793
  45. package/dist/cloudflare/core/design-system-manifest.js +0 -265
  46. package/dist/cloudflare/core/design-system-tools.js +0 -882
  47. package/dist/cloudflare/core/diagnose-tool.js +0 -100
  48. package/dist/cloudflare/core/diff/changelog-formatter.js +0 -275
  49. package/dist/cloudflare/core/diff/diff-engine.js +0 -334
  50. package/dist/cloudflare/core/diff/property-compare.js +0 -36
  51. package/dist/cloudflare/core/diff/version-cache.js +0 -74
  52. package/dist/cloudflare/core/enrichment/enrichment-service.js +0 -374
  53. package/dist/cloudflare/core/enrichment/index.js +0 -7
  54. package/dist/cloudflare/core/enrichment/relationship-mapper.js +0 -351
  55. package/dist/cloudflare/core/enrichment/style-resolver.js +0 -346
  56. package/dist/cloudflare/core/figjam-tools.js +0 -547
  57. package/dist/cloudflare/core/figma-api.js +0 -509
  58. package/dist/cloudflare/core/figma-connector.js +0 -7
  59. package/dist/cloudflare/core/figma-reconstruction-spec.js +0 -402
  60. package/dist/cloudflare/core/figma-style-extractor.js +0 -311
  61. package/dist/cloudflare/core/figma-tools.js +0 -3353
  62. package/dist/cloudflare/core/identity.js +0 -96
  63. package/dist/cloudflare/core/library-tools.js +0 -580
  64. package/dist/cloudflare/core/logger.js +0 -53
  65. package/dist/cloudflare/core/port-discovery.js +0 -935
  66. package/dist/cloudflare/core/resolve-package-root.js +0 -11
  67. package/dist/cloudflare/core/slides-tools.js +0 -714
  68. package/dist/cloudflare/core/slot-tools.js +0 -334
  69. package/dist/cloudflare/core/snippet-injector.js +0 -96
  70. package/dist/cloudflare/core/tokens/alias-resolver.js +0 -205
  71. package/dist/cloudflare/core/tokens/config.js +0 -294
  72. package/dist/cloudflare/core/tokens/dialect.js +0 -232
  73. package/dist/cloudflare/core/tokens/figma-converter.js +0 -323
  74. package/dist/cloudflare/core/tokens/formatters/css-vars.js +0 -338
  75. package/dist/cloudflare/core/tokens/formatters/dtcg.js +0 -376
  76. package/dist/cloudflare/core/tokens/formatters/index.js +0 -45
  77. package/dist/cloudflare/core/tokens/formatters/json.js +0 -205
  78. package/dist/cloudflare/core/tokens/formatters/less.js +0 -4
  79. package/dist/cloudflare/core/tokens/formatters/scss.js +0 -258
  80. package/dist/cloudflare/core/tokens/formatters/stubs.js +0 -13
  81. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +0 -213
  82. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +0 -237
  83. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +0 -335
  84. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +0 -256
  85. package/dist/cloudflare/core/tokens/formatters/ts-module.js +0 -198
  86. package/dist/cloudflare/core/tokens/index.js +0 -16
  87. package/dist/cloudflare/core/tokens/parsers/css-vars.js +0 -4
  88. package/dist/cloudflare/core/tokens/parsers/dtcg.js +0 -280
  89. package/dist/cloudflare/core/tokens/parsers/index.js +0 -138
  90. package/dist/cloudflare/core/tokens/parsers/json.js +0 -7
  91. package/dist/cloudflare/core/tokens/parsers/scss.js +0 -4
  92. package/dist/cloudflare/core/tokens/parsers/stubs.js +0 -20
  93. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +0 -4
  94. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +0 -4
  95. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +0 -4
  96. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +0 -4
  97. package/dist/cloudflare/core/tokens/schemas.js +0 -152
  98. package/dist/cloudflare/core/tokens/transforms/color.js +0 -12
  99. package/dist/cloudflare/core/tokens/transforms/index.js +0 -29
  100. package/dist/cloudflare/core/tokens/transforms/size.js +0 -7
  101. package/dist/cloudflare/core/tokens/types.js +0 -18
  102. package/dist/cloudflare/core/tokens-tools.js +0 -1788
  103. package/dist/cloudflare/core/types/design-code.js +0 -4
  104. package/dist/cloudflare/core/types/enriched.js +0 -5
  105. package/dist/cloudflare/core/types/index.js +0 -4
  106. package/dist/cloudflare/core/variable-resolver.js +0 -85
  107. package/dist/cloudflare/core/version-tools.js +0 -1199
  108. package/dist/cloudflare/core/websocket-connector.js +0 -436
  109. package/dist/cloudflare/core/websocket-server.js +0 -1009
  110. package/dist/cloudflare/core/write-tools.js +0 -2506
  111. package/dist/cloudflare/index.js +0 -3010
@@ -1,882 +0,0 @@
1
- /**
2
- * Design System Kit Tool
3
- * MCP tool that orchestrates existing Figma API tools to produce a structured
4
- * design system specification — tokens, components, styles — in a single call.
5
- *
6
- * This enables AI code generation tools (Figma Make, v0, Cursor, Claude, etc.)
7
- * to generate code with structural fidelity to the real design system.
8
- */
9
- import { z } from "zod";
10
- import { extractFileKey } from "./figma-api.js";
11
- import { resolveFormattedVariables } from "./variable-resolver.js";
12
- import { createChildLogger } from "./logger.js";
13
- const logger = createChildLogger({ component: "design-system-tools" });
14
- // ============================================================================
15
- // Helpers
16
- // ============================================================================
17
- /**
18
- * Calculate JSON size in KB for response management
19
- */
20
- function calculateSizeKB(data) {
21
- return JSON.stringify(data).length / 1024;
22
- }
23
- /**
24
- * Wrap a promise with a timeout to prevent indefinite hangs
25
- */
26
- function withTimeout(promise, ms, label) {
27
- return Promise.race([
28
- promise,
29
- new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
30
- ]);
31
- }
32
- /**
33
- * Convert Figma RGBA (0-1 range) to hex string
34
- */
35
- function rgbaToHex(color) {
36
- const r = Math.round(color.r * 255);
37
- const g = Math.round(color.g * 255);
38
- const b = Math.round(color.b * 255);
39
- const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
40
- return hex.toUpperCase();
41
- }
42
- /**
43
- * Extract a compact visual specification from a Figma node.
44
- * Captures the essential CSS-equivalent properties an AI needs to reproduce the component.
45
- */
46
- export function extractVisualSpec(node) {
47
- if (!node)
48
- return undefined;
49
- const spec = {};
50
- let hasData = false;
51
- // Fills → background colors/gradients
52
- if (node.fills && Array.isArray(node.fills) && node.fills.length > 0) {
53
- spec.fills = node.fills
54
- .filter((f) => f.visible !== false)
55
- .map((f) => {
56
- const fill = { type: f.type };
57
- if (f.color)
58
- fill.color = rgbaToHex(f.color);
59
- if (f.opacity !== undefined)
60
- fill.opacity = f.opacity;
61
- return fill;
62
- });
63
- if (spec.fills.length > 0)
64
- hasData = true;
65
- }
66
- // Strokes → borders
67
- if (node.strokes && Array.isArray(node.strokes) && node.strokes.length > 0) {
68
- spec.strokes = node.strokes
69
- .filter((s) => s.visible !== false)
70
- .map((s) => {
71
- const stroke = { type: s.type };
72
- if (s.color)
73
- stroke.color = rgbaToHex(s.color);
74
- return stroke;
75
- });
76
- if (node.strokeWeight !== undefined)
77
- spec.strokes.forEach((s) => s.weight = node.strokeWeight);
78
- if (node.strokeAlign)
79
- spec.strokes.forEach((s) => s.align = node.strokeAlign);
80
- if (spec.strokes.length > 0)
81
- hasData = true;
82
- }
83
- // Effects → shadows, blurs
84
- if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
85
- spec.effects = node.effects
86
- .filter((e) => e.visible !== false)
87
- .map((e) => {
88
- const effect = { type: e.type };
89
- if (e.color)
90
- effect.color = rgbaToHex(e.color);
91
- if (e.offset)
92
- effect.offset = e.offset;
93
- if (e.radius !== undefined)
94
- effect.radius = e.radius;
95
- if (e.spread !== undefined)
96
- effect.spread = e.spread;
97
- return effect;
98
- });
99
- if (spec.effects.length > 0)
100
- hasData = true;
101
- }
102
- // Corner radius
103
- if (node.cornerRadius !== undefined && node.cornerRadius > 0) {
104
- spec.cornerRadius = node.cornerRadius;
105
- hasData = true;
106
- }
107
- if (node.rectangleCornerRadii) {
108
- spec.rectangleCornerRadii = node.rectangleCornerRadii;
109
- hasData = true;
110
- }
111
- // Opacity
112
- if (node.opacity !== undefined && node.opacity < 1) {
113
- spec.opacity = node.opacity;
114
- hasData = true;
115
- }
116
- // Auto-layout → CSS flex equivalent
117
- if (node.layoutMode && node.layoutMode !== "NONE") {
118
- spec.layout = {
119
- mode: node.layoutMode,
120
- };
121
- if (node.paddingTop !== undefined)
122
- spec.layout.paddingTop = node.paddingTop;
123
- if (node.paddingRight !== undefined)
124
- spec.layout.paddingRight = node.paddingRight;
125
- if (node.paddingBottom !== undefined)
126
- spec.layout.paddingBottom = node.paddingBottom;
127
- if (node.paddingLeft !== undefined)
128
- spec.layout.paddingLeft = node.paddingLeft;
129
- if (node.itemSpacing !== undefined)
130
- spec.layout.itemSpacing = node.itemSpacing;
131
- if (node.primaryAxisAlignItems)
132
- spec.layout.primaryAxisAlign = node.primaryAxisAlignItems;
133
- if (node.counterAxisAlignItems)
134
- spec.layout.counterAxisAlign = node.counterAxisAlignItems;
135
- hasData = true;
136
- }
137
- // Typography (for TEXT nodes)
138
- if (node.type === "TEXT" && node.style) {
139
- spec.typography = {};
140
- const s = node.style;
141
- if (s.fontFamily)
142
- spec.typography.fontFamily = s.fontFamily;
143
- if (s.fontSize)
144
- spec.typography.fontSize = s.fontSize;
145
- if (s.fontWeight)
146
- spec.typography.fontWeight = s.fontWeight;
147
- if (s.lineHeightPx)
148
- spec.typography.lineHeight = s.lineHeightPx;
149
- if (s.letterSpacing)
150
- spec.typography.letterSpacing = s.letterSpacing;
151
- if (s.textAlignHorizontal)
152
- spec.typography.textAlignHorizontal = s.textAlignHorizontal;
153
- hasData = true;
154
- }
155
- return hasData ? spec : undefined;
156
- }
157
- /**
158
- * Delta-encode variant visual specs against the first variant that has one.
159
- * Variants in a set share most visual properties, so repeating the full spec
160
- * on every variant inflates the payload ~30x for large sets. The base variant
161
- * keeps its full visualSpec; every other variant gets a visualSpecDelta with
162
- * only the top-level properties that differ from the base (null means the
163
- * property exists on the base but not on this variant). Variants identical to
164
- * the base carry neither field.
165
- */
166
- function deltaEncodeVariantSpecs(variants) {
167
- const base = variants.find((v) => v.visualSpec);
168
- if (!base)
169
- return;
170
- const baseSpec = base.visualSpec;
171
- for (const variant of variants) {
172
- if (variant === base || !variant.visualSpec)
173
- continue;
174
- const spec = variant.visualSpec;
175
- const delta = {};
176
- for (const key of new Set([...Object.keys(baseSpec), ...Object.keys(spec)])) {
177
- if (!(key in spec)) {
178
- delta[key] = null;
179
- }
180
- else if (!(key in baseSpec) || JSON.stringify(spec[key]) !== JSON.stringify(baseSpec[key])) {
181
- delta[key] = spec[key];
182
- }
183
- }
184
- delete variant.visualSpec;
185
- if (Object.keys(delta).length > 0)
186
- variant.visualSpecDelta = delta;
187
- }
188
- }
189
- /**
190
- * Resolve style node IDs to their actual visual values.
191
- * Styles only contain metadata from the styles endpoint — we need getNodes to get actual colors/fonts/effects.
192
- */
193
- async function resolveStyleValues(api, fileKey, styles) {
194
- const resolved = new Map();
195
- const nodeIds = styles.filter((s) => s.nodeId).map((s) => s.nodeId);
196
- if (nodeIds.length === 0)
197
- return resolved;
198
- try {
199
- const batchSize = 50;
200
- for (let i = 0; i < nodeIds.length; i += batchSize) {
201
- const batch = nodeIds.slice(i, i + batchSize);
202
- const nodeResponse = await withTimeout(api.getNodes(fileKey, batch), 30000, `getStyleNodes(batch ${Math.floor(i / batchSize) + 1})`);
203
- if (nodeResponse?.nodes) {
204
- for (const [nodeId, nodeData] of Object.entries(nodeResponse.nodes)) {
205
- const doc = nodeData?.document;
206
- if (!doc)
207
- continue;
208
- const value = {};
209
- // FILL styles → extract colors
210
- if (doc.fills && Array.isArray(doc.fills)) {
211
- value.fills = doc.fills
212
- .filter((f) => f.visible !== false)
213
- .map((f) => ({
214
- type: f.type,
215
- color: f.color ? rgbaToHex(f.color) : undefined,
216
- opacity: f.opacity,
217
- }));
218
- }
219
- // TEXT styles → extract typography
220
- if (doc.type === "TEXT" && doc.style) {
221
- value.typography = {
222
- fontFamily: doc.style.fontFamily,
223
- fontSize: doc.style.fontSize,
224
- fontWeight: doc.style.fontWeight,
225
- lineHeight: doc.style.lineHeightPx,
226
- letterSpacing: doc.style.letterSpacing,
227
- };
228
- }
229
- // EFFECT styles → extract shadows/blurs
230
- if (doc.effects && Array.isArray(doc.effects)) {
231
- value.effects = doc.effects
232
- .filter((e) => e.visible !== false)
233
- .map((e) => ({
234
- type: e.type,
235
- color: e.color ? rgbaToHex(e.color) : undefined,
236
- offset: e.offset,
237
- radius: e.radius,
238
- spread: e.spread,
239
- }));
240
- }
241
- resolved.set(nodeId, value);
242
- }
243
- }
244
- }
245
- }
246
- catch (err) {
247
- logger.warn({ error: err }, "Failed to resolve style values");
248
- }
249
- return resolved;
250
- }
251
- /**
252
- * Group variables by collection for a clean hierarchical output
253
- */
254
- function groupVariablesByCollection(formatted) {
255
- return formatted.collections.map((collection) => {
256
- const collectionVars = formatted.variables
257
- .filter((v) => v.variableCollectionId === collection.id)
258
- .map((v) => ({
259
- id: v.id,
260
- name: v.name,
261
- type: v.resolvedType,
262
- description: v.description || undefined,
263
- valuesByMode: v.valuesByMode,
264
- scopes: v.scopes,
265
- }));
266
- return {
267
- id: collection.id,
268
- name: collection.name,
269
- modes: collection.modes,
270
- variables: collectionVars,
271
- };
272
- });
273
- }
274
- /**
275
- * Deduplicate components — filter out individual variants when their
276
- * parent component set is already present.
277
- */
278
- function deduplicateComponents(components, componentSets) {
279
- const setNodeIds = new Set(componentSets.map((s) => s.node_id));
280
- // Filter out variants that belong to a known component set
281
- const standalone = components.filter((c) => {
282
- if (c.containing_frame?.containingComponentSet) {
283
- // This is a variant — check if parent set is already included
284
- // Check both direct frame nodeId and the containingComponentSet.nodeId
285
- // (some designs nest variants inside intermediate frames)
286
- const frameNodeId = c.containing_frame?.nodeId;
287
- const setNodeId = c.containing_frame?.containingComponentSet?.nodeId;
288
- if ((frameNodeId && setNodeIds.has(frameNodeId)) ||
289
- (setNodeId && setNodeIds.has(setNodeId))) {
290
- return false; // Skip, parent set covers it
291
- }
292
- }
293
- return true;
294
- });
295
- return { components: standalone, componentSets };
296
- }
297
- /**
298
- * Compress the kit for large responses
299
- */
300
- function compressKit(kit, level) {
301
- const compressed = { ...kit };
302
- if (compressed.tokens) {
303
- if (level === "compact") {
304
- // Compact: only summary counts, drop all collections/variables
305
- compressed.tokens = {
306
- collections: [],
307
- summary: compressed.tokens.summary,
308
- };
309
- }
310
- else if (level === "inventory") {
311
- // Only keep variable names and types, drop values
312
- compressed.tokens = {
313
- ...compressed.tokens,
314
- collections: compressed.tokens.collections.map((c) => ({
315
- ...c,
316
- variables: c.variables.map((v) => ({
317
- id: v.id,
318
- name: v.name,
319
- type: v.type,
320
- description: v.description,
321
- valuesByMode: {}, // Strip values
322
- scopes: v.scopes,
323
- })),
324
- })),
325
- };
326
- }
327
- }
328
- if (compressed.components) {
329
- if (level === "compact") {
330
- // Compact: drastically reduce for large systems
331
- // Separate component sets (design building blocks) from standalone components
332
- const sets = compressed.components.items.filter((c) => c.variants && c.variants.length > 0);
333
- const standalone = compressed.components.items.filter((c) => !c.variants || c.variants.length === 0);
334
- // Keep all sets (they're the main building blocks), limit standalone to 100
335
- const limitedStandalone = standalone.slice(0, 100);
336
- const trimmedItems = [...sets, ...limitedStandalone];
337
- compressed.components = {
338
- ...compressed.components,
339
- items: trimmedItems.map((c) => ({
340
- id: c.id,
341
- name: c.name,
342
- // Compact: variant count only (not individual names) for very large sets
343
- variants: c.variants
344
- ? c.variants.length > 10
345
- ? [{ name: `${c.variants.length} variants`, id: "" }]
346
- : c.variants.map((v) => ({ name: v.name, id: v.id }))
347
- : undefined,
348
- properties: c.properties
349
- ? Object.fromEntries(Object.entries(c.properties).map(([k, v]) => [
350
- k,
351
- { type: v.type, defaultValue: v.defaultValue },
352
- ]))
353
- : undefined,
354
- })),
355
- summary: {
356
- ...compressed.components.summary,
357
- totalComponents: trimmedItems.length,
358
- ...(standalone.length > 100 ? { omittedStandaloneComponents: standalone.length - 100 } : {}),
359
- },
360
- };
361
- }
362
- else if (level === "inventory") {
363
- // Only keep names and property keys — strip visual specs and variants
364
- compressed.components = {
365
- ...compressed.components,
366
- items: compressed.components.items.map((c) => ({
367
- id: c.id,
368
- name: c.name,
369
- description: c.description,
370
- properties: c.properties
371
- ? Object.fromEntries(Object.entries(c.properties).map(([k, v]) => [
372
- k,
373
- { type: v.type, defaultValue: v.defaultValue },
374
- ]))
375
- : undefined,
376
- })),
377
- };
378
- }
379
- else if (level === "summary") {
380
- // Keep visual specs but strip variant-level specs to save space
381
- compressed.components = {
382
- ...compressed.components,
383
- items: compressed.components.items.map((c) => ({
384
- ...c,
385
- variants: c.variants?.map((v) => ({ name: v.name, id: v.id })),
386
- })),
387
- };
388
- }
389
- // Drop image URLs at any compression level to save tokens
390
- compressed.components.items = compressed.components.items.map((c) => {
391
- const { imageUrl, ...rest } = c;
392
- return rest;
393
- });
394
- }
395
- if (compressed.styles) {
396
- if (level === "compact") {
397
- // Compact: only style names and types grouped by type, no resolved values
398
- compressed.styles = {
399
- ...compressed.styles,
400
- items: compressed.styles.items.map((s) => ({
401
- key: s.key,
402
- name: s.name,
403
- styleType: s.styleType,
404
- })),
405
- };
406
- }
407
- else if (level === "inventory") {
408
- // Strip resolved values in inventory mode
409
- compressed.styles = {
410
- ...compressed.styles,
411
- items: compressed.styles.items.map((s) => ({
412
- key: s.key,
413
- name: s.name,
414
- styleType: s.styleType,
415
- description: s.description,
416
- })),
417
- };
418
- }
419
- }
420
- return compressed;
421
- }
422
- // ============================================================================
423
- // Tool Registration
424
- // ============================================================================
425
- export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, variablesCache, options, getDesktopConnector) {
426
- server.tool("figma_get_design_system_kit", "PREFERRED TOOL for design system extraction — replaces separate figma_get_styles, figma_get_variables, and figma_get_component calls. " +
427
- "Returns tokens, components, and styles in a single optimized response with adaptive compression for large systems. " +
428
- "Includes component visual specs (exact colors, padding, typography, layout), rendered screenshots, " +
429
- "token values per mode (light/dark), and resolved style values. " +
430
- "Use this instead of calling individual tools to avoid context window overflow. " +
431
- "Ideal for AI code generation — use visualSpec for pixel-accurate reproduction. " +
432
- "Variant specs are delta-encoded: the base variant carries the full visualSpec, siblings carry visualSpecDelta with only the properties that differ. " +
433
- "Tokens/variables are read through the connected Desktop Bridge or cloud relay and work on ANY Figma plan — no Enterprise required. " +
434
- "If a tokens fetch ever reports the Variables REST API is plan-limited (403), the bridge/relay is the plan-independent path: ensure it's connected and retry rather than abandoning variables.", {
435
- fileKey: z
436
- .string()
437
- .optional()
438
- .describe("Figma file key. If omitted, extracted from the current browser URL."),
439
- include: z
440
- .array(z.enum(["tokens", "components", "styles"]))
441
- .optional()
442
- .default(["tokens", "components", "styles"])
443
- .describe("Which sections to include. Defaults to all."),
444
- componentIds: z
445
- .array(z.string())
446
- .optional()
447
- .describe("Optional list of specific component node IDs to include. If omitted, all published components are returned."),
448
- includeImages: z
449
- .boolean()
450
- .optional()
451
- .default(false)
452
- .describe("Include image URLs for components (adds latency). Default false."),
453
- format: z
454
- .enum(["full", "summary", "compact"])
455
- .optional()
456
- .default("full")
457
- .describe("'full' returns complete data with visual specs and resolved values. " +
458
- "'summary' strips variant-level visual specs (medium payload). " +
459
- "'compact' returns only names, types, and property definitions (smallest payload, best for large design systems). " +
460
- "Auto-compresses if response exceeds safe size regardless of format setting."),
461
- }, async ({ fileKey, include, componentIds, includeImages, format }) => {
462
- try {
463
- const api = await getFigmaAPI();
464
- // Resolve file key
465
- let resolvedFileKey = fileKey;
466
- if (!resolvedFileKey) {
467
- const currentUrl = getCurrentUrl();
468
- if (currentUrl) {
469
- resolvedFileKey = extractFileKey(currentUrl) || undefined;
470
- }
471
- }
472
- if (!resolvedFileKey) {
473
- throw new Error("No file key provided and no Figma file currently open. " +
474
- "Provide a fileKey parameter or navigate to a Figma file first.");
475
- }
476
- const errors = [];
477
- const kit = {
478
- fileKey: resolvedFileKey,
479
- generatedAt: new Date().toISOString(),
480
- format,
481
- ai_instruction: "",
482
- };
483
- // ----------------------------------------------------------------
484
- // Fetch tokens (variables)
485
- // ----------------------------------------------------------------
486
- if (include.includes("tokens")) {
487
- try {
488
- logger.info({ fileKey: resolvedFileKey }, "Fetching design tokens");
489
- // Check cache first
490
- const cacheKey = `vars:${resolvedFileKey}`;
491
- let formatted = null;
492
- if (variablesCache) {
493
- const cached = variablesCache.get(cacheKey);
494
- if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
495
- formatted = cached.data;
496
- logger.info("Using cached variables data");
497
- }
498
- }
499
- if (!formatted) {
500
- // Bridge-first: the Desktop Bridge / cloud relay reads variables on
501
- // ANY plan via the Plugin API. The Enterprise-only REST Variables API
502
- // is the fallback, used only when no bridge is connected — so most
503
- // users (non-Enterprise) no longer dead-end on a 403 here.
504
- formatted = await resolveFormattedVariables({
505
- getDesktopConnector,
506
- getFigmaAPI,
507
- fileKey: resolvedFileKey,
508
- });
509
- if (variablesCache) {
510
- variablesCache.set(cacheKey, {
511
- data: formatted,
512
- timestamp: Date.now(),
513
- });
514
- }
515
- }
516
- const collections = groupVariablesByCollection(formatted);
517
- kit.tokens = {
518
- collections,
519
- summary: formatted.summary,
520
- };
521
- }
522
- catch (err) {
523
- const msg = err instanceof Error ? err.message : String(err);
524
- logger.warn({ error: msg }, "Failed to fetch tokens");
525
- errors.push({ section: "tokens", message: msg });
526
- }
527
- }
528
- // ----------------------------------------------------------------
529
- // Fetch components
530
- // ----------------------------------------------------------------
531
- if (include.includes("components")) {
532
- try {
533
- logger.info({ fileKey: resolvedFileKey }, "Fetching components");
534
- const [componentsResponse, componentSetsResponse] = await Promise.all([
535
- withTimeout(api.getComponents(resolvedFileKey), 30000, "getComponents"),
536
- withTimeout(api.getComponentSets(resolvedFileKey), 30000, "getComponentSets"),
537
- ]);
538
- const allComponents = componentsResponse?.meta?.components || [];
539
- const allComponentSets = componentSetsResponse?.meta?.component_sets || [];
540
- const { components: standaloneComponents, componentSets } = deduplicateComponents(allComponents, allComponentSets);
541
- // Filter by component IDs if provided
542
- let targetComponents = standaloneComponents;
543
- let targetSets = componentSets;
544
- if (componentIds && componentIds.length > 0) {
545
- const idSet = new Set(componentIds);
546
- targetComponents = standaloneComponents.filter((c) => idSet.has(c.node_id));
547
- targetSets = componentSets.filter((s) => idSet.has(s.node_id));
548
- }
549
- // Build component specs
550
- const componentSpecs = [];
551
- // Collect all node IDs we need to fetch details for (batched, not N+1)
552
- const allNodeIds = [
553
- ...targetSets.map((s) => s.node_id),
554
- ...targetComponents.map((c) => c.node_id),
555
- ];
556
- // Batch fetch ALL node details in one call (max 50 per batch)
557
- const nodeDetailsMap = {};
558
- if (allNodeIds.length > 0) {
559
- try {
560
- const batchSize = 50;
561
- for (let i = 0; i < allNodeIds.length; i += batchSize) {
562
- const batch = allNodeIds.slice(i, i + batchSize);
563
- const nodeResponse = await withTimeout(api.getNodes(resolvedFileKey, batch, { depth: 2 }), 30000, `getNodes(batch ${Math.floor(i / batchSize) + 1})`);
564
- if (nodeResponse?.nodes) {
565
- for (const [nodeId, nodeData] of Object.entries(nodeResponse.nodes)) {
566
- nodeDetailsMap[nodeId] = nodeData?.document;
567
- }
568
- }
569
- }
570
- }
571
- catch (err) {
572
- logger.warn({ error: err }, "Failed to batch-fetch component node details");
573
- }
574
- }
575
- // Process component sets (multi-variant components)
576
- for (const set of targetSets) {
577
- const spec = {
578
- id: set.node_id,
579
- name: set.name,
580
- description: set.description || undefined,
581
- };
582
- // Use pre-fetched node details
583
- const setNode = nodeDetailsMap[set.node_id];
584
- // Get variant info from the child components
585
- // Match by component_set_id, containing_frame.nodeId, OR containingComponentSet.nodeId
586
- // (some designs nest variants inside intermediate frames)
587
- const variants = allComponents
588
- .filter((c) => c.component_set_id === set.node_id ||
589
- c.containing_frame?.nodeId === set.node_id ||
590
- c.containing_frame?.containingComponentSet?.nodeId === set.node_id)
591
- .map((c) => {
592
- const entry = { name: c.name, id: c.node_id };
593
- // Attach visual spec from depth-2 children of the set node
594
- if (setNode?.children) {
595
- const variantNode = setNode.children.find((ch) => ch.id === c.node_id);
596
- if (variantNode) {
597
- const vs = extractVisualSpec(variantNode);
598
- if (vs)
599
- entry.visualSpec = vs;
600
- }
601
- }
602
- return entry;
603
- });
604
- if (variants.length > 0) {
605
- // Variants share most visual properties — keep the full spec on
606
- // the base variant only and encode the rest as deltas
607
- deltaEncodeVariantSpecs(variants);
608
- spec.variants = variants;
609
- }
610
- if (setNode?.componentPropertyDefinitions) {
611
- spec.properties = setNode.componentPropertyDefinitions;
612
- }
613
- if (setNode?.absoluteBoundingBox) {
614
- spec.bounds = {
615
- width: setNode.absoluteBoundingBox.width,
616
- height: setNode.absoluteBoundingBox.height,
617
- };
618
- }
619
- // Extract visual spec from the set node itself
620
- if (setNode) {
621
- const setSpec = extractVisualSpec(setNode);
622
- if (setSpec) {
623
- spec.visualSpec = setSpec;
624
- }
625
- }
626
- componentSpecs.push(spec);
627
- }
628
- // Process standalone components (not part of a set)
629
- for (const comp of targetComponents) {
630
- const spec = {
631
- id: comp.node_id,
632
- name: comp.name,
633
- description: comp.description || undefined,
634
- };
635
- // Use pre-fetched node details
636
- const node = nodeDetailsMap[comp.node_id];
637
- if (node?.componentPropertyDefinitions) {
638
- spec.properties = node.componentPropertyDefinitions;
639
- }
640
- if (node?.absoluteBoundingBox) {
641
- spec.bounds = {
642
- width: node.absoluteBoundingBox.width,
643
- height: node.absoluteBoundingBox.height,
644
- };
645
- }
646
- // Extract visual spec from the component node
647
- if (node) {
648
- const nodeSpec = extractVisualSpec(node);
649
- if (nodeSpec) {
650
- spec.visualSpec = nodeSpec;
651
- }
652
- }
653
- componentSpecs.push(spec);
654
- }
655
- // Optionally fetch component images
656
- if (includeImages && componentSpecs.length > 0) {
657
- try {
658
- const nodeIds = componentSpecs.map((c) => c.id);
659
- // Batch in groups of 50 to stay within API limits
660
- const batchSize = 50;
661
- for (let i = 0; i < nodeIds.length; i += batchSize) {
662
- const batch = nodeIds.slice(i, i + batchSize);
663
- const imagesResult = await withTimeout(api.getImages(resolvedFileKey, batch, { scale: 2, format: "png" }), 30000, "getImages");
664
- if (imagesResult?.images) {
665
- for (const spec of componentSpecs) {
666
- const url = imagesResult.images[spec.id];
667
- if (url) {
668
- spec.imageUrl = url;
669
- }
670
- }
671
- }
672
- }
673
- }
674
- catch (err) {
675
- const msg = err instanceof Error ? err.message : String(err);
676
- logger.warn({ error: msg }, "Failed to fetch component images");
677
- errors.push({ section: "component_images", message: msg });
678
- }
679
- }
680
- kit.components = {
681
- items: componentSpecs,
682
- summary: {
683
- totalComponents: componentSpecs.length,
684
- totalComponentSets: targetSets.length,
685
- },
686
- };
687
- }
688
- catch (err) {
689
- const msg = err instanceof Error ? err.message : String(err);
690
- logger.warn({ error: msg }, "Failed to fetch components");
691
- errors.push({ section: "components", message: msg });
692
- }
693
- }
694
- // ----------------------------------------------------------------
695
- // Fetch styles
696
- // ----------------------------------------------------------------
697
- if (include.includes("styles")) {
698
- try {
699
- logger.info({ fileKey: resolvedFileKey }, "Fetching styles");
700
- const stylesResponse = await withTimeout(api.getStyles(resolvedFileKey), 30000, "getStyles");
701
- const allStyles = stylesResponse?.meta?.styles || [];
702
- const styleSpecs = allStyles.map((s) => ({
703
- key: s.key,
704
- name: s.name,
705
- styleType: s.style_type,
706
- description: s.description || undefined,
707
- nodeId: s.node_id,
708
- }));
709
- // Resolve actual values for styles (colors, typography, effects)
710
- if (styleSpecs.length > 0) {
711
- try {
712
- const resolvedValues = await resolveStyleValues(api, resolvedFileKey, styleSpecs);
713
- for (const style of styleSpecs) {
714
- if (style.nodeId && resolvedValues.has(style.nodeId)) {
715
- style.resolvedValue = resolvedValues.get(style.nodeId);
716
- }
717
- }
718
- }
719
- catch (err) {
720
- logger.warn({ error: err }, "Failed to resolve style values");
721
- }
722
- }
723
- const stylesByType = {};
724
- for (const s of styleSpecs) {
725
- stylesByType[s.styleType] = (stylesByType[s.styleType] || 0) + 1;
726
- }
727
- kit.styles = {
728
- items: styleSpecs,
729
- summary: {
730
- totalStyles: styleSpecs.length,
731
- stylesByType,
732
- },
733
- };
734
- }
735
- catch (err) {
736
- const msg = err instanceof Error ? err.message : String(err);
737
- logger.warn({ error: msg }, "Failed to fetch styles");
738
- errors.push({ section: "styles", message: msg });
739
- }
740
- }
741
- // ----------------------------------------------------------------
742
- // Build AI instruction
743
- // ----------------------------------------------------------------
744
- if (errors.length > 0) {
745
- kit.errors = errors;
746
- }
747
- const sections = [];
748
- if (kit.tokens)
749
- sections.push(`${kit.tokens.summary.totalVariables} tokens in ${kit.tokens.summary.totalCollections} collections`);
750
- if (kit.components)
751
- sections.push(`${kit.components.summary.totalComponents} components (${kit.components.summary.totalComponentSets} sets)`);
752
- if (kit.styles)
753
- sections.push(`${kit.styles.summary.totalStyles} styles`);
754
- kit.ai_instruction =
755
- "DESIGN SYSTEM SPECIFICATION — STRICT VISUAL FIDELITY REQUIRED\n\n" +
756
- `Contains: ${sections.join(", ")}.\n\n` +
757
- "RULES:\n" +
758
- "1. ONLY use colors, spacing, and typography values from this data. " +
759
- "Do NOT invent, guess, or add any visual properties not explicitly present.\n" +
760
- "2. Map 'visualSpec' directly to CSS:\n" +
761
- " - fills[].color → background-color (e.g. #181818)\n" +
762
- " - strokes[].color/weight → border (e.g. 1px solid #9747FF)\n" +
763
- " - effects[] → box-shadow (type DROP_SHADOW: offset.x offset.y radius spread color)\n" +
764
- " - cornerRadius → border-radius\n" +
765
- " - layout.mode HORIZONTAL → flex-direction:row, VERTICAL → flex-direction:column\n" +
766
- " - layout.paddingTop/Right/Bottom/Left → padding\n" +
767
- " - layout.itemSpacing → gap\n" +
768
- " - layout.primaryAxisAlign → justify-content, counterAxisAlign → align-items\n" +
769
- " - typography → font-family, font-size, font-weight, line-height, letter-spacing\n" +
770
- " - Variant specs are delta-encoded: one base variant carries the full visualSpec; " +
771
- "sibling variants carry 'visualSpecDelta' with ONLY the properties that differ from the base " +
772
- "(null = property absent on this variant). Merge base visualSpec + visualSpecDelta to get a " +
773
- "variant's full spec. A variant with neither field is visually identical to the base.\n" +
774
- "3. Do NOT add decorative elements (colored borders, accents, dividers, gradients) " +
775
- "unless they appear in the visualSpec data.\n" +
776
- "4. Use 'imageUrl' screenshots as the visual ground truth. If the screenshot " +
777
- "shows a simple dark card, do not add colored side borders or other embellishments.\n" +
778
- "5. Style 'resolvedValue' contains the exact design system colors and typography — " +
779
- "match these values precisely, do not substitute similar colors.\n" +
780
- "6. Component 'properties' define the component API (props). " +
781
- "VARIANT type properties define the visual variants (e.g. Info, Danger, Success). " +
782
- "BOOLEAN properties toggle features. TEXT properties accept string content.\n" +
783
- "7. When applying to an existing component library (e.g. shadcn, MUI, Chakra), " +
784
- "override the library's default theme values with the exact colors, spacing, and " +
785
- "typography from this specification. Do not blend with library defaults.";
786
- // ----------------------------------------------------------------
787
- // Adaptive compression for large responses
788
- // Thresholds tuned for consumer AI context windows (~128K tokens ≈ ~400KB text)
789
- // ----------------------------------------------------------------
790
- const sizeKB = calculateSizeKB(kit);
791
- logger.info({ sizeKB: sizeKB.toFixed(0), format }, "Kit assembled, checking compression");
792
- // Determine compression level from format + size
793
- let compressionLevel = null;
794
- if (format === "compact") {
795
- compressionLevel = "compact";
796
- }
797
- else if (format === "summary") {
798
- compressionLevel = "summary";
799
- }
800
- // Auto-compress based on size regardless of format setting
801
- // Lower thresholds to stay within consumer context windows
802
- if (sizeKB > 500) {
803
- compressionLevel = "compact"; // >500KB → just names and types
804
- }
805
- else if (sizeKB > 200) {
806
- // Upgrade to at least inventory if not already more aggressive
807
- if (!compressionLevel || compressionLevel === "summary") {
808
- compressionLevel = "inventory";
809
- }
810
- }
811
- else if (sizeKB > 100) {
812
- // Upgrade to at least summary
813
- if (!compressionLevel) {
814
- compressionLevel = "summary";
815
- }
816
- }
817
- if (compressionLevel) {
818
- const compressed = compressKit(kit, compressionLevel);
819
- const compressedSizeKB = calculateSizeKB(compressed);
820
- if (sizeKB > 100) {
821
- compressed.ai_instruction =
822
- `Response auto-compressed (${compressionLevel}) from ${sizeKB.toFixed(0)}KB to ${compressedSizeKB.toFixed(0)}KB. ` +
823
- compressed.ai_instruction +
824
- " For full visual specs of specific components, re-call with specific componentIds and format='full'.";
825
- }
826
- logger.info({ originalKB: sizeKB.toFixed(0), compressedKB: compressedSizeKB.toFixed(0), level: compressionLevel }, "Kit compressed");
827
- return {
828
- content: [
829
- {
830
- type: "text",
831
- text: JSON.stringify(compressed),
832
- },
833
- ],
834
- };
835
- }
836
- return {
837
- content: [
838
- {
839
- type: "text",
840
- text: JSON.stringify(kit),
841
- },
842
- ],
843
- };
844
- }
845
- catch (error) {
846
- logger.error({ error }, "Failed to generate design system kit");
847
- const errorMessage = error instanceof Error ? error.message : String(error);
848
- // Check if it's an auth error
849
- let parsedError = null;
850
- try {
851
- parsedError = JSON.parse(errorMessage);
852
- }
853
- catch {
854
- // Not a JSON error
855
- }
856
- if (parsedError?.error === "authentication_required" || parsedError?.error === "oauth_error") {
857
- return {
858
- content: [
859
- {
860
- type: "text",
861
- text: JSON.stringify(parsedError),
862
- },
863
- ],
864
- isError: true,
865
- };
866
- }
867
- return {
868
- content: [
869
- {
870
- type: "text",
871
- text: JSON.stringify({
872
- error: errorMessage,
873
- message: "Failed to generate design system kit",
874
- hint: "Ensure you have a valid Figma file key and the file contains published components/variables.",
875
- }),
876
- },
877
- ],
878
- isError: true,
879
- };
880
- }
881
- });
882
- }