@mp3wizard/figma-console-mcp 1.14.0

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