@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,2092 @@
1
+ import { z } from "zod";
2
+ import { createChildLogger } from "./logger.js";
3
+ const logger = createChildLogger({ component: "write-tools" });
4
+ /**
5
+ * Register write/manipulation tools that require a Desktop Bridge connector.
6
+ * Used by both local mode (src/local.ts) and cloud mode (src/index.ts).
7
+ */
8
+ export function registerWriteTools(server, getDesktopConnector) {
9
+ // ============================================================================
10
+ // EXECUTION TOOL
11
+ // ============================================================================
12
+ server.tool("figma_execute", `Execute arbitrary JavaScript in Figma's plugin context with full access to the figma API. Use for complex operations not covered by other tools. Requires Desktop Bridge plugin. CAUTION: Can modify your document.
13
+
14
+ **COMPONENT INSTANCES:** For instances (node.type === 'INSTANCE'), use figma_set_instance_properties — direct text editing FAILS SILENTLY. Check instance.componentProperties for available props (may have #nodeId suffixes).
15
+
16
+ **RESULT ANALYSIS:** Check resultAnalysis.warning for silent failures (empty arrays, null returns).
17
+
18
+ **VALIDATION:** After creating/modifying visuals: screenshot with figma_capture_screenshot, check alignment/spacing/proportions, iterate up to 3x.
19
+
20
+ **PLACEMENT:** Always create components inside a Section or Frame, never on blank canvas. Use parent.insertChild(0, bg) for z-ordering backgrounds behind content.`, {
21
+ code: z
22
+ .string()
23
+ .describe("JavaScript code to execute. Has access to the 'figma' global object. " +
24
+ "Example: 'const rect = figma.createRectangle(); rect.resize(100, 100); return { id: rect.id };'"),
25
+ timeout: z
26
+ .number()
27
+ .optional()
28
+ .default(5000)
29
+ .describe("Execution timeout in milliseconds (default: 5000, max: 30000)"),
30
+ }, async ({ code, timeout }) => {
31
+ try {
32
+ const connector = await getDesktopConnector();
33
+ const result = await connector.executeCodeViaUI(code, Math.min(timeout, 30000));
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify({
39
+ success: result.success,
40
+ result: result.result,
41
+ error: result.error,
42
+ resultAnalysis: result.resultAnalysis,
43
+ fileContext: result.fileContext,
44
+ timestamp: Date.now(),
45
+ }),
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ catch (error) {
51
+ logger.error({ error }, "Failed to execute code in Figma plugin context");
52
+ return {
53
+ content: [
54
+ {
55
+ type: "text",
56
+ text: JSON.stringify({
57
+ error: error instanceof Error ? error.message : String(error),
58
+ message: "Failed to execute code in Figma plugin context",
59
+ hint: "Make sure the Desktop Bridge plugin is running in Figma",
60
+ }),
61
+ },
62
+ ],
63
+ isError: true,
64
+ };
65
+ }
66
+ });
67
+ // ============================================================================
68
+ // VARIABLE MANAGEMENT TOOLS
69
+ // ============================================================================
70
+ // Tool: Update a variable's value
71
+ server.tool("figma_update_variable", "Update a single variable's value. For multiple updates, use figma_batch_update_variables instead (10-50x faster). Use figma_get_variables first for IDs. COLOR: hex '#FF0000', FLOAT: number, STRING: text, BOOLEAN: true/false. Requires Desktop Bridge plugin.", {
72
+ variableId: z
73
+ .string()
74
+ .describe("The variable ID to update (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
75
+ modeId: z
76
+ .string()
77
+ .describe("The mode ID to update the value in (e.g., '1:0'). Get this from the variable's collection modes."),
78
+ value: z
79
+ .union([z.string(), z.number(), z.boolean()])
80
+ .describe("The new value. For COLOR: hex string like '#FF0000'. For FLOAT: number. For STRING: text. For BOOLEAN: true/false."),
81
+ }, async ({ variableId, modeId, value }) => {
82
+ try {
83
+ const connector = await getDesktopConnector();
84
+ const result = await connector.updateVariable(variableId, modeId, value);
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: JSON.stringify({
90
+ success: true,
91
+ message: `Variable "${result.variable.name}" updated successfully`,
92
+ variable: result.variable,
93
+ timestamp: Date.now(),
94
+ }),
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ catch (error) {
100
+ logger.error({ error }, "Failed to update variable");
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: JSON.stringify({
106
+ error: error instanceof Error ? error.message : String(error),
107
+ message: "Failed to update variable",
108
+ hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
109
+ }),
110
+ },
111
+ ],
112
+ isError: true,
113
+ };
114
+ }
115
+ });
116
+ // Tool: Create a new variable
117
+ server.tool("figma_create_variable", "Create a single Figma variable. For multiple variables, use figma_batch_create_variables instead (10-50x faster). Use figma_get_variables first to get collection IDs. Supports COLOR, FLOAT, STRING, BOOLEAN. Requires Desktop Bridge plugin.", {
118
+ name: z
119
+ .string()
120
+ .describe("Name for the new variable (e.g., 'primary-blue')"),
121
+ collectionId: z
122
+ .string()
123
+ .describe("The collection ID to create the variable in (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
124
+ resolvedType: z
125
+ .enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
126
+ .describe("The variable type: COLOR, FLOAT, STRING, or BOOLEAN"),
127
+ description: z
128
+ .string()
129
+ .optional()
130
+ .describe("Optional description for the variable"),
131
+ valuesByMode: z
132
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
133
+ .optional()
134
+ .describe("Optional initial values by mode ID. Example: { '1:0': '#FF0000', '1:1': '#0000FF' }"),
135
+ }, async ({ name, collectionId, resolvedType, description, valuesByMode, }) => {
136
+ try {
137
+ const connector = await getDesktopConnector();
138
+ const result = await connector.createVariable(name, collectionId, resolvedType, {
139
+ description,
140
+ valuesByMode,
141
+ });
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify({
147
+ success: true,
148
+ message: `Variable "${name}" created successfully`,
149
+ variable: result.variable,
150
+ timestamp: Date.now(),
151
+ }),
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ catch (error) {
157
+ logger.error({ error }, "Failed to create variable");
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: JSON.stringify({
163
+ error: error instanceof Error ? error.message : String(error),
164
+ message: "Failed to create variable",
165
+ hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
166
+ }),
167
+ },
168
+ ],
169
+ isError: true,
170
+ };
171
+ }
172
+ });
173
+ // Tool: Create a new variable collection
174
+ server.tool("figma_create_variable_collection", "Create an empty variable collection. To create a collection WITH variables and modes in one step, use figma_setup_design_tokens instead. Requires Desktop Bridge plugin.", {
175
+ name: z
176
+ .string()
177
+ .describe("Name for the new collection (e.g., 'Brand Colors')"),
178
+ initialModeName: z
179
+ .string()
180
+ .optional()
181
+ .describe("Name for the initial mode (default mode is created automatically). Example: 'Light'"),
182
+ additionalModes: z
183
+ .array(z.string())
184
+ .optional()
185
+ .describe("Additional mode names to create. Example: ['Dark', 'High Contrast']"),
186
+ }, async ({ name, initialModeName, additionalModes }) => {
187
+ try {
188
+ const connector = await getDesktopConnector();
189
+ const result = await connector.createVariableCollection(name, {
190
+ initialModeName,
191
+ additionalModes,
192
+ });
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: JSON.stringify({
198
+ success: true,
199
+ message: `Collection "${name}" created successfully`,
200
+ collection: result.collection,
201
+ timestamp: Date.now(),
202
+ }),
203
+ },
204
+ ],
205
+ };
206
+ }
207
+ catch (error) {
208
+ logger.error({ error }, "Failed to create collection");
209
+ return {
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: JSON.stringify({
214
+ error: error instanceof Error ? error.message : String(error),
215
+ message: "Failed to create variable collection",
216
+ hint: "Make sure the Desktop Bridge plugin is running in Figma",
217
+ }),
218
+ },
219
+ ],
220
+ isError: true,
221
+ };
222
+ }
223
+ });
224
+ // Tool: Delete a variable
225
+ server.tool("figma_delete_variable", "Delete a Figma variable. WARNING: This is a destructive operation that cannot be undone (except with Figma's undo). Use figma_get_variables first to get variable IDs. Requires the Desktop Bridge plugin to be running.", {
226
+ variableId: z
227
+ .string()
228
+ .describe("The variable ID to delete (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
229
+ }, async ({ variableId }) => {
230
+ try {
231
+ const connector = await getDesktopConnector();
232
+ const result = await connector.deleteVariable(variableId);
233
+ return {
234
+ content: [
235
+ {
236
+ type: "text",
237
+ text: JSON.stringify({
238
+ success: true,
239
+ message: `Variable "${result.deleted.name}" deleted successfully`,
240
+ deleted: result.deleted,
241
+ timestamp: Date.now(),
242
+ warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
243
+ }),
244
+ },
245
+ ],
246
+ };
247
+ }
248
+ catch (error) {
249
+ logger.error({ error }, "Failed to delete variable");
250
+ return {
251
+ content: [
252
+ {
253
+ type: "text",
254
+ text: JSON.stringify({
255
+ error: error instanceof Error ? error.message : String(error),
256
+ message: "Failed to delete variable",
257
+ hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
258
+ }),
259
+ },
260
+ ],
261
+ isError: true,
262
+ };
263
+ }
264
+ });
265
+ // Tool: Delete a variable collection
266
+ server.tool("figma_delete_variable_collection", "Delete a Figma variable collection and ALL its variables. WARNING: This is a destructive operation that deletes all variables in the collection and cannot be undone (except with Figma's undo). Requires the Desktop Bridge plugin to be running.", {
267
+ collectionId: z
268
+ .string()
269
+ .describe("The collection ID to delete (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
270
+ }, async ({ collectionId }) => {
271
+ try {
272
+ const connector = await getDesktopConnector();
273
+ const result = await connector.deleteVariableCollection(collectionId);
274
+ return {
275
+ content: [
276
+ {
277
+ type: "text",
278
+ text: JSON.stringify({
279
+ success: true,
280
+ message: `Collection "${result.deleted.name}" and ${result.deleted.variableCount} variables deleted successfully`,
281
+ deleted: result.deleted,
282
+ timestamp: Date.now(),
283
+ warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
284
+ }),
285
+ },
286
+ ],
287
+ };
288
+ }
289
+ catch (error) {
290
+ logger.error({ error }, "Failed to delete collection");
291
+ return {
292
+ content: [
293
+ {
294
+ type: "text",
295
+ text: JSON.stringify({
296
+ error: error instanceof Error ? error.message : String(error),
297
+ message: "Failed to delete variable collection",
298
+ hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
299
+ }),
300
+ },
301
+ ],
302
+ isError: true,
303
+ };
304
+ }
305
+ });
306
+ // Tool: Rename a variable
307
+ server.tool("figma_rename_variable", "Rename an existing Figma variable. This updates the variable's name while preserving all its values and settings. Requires the Desktop Bridge plugin to be running.", {
308
+ variableId: z
309
+ .string()
310
+ .describe("The variable ID to rename (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
311
+ newName: z
312
+ .string()
313
+ .describe("The new name for the variable. Can include slashes for grouping (e.g., 'colors/primary/background')."),
314
+ }, async ({ variableId, newName }) => {
315
+ try {
316
+ const connector = await getDesktopConnector();
317
+ const result = await connector.renameVariable(variableId, newName);
318
+ return {
319
+ content: [
320
+ {
321
+ type: "text",
322
+ text: JSON.stringify({
323
+ success: true,
324
+ message: `Variable renamed from "${result.oldName}" to "${result.variable.name}"`,
325
+ oldName: result.oldName,
326
+ variable: result.variable,
327
+ timestamp: Date.now(),
328
+ }),
329
+ },
330
+ ],
331
+ };
332
+ }
333
+ catch (error) {
334
+ logger.error({ error }, "Failed to rename variable");
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: JSON.stringify({
340
+ error: error instanceof Error ? error.message : String(error),
341
+ message: "Failed to rename variable",
342
+ hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
343
+ }),
344
+ },
345
+ ],
346
+ isError: true,
347
+ };
348
+ }
349
+ });
350
+ // Tool: Add a mode to a collection
351
+ server.tool("figma_add_mode", "Add a new mode to an existing Figma variable collection. Modes allow variables to have different values for different contexts (e.g., Light/Dark themes, device sizes). Requires the Desktop Bridge plugin to be running.", {
352
+ collectionId: z
353
+ .string()
354
+ .describe("The collection ID to add the mode to (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
355
+ modeName: z
356
+ .string()
357
+ .describe("The name for the new mode (e.g., 'Dark', 'Mobile', 'High Contrast')."),
358
+ }, async ({ collectionId, modeName }) => {
359
+ try {
360
+ const connector = await getDesktopConnector();
361
+ const result = await connector.addMode(collectionId, modeName);
362
+ return {
363
+ content: [
364
+ {
365
+ type: "text",
366
+ text: JSON.stringify({
367
+ success: true,
368
+ message: `Mode "${modeName}" added to collection "${result.collection.name}"`,
369
+ newMode: result.newMode,
370
+ collection: result.collection,
371
+ timestamp: Date.now(),
372
+ }),
373
+ },
374
+ ],
375
+ };
376
+ }
377
+ catch (error) {
378
+ logger.error({ error }, "Failed to add mode");
379
+ return {
380
+ content: [
381
+ {
382
+ type: "text",
383
+ text: JSON.stringify({
384
+ error: error instanceof Error ? error.message : String(error),
385
+ message: "Failed to add mode to collection",
386
+ hint: "Make sure the Desktop Bridge plugin is running, the collection ID is correct, and you haven't exceeded Figma's mode limit",
387
+ }),
388
+ },
389
+ ],
390
+ isError: true,
391
+ };
392
+ }
393
+ });
394
+ // Tool: Rename a mode in a collection
395
+ server.tool("figma_rename_mode", "Rename an existing mode in a Figma variable collection. Requires the Desktop Bridge plugin to be running.", {
396
+ collectionId: z
397
+ .string()
398
+ .describe("The collection ID containing the mode (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
399
+ modeId: z
400
+ .string()
401
+ .describe("The mode ID to rename (e.g., '123:0'). Get this from the collection's modes array in figma_get_variables."),
402
+ newName: z
403
+ .string()
404
+ .describe("The new name for the mode (e.g., 'Dark Theme', 'Tablet')."),
405
+ }, async ({ collectionId, modeId, newName }) => {
406
+ try {
407
+ const connector = await getDesktopConnector();
408
+ const result = await connector.renameMode(collectionId, modeId, newName);
409
+ return {
410
+ content: [
411
+ {
412
+ type: "text",
413
+ text: JSON.stringify({
414
+ success: true,
415
+ message: `Mode renamed from "${result.oldName}" to "${newName}"`,
416
+ oldName: result.oldName,
417
+ collection: result.collection,
418
+ timestamp: Date.now(),
419
+ }),
420
+ },
421
+ ],
422
+ };
423
+ }
424
+ catch (error) {
425
+ logger.error({ error }, "Failed to rename mode");
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: JSON.stringify({
431
+ error: error instanceof Error ? error.message : String(error),
432
+ message: "Failed to rename mode",
433
+ hint: "Make sure the Desktop Bridge plugin is running, the collection ID and mode ID are correct",
434
+ }),
435
+ },
436
+ ],
437
+ isError: true,
438
+ };
439
+ }
440
+ });
441
+ // ============================================================================
442
+ // BATCH OPERATIONS (Performance-Optimized)
443
+ // ============================================================================
444
+ // Execute multiple variable operations in a single roundtrip,
445
+ // reducing per-operation overhead from ~60-170ms to near-zero.
446
+ // Use these instead of calling individual tools repeatedly.
447
+ // Tool: Batch create variables
448
+ server.tool("figma_batch_create_variables", "Create multiple variables in one operation. Use instead of calling figma_create_variable repeatedly — up to 50x faster for bulk operations. Get collection IDs from figma_get_variables first. Requires Desktop Bridge plugin.", {
449
+ collectionId: z
450
+ .string()
451
+ .describe("Collection ID to create all variables in (e.g., 'VariableCollectionId:123:456')"),
452
+ variables: z
453
+ .array(z.object({
454
+ name: z.string().describe("Variable name (e.g., 'primary-blue')"),
455
+ resolvedType: z
456
+ .enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
457
+ .describe("Variable type"),
458
+ description: z
459
+ .string()
460
+ .optional()
461
+ .describe("Optional description"),
462
+ valuesByMode: z
463
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
464
+ .optional()
465
+ .describe("Values by mode ID. For COLOR: hex like '#FF0000'. Example: { '1:0': '#FF0000' }"),
466
+ }))
467
+ .min(1)
468
+ .max(100)
469
+ .describe("Array of variables to create (1-100)"),
470
+ }, async ({ collectionId, variables }) => {
471
+ try {
472
+ const connector = await getDesktopConnector();
473
+ const script = `
474
+ const results = [];
475
+ const collectionId = ${JSON.stringify(collectionId)};
476
+ const vars = ${JSON.stringify(variables)};
477
+
478
+ function hexToRgba(hex) {
479
+ hex = hex.replace('#', '');
480
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
481
+ return {
482
+ r: parseInt(hex.substring(0, 2), 16) / 255,
483
+ g: parseInt(hex.substring(2, 4), 16) / 255,
484
+ b: parseInt(hex.substring(4, 6), 16) / 255,
485
+ a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
486
+ };
487
+ }
488
+
489
+ const collection = await figma.variables.getVariableCollectionByIdAsync(collectionId);
490
+ if (!collection) return { created: 0, failed: vars.length, results: vars.map(v => ({ success: false, name: v.name, error: 'Collection not found: ' + collectionId })) };
491
+
492
+ for (const v of vars) {
493
+ try {
494
+ const variable = figma.variables.createVariable(v.name, collection, v.resolvedType);
495
+ if (v.description) variable.description = v.description;
496
+ if (v.valuesByMode) {
497
+ for (const [modeId, value] of Object.entries(v.valuesByMode)) {
498
+ const processed = v.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
499
+ variable.setValueForMode(modeId, processed);
500
+ }
501
+ }
502
+ results.push({ success: true, name: v.name, id: variable.id });
503
+ } catch (err) {
504
+ results.push({ success: false, name: v.name, error: String(err) });
505
+ }
506
+ }
507
+
508
+ return {
509
+ created: results.filter(r => r.success).length,
510
+ failed: results.filter(r => !r.success).length,
511
+ results
512
+ };`;
513
+ const timeout = Math.max(5000, variables.length * 200);
514
+ const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
515
+ if (result.error) {
516
+ return {
517
+ content: [
518
+ {
519
+ type: "text",
520
+ text: JSON.stringify({
521
+ error: result.error,
522
+ message: "Batch create failed during execution",
523
+ hint: "Check that the collection ID is valid and the Desktop Bridge plugin is running",
524
+ }),
525
+ },
526
+ ],
527
+ isError: true,
528
+ };
529
+ }
530
+ return {
531
+ content: [
532
+ {
533
+ type: "text",
534
+ text: JSON.stringify({
535
+ success: true,
536
+ message: `Batch created ${result.result?.created ?? 0} variables (${result.result?.failed ?? 0} failed)`,
537
+ ...result.result,
538
+ timestamp: Date.now(),
539
+ }),
540
+ },
541
+ ],
542
+ };
543
+ }
544
+ catch (error) {
545
+ logger.error({ error }, "Failed to batch create variables");
546
+ return {
547
+ content: [
548
+ {
549
+ type: "text",
550
+ text: JSON.stringify({
551
+ error: error instanceof Error
552
+ ? error.message
553
+ : String(error),
554
+ message: "Failed to batch create variables",
555
+ hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
556
+ }),
557
+ },
558
+ ],
559
+ isError: true,
560
+ };
561
+ }
562
+ });
563
+ // Tool: Batch update variables
564
+ server.tool("figma_batch_update_variables", "Update multiple variable values in one operation. Use instead of calling figma_update_variable repeatedly — up to 50x faster for bulk updates. Get variable/mode IDs from figma_get_variables first. Requires Desktop Bridge plugin.", {
565
+ updates: z
566
+ .array(z.object({
567
+ variableId: z
568
+ .string()
569
+ .describe("Variable ID (e.g., 'VariableID:123:456')"),
570
+ modeId: z
571
+ .string()
572
+ .describe("Mode ID (e.g., '1:0')"),
573
+ value: z
574
+ .union([z.string(), z.number(), z.boolean()])
575
+ .describe("New value. COLOR: hex like '#FF0000'. FLOAT: number. STRING: text. BOOLEAN: true/false."),
576
+ }))
577
+ .min(1)
578
+ .max(100)
579
+ .describe("Array of updates to apply (1-100)"),
580
+ }, async ({ updates }) => {
581
+ try {
582
+ const connector = await getDesktopConnector();
583
+ const script = `
584
+ const results = [];
585
+ const updates = ${JSON.stringify(updates)};
586
+
587
+ function hexToRgba(hex) {
588
+ hex = hex.replace('#', '');
589
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
590
+ return {
591
+ r: parseInt(hex.substring(0, 2), 16) / 255,
592
+ g: parseInt(hex.substring(2, 4), 16) / 255,
593
+ b: parseInt(hex.substring(4, 6), 16) / 255,
594
+ a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
595
+ };
596
+ }
597
+
598
+ for (const u of updates) {
599
+ try {
600
+ const variable = await figma.variables.getVariableByIdAsync(u.variableId);
601
+ if (!variable) throw new Error('Variable not found: ' + u.variableId);
602
+ const isColor = variable.resolvedType === 'COLOR';
603
+ const processed = isColor && typeof u.value === 'string' ? hexToRgba(u.value) : u.value;
604
+ variable.setValueForMode(u.modeId, processed);
605
+ results.push({ success: true, variableId: u.variableId, name: variable.name });
606
+ } catch (err) {
607
+ results.push({ success: false, variableId: u.variableId, error: String(err) });
608
+ }
609
+ }
610
+
611
+ return {
612
+ updated: results.filter(r => r.success).length,
613
+ failed: results.filter(r => !r.success).length,
614
+ results
615
+ };`;
616
+ const timeout = Math.max(5000, updates.length * 150);
617
+ const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
618
+ if (result.error) {
619
+ return {
620
+ content: [
621
+ {
622
+ type: "text",
623
+ text: JSON.stringify({
624
+ error: result.error,
625
+ message: "Batch update failed during execution",
626
+ hint: "Check that variable IDs and mode IDs are valid",
627
+ }),
628
+ },
629
+ ],
630
+ isError: true,
631
+ };
632
+ }
633
+ return {
634
+ content: [
635
+ {
636
+ type: "text",
637
+ text: JSON.stringify({
638
+ success: true,
639
+ message: `Batch updated ${result.result?.updated ?? 0} variables (${result.result?.failed ?? 0} failed)`,
640
+ ...result.result,
641
+ timestamp: Date.now(),
642
+ }),
643
+ },
644
+ ],
645
+ };
646
+ }
647
+ catch (error) {
648
+ logger.error({ error }, "Failed to batch update variables");
649
+ return {
650
+ content: [
651
+ {
652
+ type: "text",
653
+ text: JSON.stringify({
654
+ error: error instanceof Error
655
+ ? error.message
656
+ : String(error),
657
+ message: "Failed to batch update variables",
658
+ hint: "Make sure the Desktop Bridge plugin is running and variable/mode IDs are correct",
659
+ }),
660
+ },
661
+ ],
662
+ isError: true,
663
+ };
664
+ }
665
+ });
666
+ // Tool: Setup design tokens (collection + modes + variables atomically)
667
+ server.tool("figma_setup_design_tokens", "Create a complete design token structure in one operation: collection, modes, and all variables. Ideal for importing CSS custom properties or design tokens into Figma. Requires Desktop Bridge plugin.", {
668
+ collectionName: z
669
+ .string()
670
+ .describe("Name for the token collection (e.g., 'Brand Tokens')"),
671
+ modes: z
672
+ .array(z.string())
673
+ .min(1)
674
+ .max(4)
675
+ .describe("Mode names (first becomes default). Example: ['Light', 'Dark']"),
676
+ tokens: z
677
+ .array(z.object({
678
+ name: z
679
+ .string()
680
+ .describe("Token name (e.g., 'color/primary')"),
681
+ resolvedType: z
682
+ .enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
683
+ .describe("Token type"),
684
+ description: z
685
+ .string()
686
+ .optional()
687
+ .describe("Optional description"),
688
+ values: z
689
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
690
+ .describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }"),
691
+ }))
692
+ .min(1)
693
+ .max(100)
694
+ .describe("Token definitions (1-100)"),
695
+ }, async ({ collectionName, modes, tokens }) => {
696
+ try {
697
+ const connector = await getDesktopConnector();
698
+ const script = `
699
+ const collectionName = ${JSON.stringify(collectionName)};
700
+ const modeNames = ${JSON.stringify(modes)};
701
+ const tokenDefs = ${JSON.stringify(tokens)};
702
+
703
+ function hexToRgba(hex) {
704
+ hex = hex.replace('#', '');
705
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
706
+ return {
707
+ r: parseInt(hex.substring(0, 2), 16) / 255,
708
+ g: parseInt(hex.substring(2, 4), 16) / 255,
709
+ b: parseInt(hex.substring(4, 6), 16) / 255,
710
+ a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
711
+ };
712
+ }
713
+
714
+ // Step 1: Create collection
715
+ const collection = figma.variables.createVariableCollection(collectionName);
716
+ const modeMap = {};
717
+
718
+ // Step 2: Set up modes - first mode uses the default mode that was auto-created
719
+ const defaultModeId = collection.modes[0].modeId;
720
+ collection.renameMode(defaultModeId, modeNames[0]);
721
+ modeMap[modeNames[0]] = defaultModeId;
722
+
723
+ for (let i = 1; i < modeNames.length; i++) {
724
+ const newModeId = collection.addMode(modeNames[i]);
725
+ modeMap[modeNames[i]] = newModeId;
726
+ }
727
+
728
+ // Step 3: Create all variables with values
729
+ const results = [];
730
+ for (const t of tokenDefs) {
731
+ try {
732
+ const variable = figma.variables.createVariable(t.name, collection, t.resolvedType);
733
+ if (t.description) variable.description = t.description;
734
+ for (const [modeName, value] of Object.entries(t.values)) {
735
+ const modeId = modeMap[modeName];
736
+ if (!modeId) { results.push({ success: false, name: t.name, error: 'Unknown mode: ' + modeName }); continue; }
737
+ const processed = t.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
738
+ variable.setValueForMode(modeId, processed);
739
+ }
740
+ results.push({ success: true, name: t.name, id: variable.id });
741
+ } catch (err) {
742
+ results.push({ success: false, name: t.name, error: String(err) });
743
+ }
744
+ }
745
+
746
+ return {
747
+ collectionId: collection.id,
748
+ collectionName: collectionName,
749
+ modes: modeMap,
750
+ created: results.filter(r => r.success).length,
751
+ failed: results.filter(r => !r.success).length,
752
+ results
753
+ };`;
754
+ const timeout = Math.max(10000, tokens.length * 200 + modes.length * 500);
755
+ const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
756
+ if (result.error) {
757
+ return {
758
+ content: [
759
+ {
760
+ type: "text",
761
+ text: JSON.stringify({
762
+ error: result.error,
763
+ message: "Design token setup failed during execution",
764
+ hint: "Check the token definitions and ensure the Desktop Bridge plugin is running",
765
+ }),
766
+ },
767
+ ],
768
+ isError: true,
769
+ };
770
+ }
771
+ return {
772
+ content: [
773
+ {
774
+ type: "text",
775
+ text: JSON.stringify({
776
+ success: true,
777
+ message: `Created collection "${collectionName}" with ${modes.length} mode(s) and ${result.result?.created ?? 0} tokens`,
778
+ ...result.result,
779
+ timestamp: Date.now(),
780
+ }),
781
+ },
782
+ ],
783
+ };
784
+ }
785
+ catch (error) {
786
+ logger.error({ error }, "Failed to setup design tokens");
787
+ return {
788
+ content: [
789
+ {
790
+ type: "text",
791
+ text: JSON.stringify({
792
+ error: error instanceof Error
793
+ ? error.message
794
+ : String(error),
795
+ message: "Failed to setup design tokens",
796
+ hint: "Make sure the Desktop Bridge plugin is running in Figma",
797
+ }),
798
+ },
799
+ ],
800
+ isError: true,
801
+ };
802
+ }
803
+ });
804
+ // ============================================================================
805
+ // COMPONENT INSTANTIATION & PROPERTY TOOLS
806
+ // ============================================================================
807
+ // Tool: Instantiate Component
808
+ server.tool("figma_instantiate_component", `Create an instance of a component from the design system.
809
+
810
+ **CRITICAL: Always pass BOTH componentKey AND nodeId together!**
811
+ Search results return both identifiers. Pass both so the tool can automatically fall back to nodeId if the component isn't published to a library. Most local/unpublished components require nodeId.
812
+
813
+ **IMPORTANT: Always re-search before instantiating!**
814
+ NodeIds are session-specific and may be stale from previous conversations. ALWAYS search for components at the start of each design session to get current, valid identifiers.
815
+
816
+ **VISUAL VALIDATION WORKFLOW:**
817
+ After instantiating components, use figma_take_screenshot to verify the result looks correct. Check placement, sizing, and visual balance.`, {
818
+ componentKey: z
819
+ .string()
820
+ .optional()
821
+ .describe("The component key from search results. Pass this WITH nodeId for automatic fallback."),
822
+ nodeId: z
823
+ .string()
824
+ .optional()
825
+ .describe("The node ID from search results. ALWAYS pass this alongside componentKey - most local components need it."),
826
+ variant: z
827
+ .record(z.string())
828
+ .optional()
829
+ .describe("Variant properties to set (e.g., { Type: 'Simple', State: 'Active' })"),
830
+ overrides: z
831
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
832
+ .optional()
833
+ .describe("Property overrides (e.g., { 'Button Label': 'Click Me' })"),
834
+ position: z
835
+ .object({
836
+ x: z.number(),
837
+ y: z.number(),
838
+ })
839
+ .optional()
840
+ .describe("Position on canvas (default: 0, 0)"),
841
+ parentId: z
842
+ .string()
843
+ .optional()
844
+ .describe("Parent node ID to append the instance to"),
845
+ }, async ({ componentKey, nodeId, variant, overrides, position, parentId, }) => {
846
+ try {
847
+ if (!componentKey && !nodeId) {
848
+ throw new Error("Either componentKey or nodeId is required");
849
+ }
850
+ const connector = await getDesktopConnector();
851
+ const result = await connector.instantiateComponent(componentKey || "", {
852
+ nodeId,
853
+ position,
854
+ overrides,
855
+ variant,
856
+ parentId,
857
+ });
858
+ if (!result.success) {
859
+ throw new Error(result.error || "Failed to instantiate component");
860
+ }
861
+ return {
862
+ content: [
863
+ {
864
+ type: "text",
865
+ text: JSON.stringify({
866
+ success: true,
867
+ message: "Component instantiated successfully",
868
+ instance: result.instance,
869
+ timestamp: Date.now(),
870
+ }),
871
+ },
872
+ ],
873
+ };
874
+ }
875
+ catch (error) {
876
+ logger.error({ error }, "Failed to instantiate component");
877
+ return {
878
+ content: [
879
+ {
880
+ type: "text",
881
+ text: JSON.stringify({
882
+ error: error instanceof Error ? error.message : String(error),
883
+ message: "Failed to instantiate component",
884
+ hint: "Make sure the component key is correct and the Desktop Bridge plugin is running",
885
+ }),
886
+ },
887
+ ],
888
+ isError: true,
889
+ };
890
+ }
891
+ });
892
+ // ============================================================================
893
+ // Component Property Management Tools
894
+ // ============================================================================
895
+ // Tool: Set Node Description
896
+ server.tool("figma_set_description", "Set the description text on a component, component set, or style. Descriptions appear in Dev Mode and help document design intent. Supports plain text and markdown formatting.", {
897
+ nodeId: z
898
+ .string()
899
+ .describe("The node ID of the component or style to update (e.g., '123:456')"),
900
+ description: z.string().describe("The plain text description to set"),
901
+ descriptionMarkdown: z
902
+ .string()
903
+ .optional()
904
+ .describe("Optional rich text description using markdown formatting"),
905
+ }, async ({ nodeId, description, descriptionMarkdown }) => {
906
+ try {
907
+ const connector = await getDesktopConnector();
908
+ const result = await connector.setNodeDescription(nodeId, description, descriptionMarkdown);
909
+ if (!result.success) {
910
+ throw new Error(result.error || "Failed to set description");
911
+ }
912
+ return {
913
+ content: [
914
+ {
915
+ type: "text",
916
+ text: JSON.stringify({
917
+ success: true,
918
+ message: "Description set successfully",
919
+ node: result.node,
920
+ }),
921
+ },
922
+ ],
923
+ };
924
+ }
925
+ catch (error) {
926
+ logger.error({ error }, "Failed to set description");
927
+ return {
928
+ content: [
929
+ {
930
+ type: "text",
931
+ text: JSON.stringify({
932
+ error: error instanceof Error ? error.message : String(error),
933
+ hint: "Make sure the node supports descriptions (components, component sets, styles)",
934
+ }),
935
+ },
936
+ ],
937
+ isError: true,
938
+ };
939
+ }
940
+ });
941
+ // Tool: Add Component Property
942
+ server.tool("figma_add_component_property", "Add a new component property to a component or component set. Properties enable dynamic content and behavior in component instances. Supported types: BOOLEAN (toggle), TEXT (string), INSTANCE_SWAP (component swap), VARIANT (variant selection).", {
943
+ nodeId: z.string().describe("The component or component set node ID"),
944
+ propertyName: z
945
+ .string()
946
+ .describe("Name for the new property (e.g., 'Show Icon', 'Button Label')"),
947
+ type: z
948
+ .enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT"])
949
+ .describe("Property type: BOOLEAN for toggles, TEXT for strings, INSTANCE_SWAP for component swaps, VARIANT for variant selection"),
950
+ defaultValue: z
951
+ .union([z.string(), z.number(), z.boolean()])
952
+ .describe("Default value for the property. BOOLEAN: true/false, TEXT: string, INSTANCE_SWAP: component key, VARIANT: variant value"),
953
+ }, async ({ nodeId, propertyName, type, defaultValue }) => {
954
+ try {
955
+ const connector = await getDesktopConnector();
956
+ const result = await connector.addComponentProperty(nodeId, propertyName, type, defaultValue);
957
+ if (!result.success) {
958
+ throw new Error(result.error || "Failed to add property");
959
+ }
960
+ return {
961
+ content: [
962
+ {
963
+ type: "text",
964
+ text: JSON.stringify({
965
+ success: true,
966
+ message: "Component property added",
967
+ propertyName: result.propertyName,
968
+ hint: "The property name includes a unique suffix (e.g., 'Show Icon#123:456'). Use the full name for editing/deleting.",
969
+ }),
970
+ },
971
+ ],
972
+ };
973
+ }
974
+ catch (error) {
975
+ logger.error({ error }, "Failed to add component property");
976
+ return {
977
+ content: [
978
+ {
979
+ type: "text",
980
+ text: JSON.stringify({
981
+ error: error instanceof Error ? error.message : String(error),
982
+ hint: "Cannot add properties to variant components. Add to the parent component set instead.",
983
+ }),
984
+ },
985
+ ],
986
+ isError: true,
987
+ };
988
+ }
989
+ });
990
+ // Tool: Edit Component Property
991
+ server.tool("figma_edit_component_property", "Edit an existing component property. Can change the name, default value, or preferred values (for INSTANCE_SWAP). Use the full property name including the unique suffix.", {
992
+ nodeId: z.string().describe("The component or component set node ID"),
993
+ propertyName: z
994
+ .string()
995
+ .describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
996
+ newValue: z
997
+ .object({
998
+ name: z.string().optional().describe("New name for the property"),
999
+ defaultValue: z
1000
+ .union([z.string(), z.number(), z.boolean()])
1001
+ .optional()
1002
+ .describe("New default value"),
1003
+ preferredValues: z
1004
+ .array(z.object({
1005
+ type: z
1006
+ .enum(["COMPONENT", "COMPONENT_SET"])
1007
+ .describe("Type of preferred value"),
1008
+ key: z.string().describe("Component or component set key"),
1009
+ }))
1010
+ .optional()
1011
+ .describe("Preferred values (INSTANCE_SWAP only)"),
1012
+ })
1013
+ .describe("Object with the values to update"),
1014
+ }, async ({ nodeId, propertyName, newValue }) => {
1015
+ try {
1016
+ const connector = await getDesktopConnector();
1017
+ const result = await connector.editComponentProperty(nodeId, propertyName, newValue);
1018
+ if (!result.success) {
1019
+ throw new Error(result.error || "Failed to edit property");
1020
+ }
1021
+ return {
1022
+ content: [
1023
+ {
1024
+ type: "text",
1025
+ text: JSON.stringify({
1026
+ success: true,
1027
+ message: "Component property updated",
1028
+ propertyName: result.propertyName,
1029
+ }),
1030
+ },
1031
+ ],
1032
+ };
1033
+ }
1034
+ catch (error) {
1035
+ logger.error({ error }, "Failed to edit component property");
1036
+ return {
1037
+ content: [
1038
+ {
1039
+ type: "text",
1040
+ text: JSON.stringify({
1041
+ error: error instanceof Error ? error.message : String(error),
1042
+ }),
1043
+ },
1044
+ ],
1045
+ isError: true,
1046
+ };
1047
+ }
1048
+ });
1049
+ // Tool: Delete Component Property
1050
+ server.tool("figma_delete_component_property", "Delete a component property. Only works with BOOLEAN, TEXT, and INSTANCE_SWAP properties (not VARIANT). This is a destructive operation.", {
1051
+ nodeId: z.string().describe("The component or component set node ID"),
1052
+ propertyName: z
1053
+ .string()
1054
+ .describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
1055
+ }, async ({ nodeId, propertyName }) => {
1056
+ try {
1057
+ const connector = await getDesktopConnector();
1058
+ const result = await connector.deleteComponentProperty(nodeId, propertyName);
1059
+ if (!result.success) {
1060
+ throw new Error(result.error || "Failed to delete property");
1061
+ }
1062
+ return {
1063
+ content: [
1064
+ {
1065
+ type: "text",
1066
+ text: JSON.stringify({
1067
+ success: true,
1068
+ message: "Component property deleted",
1069
+ }),
1070
+ },
1071
+ ],
1072
+ };
1073
+ }
1074
+ catch (error) {
1075
+ logger.error({ error }, "Failed to delete component property");
1076
+ return {
1077
+ content: [
1078
+ {
1079
+ type: "text",
1080
+ text: JSON.stringify({
1081
+ error: error instanceof Error ? error.message : String(error),
1082
+ hint: "Cannot delete VARIANT properties. Only BOOLEAN, TEXT, and INSTANCE_SWAP can be deleted.",
1083
+ }),
1084
+ },
1085
+ ],
1086
+ isError: true,
1087
+ };
1088
+ }
1089
+ });
1090
+ // ============================================================================
1091
+ // Node Manipulation Tools
1092
+ // ============================================================================
1093
+ // Tool: Resize Node
1094
+ server.tool("figma_resize_node", "Resize a node to specific dimensions. By default respects child constraints; use withConstraints=false to ignore them.", {
1095
+ nodeId: z.string().describe("The node ID to resize"),
1096
+ width: z.number().describe("New width in pixels"),
1097
+ height: z.number().describe("New height in pixels"),
1098
+ withConstraints: z
1099
+ .boolean()
1100
+ .optional()
1101
+ .default(true)
1102
+ .describe("Whether to apply child constraints during resize (default: true)"),
1103
+ }, async ({ nodeId, width, height, withConstraints }) => {
1104
+ try {
1105
+ const connector = await getDesktopConnector();
1106
+ const result = await connector.resizeNode(nodeId, width, height, withConstraints);
1107
+ if (!result.success) {
1108
+ throw new Error(result.error || "Failed to resize node");
1109
+ }
1110
+ return {
1111
+ content: [
1112
+ {
1113
+ type: "text",
1114
+ text: JSON.stringify({
1115
+ success: true,
1116
+ message: `Node resized to ${width}x${height}`,
1117
+ node: result.node,
1118
+ }),
1119
+ },
1120
+ ],
1121
+ };
1122
+ }
1123
+ catch (error) {
1124
+ logger.error({ error }, "Failed to resize node");
1125
+ return {
1126
+ content: [
1127
+ {
1128
+ type: "text",
1129
+ text: JSON.stringify({
1130
+ error: error instanceof Error ? error.message : String(error),
1131
+ }),
1132
+ },
1133
+ ],
1134
+ isError: true,
1135
+ };
1136
+ }
1137
+ });
1138
+ // Tool: Move Node
1139
+ server.tool("figma_move_node", "Move a node to a new position within its parent.", {
1140
+ nodeId: z.string().describe("The node ID to move"),
1141
+ x: z.number().describe("New X position"),
1142
+ y: z.number().describe("New Y position"),
1143
+ }, async ({ nodeId, x, y }) => {
1144
+ try {
1145
+ const connector = await getDesktopConnector();
1146
+ const result = await connector.moveNode(nodeId, x, y);
1147
+ if (!result.success) {
1148
+ throw new Error(result.error || "Failed to move node");
1149
+ }
1150
+ return {
1151
+ content: [
1152
+ {
1153
+ type: "text",
1154
+ text: JSON.stringify({
1155
+ success: true,
1156
+ message: `Node moved to (${x}, ${y})`,
1157
+ node: result.node,
1158
+ }),
1159
+ },
1160
+ ],
1161
+ };
1162
+ }
1163
+ catch (error) {
1164
+ logger.error({ error }, "Failed to move node");
1165
+ return {
1166
+ content: [
1167
+ {
1168
+ type: "text",
1169
+ text: JSON.stringify({
1170
+ error: error instanceof Error ? error.message : String(error),
1171
+ }),
1172
+ },
1173
+ ],
1174
+ isError: true,
1175
+ };
1176
+ }
1177
+ });
1178
+ // Tool: Set Node Fills
1179
+ server.tool("figma_set_fills", "Set the fill colors on a node. Accepts hex color strings (e.g., '#FF0000') or full paint objects.", {
1180
+ nodeId: z.string().describe("The node ID to modify"),
1181
+ fills: z
1182
+ .array(z.object({
1183
+ type: z
1184
+ .literal("SOLID")
1185
+ .describe("Fill type (currently only SOLID supported)"),
1186
+ color: z
1187
+ .string()
1188
+ .describe("Hex color string (e.g., '#FF0000', '#FF000080' for transparency)"),
1189
+ opacity: z
1190
+ .number()
1191
+ .optional()
1192
+ .describe("Opacity 0-1 (default: 1)"),
1193
+ }))
1194
+ .describe("Array of fill objects"),
1195
+ }, async ({ nodeId, fills }) => {
1196
+ try {
1197
+ const connector = await getDesktopConnector();
1198
+ const result = await connector.setNodeFills(nodeId, fills);
1199
+ if (!result.success) {
1200
+ throw new Error(result.error || "Failed to set fills");
1201
+ }
1202
+ return {
1203
+ content: [
1204
+ {
1205
+ type: "text",
1206
+ text: JSON.stringify({
1207
+ success: true,
1208
+ message: "Fills updated",
1209
+ node: result.node,
1210
+ }),
1211
+ },
1212
+ ],
1213
+ };
1214
+ }
1215
+ catch (error) {
1216
+ logger.error({ error }, "Failed to set fills");
1217
+ return {
1218
+ content: [
1219
+ {
1220
+ type: "text",
1221
+ text: JSON.stringify({
1222
+ error: error instanceof Error ? error.message : String(error),
1223
+ }),
1224
+ },
1225
+ ],
1226
+ isError: true,
1227
+ };
1228
+ }
1229
+ });
1230
+ // Tool: Set Image Fill on nodes
1231
+ server.tool("figma_set_image_fill", "Set an image fill on one or more Figma nodes. The imageData parameter accepts a base64-encoded " +
1232
+ "image string (JPEG/PNG). The image is decoded in the browser bridge and passed " +
1233
+ "as raw bytes to the Figma plugin. Requires Desktop Bridge plugin.", {
1234
+ nodeIds: z.array(z.string()).describe("Array of node IDs to apply the image fill to"),
1235
+ imageData: z.string().describe("Base64-encoded image data (JPEG/PNG)"),
1236
+ scaleMode: z.enum(["FILL", "FIT", "CROP", "TILE"]).optional().describe("How the image fills the node (default: FILL)"),
1237
+ }, async ({ nodeIds, imageData, scaleMode }) => {
1238
+ try {
1239
+ const connector = await getDesktopConnector();
1240
+ const result = await connector.setImageFill(nodeIds, imageData, scaleMode || "FILL");
1241
+ if (!result.success) {
1242
+ throw new Error(result.error || "Failed to set image fill");
1243
+ }
1244
+ return {
1245
+ content: [
1246
+ {
1247
+ type: "text",
1248
+ text: JSON.stringify({
1249
+ success: true,
1250
+ message: `Image fill applied to ${result.updatedCount || 0} node(s)`,
1251
+ imageHash: result.imageHash,
1252
+ nodes: result.nodes,
1253
+ }),
1254
+ },
1255
+ ],
1256
+ };
1257
+ }
1258
+ catch (error) {
1259
+ logger.error({ error }, "Failed to set image fill");
1260
+ return {
1261
+ content: [
1262
+ {
1263
+ type: "text",
1264
+ text: JSON.stringify({
1265
+ error: error instanceof Error ? error.message : String(error),
1266
+ }),
1267
+ },
1268
+ ],
1269
+ isError: true,
1270
+ };
1271
+ }
1272
+ });
1273
+ // Tool: Set Node Strokes
1274
+ server.tool("figma_set_strokes", "Set the stroke (border) on a node. Accepts hex color strings and optional stroke weight.", {
1275
+ nodeId: z.string().describe("The node ID to modify"),
1276
+ strokes: z
1277
+ .array(z.object({
1278
+ type: z.literal("SOLID").describe("Stroke type"),
1279
+ color: z.string().describe("Hex color string"),
1280
+ opacity: z.number().optional().describe("Opacity 0-1"),
1281
+ }))
1282
+ .describe("Array of stroke objects"),
1283
+ strokeWeight: z
1284
+ .number()
1285
+ .optional()
1286
+ .describe("Stroke thickness in pixels"),
1287
+ }, async ({ nodeId, strokes, strokeWeight }) => {
1288
+ try {
1289
+ const connector = await getDesktopConnector();
1290
+ const result = await connector.setNodeStrokes(nodeId, strokes, strokeWeight);
1291
+ if (!result.success) {
1292
+ throw new Error(result.error || "Failed to set strokes");
1293
+ }
1294
+ return {
1295
+ content: [
1296
+ {
1297
+ type: "text",
1298
+ text: JSON.stringify({
1299
+ success: true,
1300
+ message: "Strokes updated",
1301
+ node: result.node,
1302
+ }),
1303
+ },
1304
+ ],
1305
+ };
1306
+ }
1307
+ catch (error) {
1308
+ logger.error({ error }, "Failed to set strokes");
1309
+ return {
1310
+ content: [
1311
+ {
1312
+ type: "text",
1313
+ text: JSON.stringify({
1314
+ error: error instanceof Error ? error.message : String(error),
1315
+ }),
1316
+ },
1317
+ ],
1318
+ isError: true,
1319
+ };
1320
+ }
1321
+ });
1322
+ // Tool: Clone Node
1323
+ server.tool("figma_clone_node", "Duplicate a node. The clone is placed at a slight offset from the original.", {
1324
+ nodeId: z.string().describe("The node ID to clone"),
1325
+ }, async ({ nodeId }) => {
1326
+ try {
1327
+ const connector = await getDesktopConnector();
1328
+ const result = await connector.cloneNode(nodeId);
1329
+ if (!result.success) {
1330
+ throw new Error(result.error || "Failed to clone node");
1331
+ }
1332
+ return {
1333
+ content: [
1334
+ {
1335
+ type: "text",
1336
+ text: JSON.stringify({
1337
+ success: true,
1338
+ message: "Node cloned",
1339
+ clonedNode: result.node,
1340
+ }),
1341
+ },
1342
+ ],
1343
+ };
1344
+ }
1345
+ catch (error) {
1346
+ logger.error({ error }, "Failed to clone node");
1347
+ return {
1348
+ content: [
1349
+ {
1350
+ type: "text",
1351
+ text: JSON.stringify({
1352
+ error: error instanceof Error ? error.message : String(error),
1353
+ }),
1354
+ },
1355
+ ],
1356
+ isError: true,
1357
+ };
1358
+ }
1359
+ });
1360
+ // Tool: Delete Node
1361
+ server.tool("figma_delete_node", "Delete a node from the canvas. WARNING: This is a destructive operation (can be undone with Figma's undo).", {
1362
+ nodeId: z.string().describe("The node ID to delete"),
1363
+ }, async ({ nodeId }) => {
1364
+ try {
1365
+ const connector = await getDesktopConnector();
1366
+ const result = await connector.deleteNode(nodeId);
1367
+ if (!result.success) {
1368
+ throw new Error(result.error || "Failed to delete node");
1369
+ }
1370
+ return {
1371
+ content: [
1372
+ {
1373
+ type: "text",
1374
+ text: JSON.stringify({
1375
+ success: true,
1376
+ message: "Node deleted",
1377
+ deleted: result.deleted,
1378
+ }),
1379
+ },
1380
+ ],
1381
+ };
1382
+ }
1383
+ catch (error) {
1384
+ logger.error({ error }, "Failed to delete node");
1385
+ return {
1386
+ content: [
1387
+ {
1388
+ type: "text",
1389
+ text: JSON.stringify({
1390
+ error: error instanceof Error ? error.message : String(error),
1391
+ }),
1392
+ },
1393
+ ],
1394
+ isError: true,
1395
+ };
1396
+ }
1397
+ });
1398
+ // Tool: Rename Node
1399
+ server.tool("figma_rename_node", "Rename a node in the layer panel.", {
1400
+ nodeId: z.string().describe("The node ID to rename"),
1401
+ newName: z.string().describe("The new name for the node"),
1402
+ }, async ({ nodeId, newName }) => {
1403
+ try {
1404
+ const connector = await getDesktopConnector();
1405
+ const result = await connector.renameNode(nodeId, newName);
1406
+ if (!result.success) {
1407
+ throw new Error(result.error || "Failed to rename node");
1408
+ }
1409
+ return {
1410
+ content: [
1411
+ {
1412
+ type: "text",
1413
+ text: JSON.stringify({
1414
+ success: true,
1415
+ message: `Node renamed to "${newName}"`,
1416
+ node: result.node,
1417
+ }),
1418
+ },
1419
+ ],
1420
+ };
1421
+ }
1422
+ catch (error) {
1423
+ logger.error({ error }, "Failed to rename node");
1424
+ return {
1425
+ content: [
1426
+ {
1427
+ type: "text",
1428
+ text: JSON.stringify({
1429
+ error: error instanceof Error ? error.message : String(error),
1430
+ }),
1431
+ },
1432
+ ],
1433
+ isError: true,
1434
+ };
1435
+ }
1436
+ });
1437
+ // Tool: Set Text Content
1438
+ server.tool("figma_set_text", "Set the text content of a text node. Optionally adjust font size.", {
1439
+ nodeId: z.string().describe("The text node ID"),
1440
+ text: z.string().describe("The new text content"),
1441
+ fontSize: z.number().optional().describe("Optional font size to set"),
1442
+ }, async ({ nodeId, text, fontSize }) => {
1443
+ try {
1444
+ const connector = await getDesktopConnector();
1445
+ const result = await connector.setTextContent(nodeId, text, fontSize ? { fontSize } : undefined);
1446
+ if (!result.success) {
1447
+ throw new Error(result.error || "Failed to set text");
1448
+ }
1449
+ return {
1450
+ content: [
1451
+ {
1452
+ type: "text",
1453
+ text: JSON.stringify({
1454
+ success: true,
1455
+ message: "Text content updated",
1456
+ node: result.node,
1457
+ }),
1458
+ },
1459
+ ],
1460
+ };
1461
+ }
1462
+ catch (error) {
1463
+ logger.error({ error }, "Failed to set text content");
1464
+ return {
1465
+ content: [
1466
+ {
1467
+ type: "text",
1468
+ text: JSON.stringify({
1469
+ error: error instanceof Error ? error.message : String(error),
1470
+ hint: "Make sure the node is a TEXT node",
1471
+ }),
1472
+ },
1473
+ ],
1474
+ isError: true,
1475
+ };
1476
+ }
1477
+ });
1478
+ // Tool: Create Child Node
1479
+ server.tool("figma_create_child", "Create a new child node inside a parent container. Useful for adding shapes, text, or frames to existing structures.", {
1480
+ parentId: z.string().describe("The parent node ID"),
1481
+ nodeType: z
1482
+ .enum(["RECTANGLE", "ELLIPSE", "FRAME", "TEXT", "LINE"])
1483
+ .describe("Type of node to create"),
1484
+ properties: z
1485
+ .object({
1486
+ name: z.string().optional().describe("Name for the new node"),
1487
+ x: z.number().optional().describe("X position within parent"),
1488
+ y: z.number().optional().describe("Y position within parent"),
1489
+ width: z.number().optional().describe("Width (default: 100)"),
1490
+ height: z.number().optional().describe("Height (default: 100)"),
1491
+ fills: z
1492
+ .array(z.object({
1493
+ type: z.literal("SOLID"),
1494
+ color: z.string(),
1495
+ }))
1496
+ .optional()
1497
+ .describe("Fill colors (hex strings)"),
1498
+ text: z
1499
+ .string()
1500
+ .optional()
1501
+ .describe("Text content (for TEXT nodes only)"),
1502
+ })
1503
+ .optional()
1504
+ .describe("Properties for the new node"),
1505
+ }, async ({ parentId, nodeType, properties }) => {
1506
+ try {
1507
+ const connector = await getDesktopConnector();
1508
+ const result = await connector.createChildNode(parentId, nodeType, properties);
1509
+ if (!result.success) {
1510
+ throw new Error(result.error || "Failed to create node");
1511
+ }
1512
+ return {
1513
+ content: [
1514
+ {
1515
+ type: "text",
1516
+ text: JSON.stringify({
1517
+ success: true,
1518
+ message: `Created ${nodeType} node`,
1519
+ node: result.node,
1520
+ }),
1521
+ },
1522
+ ],
1523
+ };
1524
+ }
1525
+ catch (error) {
1526
+ logger.error({ error }, "Failed to create child node");
1527
+ return {
1528
+ content: [
1529
+ {
1530
+ type: "text",
1531
+ text: JSON.stringify({
1532
+ error: error instanceof Error ? error.message : String(error),
1533
+ hint: "Make sure the parent node supports children (frames, groups, etc.)",
1534
+ }),
1535
+ },
1536
+ ],
1537
+ isError: true,
1538
+ };
1539
+ }
1540
+ });
1541
+ // ============================================================================
1542
+ // Component Set Arrangement Tool
1543
+ // ============================================================================
1544
+ // Tool: Arrange Component Set (Professional Layout with Native Visualization)
1545
+ // Recreates component set using figma.combineAsVariants() for proper purple dashed frame
1546
+ server.tool("figma_arrange_component_set", `Organize a component set with Figma's native purple dashed visualization. Use after creating variants, adding states (hover/disabled/pressed), or when component sets need cleanup.
1547
+
1548
+ Recreates the set using figma.combineAsVariants() for proper Figma integration, applies purple dashed border styling, and arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size). Creates a white container with title, row/column labels, and the component set.`, {
1549
+ componentSetId: z
1550
+ .string()
1551
+ .optional()
1552
+ .describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
1553
+ componentSetName: z
1554
+ .string()
1555
+ .optional()
1556
+ .describe("Name of the component set to find. Used if componentSetId not provided."),
1557
+ options: z
1558
+ .object({
1559
+ gap: z
1560
+ .number()
1561
+ .optional()
1562
+ .default(24)
1563
+ .describe("Gap between grid cells in pixels (default: 24)"),
1564
+ cellPadding: z
1565
+ .number()
1566
+ .optional()
1567
+ .default(20)
1568
+ .describe("Padding inside each cell around the variant (default: 20)"),
1569
+ columnProperty: z
1570
+ .string()
1571
+ .optional()
1572
+ .describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
1573
+ })
1574
+ .optional()
1575
+ .describe("Layout options"),
1576
+ }, async ({ componentSetId, componentSetName, options }) => {
1577
+ try {
1578
+ const connector = await getDesktopConnector();
1579
+ // Build the code to execute in Figma
1580
+ const code = `
1581
+ // ============================================================================
1582
+ // COMPONENT SET ARRANGEMENT WITH PROPER LABELS AND CONTAINER
1583
+ // Creates: White container frame -> Row labels (left) -> Column headers (top) -> Component set (center)
1584
+ // Uses auto-layout for proper alignment of labels with grid cells
1585
+ // ============================================================================
1586
+
1587
+ // Configuration
1588
+ const config = ${JSON.stringify(options || {})};
1589
+ const gap = config.gap ?? 24;
1590
+ const cellPadding = config.cellPadding ?? 20;
1591
+ const columnProperty = config.columnProperty || null;
1592
+
1593
+ // Layout constants
1594
+ const LABEL_FONT_SIZE = 12;
1595
+ const LABEL_COLOR = { r: 0.4, g: 0.4, b: 0.4 }; // Gray text
1596
+ const TITLE_FONT_SIZE = 24;
1597
+ const TITLE_COLOR = { r: 0.1, g: 0.1, b: 0.1 }; // Dark text
1598
+ const CONTAINER_PADDING = 40;
1599
+ const LABEL_GAP = 16; // Gap between labels and component set
1600
+ const COLUMN_HEADER_HEIGHT = 32;
1601
+
1602
+ // Find the component set
1603
+ let componentSet = null;
1604
+ const csId = ${JSON.stringify(componentSetId || null)};
1605
+ const csName = ${JSON.stringify(componentSetName || null)};
1606
+
1607
+ if (csId) {
1608
+ componentSet = await figma.getNodeByIdAsync(csId);
1609
+ } else if (csName) {
1610
+ const allNodes = figma.currentPage.findAll(n => n.type === "COMPONENT_SET" && n.name === csName);
1611
+ componentSet = allNodes[0];
1612
+ } else {
1613
+ const selection = figma.currentPage.selection;
1614
+ componentSet = selection.find(n => n.type === "COMPONENT_SET");
1615
+ }
1616
+
1617
+ if (!componentSet || componentSet.type !== "COMPONENT_SET") {
1618
+ return { error: "Component set not found. Provide componentSetId, componentSetName, or select a component set." };
1619
+ }
1620
+
1621
+ const page = figma.currentPage;
1622
+ const csOriginalX = componentSet.x;
1623
+ const csOriginalY = componentSet.y;
1624
+ const csOriginalName = componentSet.name;
1625
+
1626
+ // Get all variant components
1627
+ const variants = componentSet.children.filter(n => n.type === "COMPONENT");
1628
+ if (variants.length === 0) {
1629
+ return { error: "No variants found in component set" };
1630
+ }
1631
+
1632
+ // Parse variant properties from names
1633
+ const parseVariantName = (name) => {
1634
+ const props = {};
1635
+ const parts = name.split(", ");
1636
+ for (const part of parts) {
1637
+ const [key, value] = part.split("=");
1638
+ if (key && value) {
1639
+ props[key.trim()] = value.trim();
1640
+ }
1641
+ }
1642
+ return props;
1643
+ };
1644
+
1645
+ // Collect all properties and their unique values (preserving order)
1646
+ const propertyValues = {};
1647
+ const propertyOrder = [];
1648
+ for (const variant of variants) {
1649
+ const props = parseVariantName(variant.name);
1650
+ for (const [key, value] of Object.entries(props)) {
1651
+ if (!propertyValues[key]) {
1652
+ propertyValues[key] = new Set();
1653
+ propertyOrder.push(key);
1654
+ }
1655
+ propertyValues[key].add(value);
1656
+ }
1657
+ }
1658
+ for (const key of Object.keys(propertyValues)) {
1659
+ propertyValues[key] = Array.from(propertyValues[key]);
1660
+ }
1661
+
1662
+ // Determine grid structure: columns = last property (usually State), rows = other properties
1663
+ const columnProp = columnProperty || propertyOrder[propertyOrder.length - 1];
1664
+ const columnValues = propertyValues[columnProp] || [];
1665
+ const rowProps = propertyOrder.filter(p => p !== columnProp);
1666
+
1667
+ // Generate all row combinations
1668
+ const generateRowCombinations = (props, values) => {
1669
+ if (props.length === 0) return [{}];
1670
+ if (props.length === 1) {
1671
+ return values[props[0]].map(v => ({ [props[0]]: v }));
1672
+ }
1673
+ const result = [];
1674
+ const firstProp = props[0];
1675
+ const restProps = props.slice(1);
1676
+ const restCombos = generateRowCombinations(restProps, values);
1677
+ for (const value of values[firstProp]) {
1678
+ for (const combo of restCombos) {
1679
+ result.push({ [firstProp]: value, ...combo });
1680
+ }
1681
+ }
1682
+ return result;
1683
+ };
1684
+ const rowCombinations = generateRowCombinations(rowProps, propertyValues);
1685
+
1686
+ const totalCols = columnValues.length;
1687
+ const totalRows = rowCombinations.length;
1688
+
1689
+ // Calculate max variant dimensions
1690
+ let maxVariantWidth = 0;
1691
+ let maxVariantHeight = 0;
1692
+ for (const v of variants) {
1693
+ if (v.width > maxVariantWidth) maxVariantWidth = v.width;
1694
+ if (v.height > maxVariantHeight) maxVariantHeight = v.height;
1695
+ }
1696
+
1697
+ // Calculate cell dimensions (each cell in the grid)
1698
+ const cellWidth = Math.ceil(maxVariantWidth + cellPadding);
1699
+ const cellHeight = Math.ceil(maxVariantHeight + cellPadding);
1700
+
1701
+ // Calculate component set dimensions
1702
+ const edgePadding = 24; // Padding inside component set
1703
+ const csWidth = (totalCols * cellWidth) + ((totalCols - 1) * gap) + (edgePadding * 2);
1704
+ const csHeight = (totalRows * cellHeight) + ((totalRows - 1) * gap) + (edgePadding * 2);
1705
+
1706
+ // ============================================================================
1707
+ // STEP 1: Remove old labels and container frames from previous arrangements
1708
+ // ============================================================================
1709
+ const oldElements = page.children.filter(n =>
1710
+ (n.type === "TEXT" && (n.name.startsWith("Row: ") || n.name.startsWith("Col: "))) ||
1711
+ (n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"))
1712
+ );
1713
+ for (const el of oldElements) {
1714
+ el.remove();
1715
+ }
1716
+
1717
+ // ============================================================================
1718
+ // STEP 2: Clone variants and recreate component set with native visualization
1719
+ // ============================================================================
1720
+ const clonedVariants = [];
1721
+ for (const variant of variants) {
1722
+ const clone = variant.clone();
1723
+ page.appendChild(clone);
1724
+ clonedVariants.push(clone);
1725
+ }
1726
+
1727
+ // Delete the old component set
1728
+ componentSet.remove();
1729
+
1730
+ // Recreate using figma.combineAsVariants() for native purple dashed frame
1731
+ const newComponentSet = figma.combineAsVariants(clonedVariants, page);
1732
+ newComponentSet.name = csOriginalName;
1733
+
1734
+ // Apply purple dashed border (Figma's native component set styling)
1735
+ newComponentSet.strokes = [{
1736
+ type: 'SOLID',
1737
+ color: { r: 151/255, g: 71/255, b: 255/255 } // Figma's purple: #9747FF
1738
+ }];
1739
+ newComponentSet.dashPattern = [10, 5];
1740
+ newComponentSet.strokeWeight = 1;
1741
+ newComponentSet.strokeAlign = "INSIDE";
1742
+
1743
+ // ============================================================================
1744
+ // STEP 3: Arrange variants in grid pattern inside component set
1745
+ // ============================================================================
1746
+ const newVariants = newComponentSet.children.filter(n => n.type === "COMPONENT");
1747
+
1748
+ for (const variant of newVariants) {
1749
+ const props = parseVariantName(variant.name);
1750
+ const colValue = props[columnProp];
1751
+ const colIdx = columnValues.indexOf(colValue);
1752
+
1753
+ // Find matching row
1754
+ let rowIdx = -1;
1755
+ for (let i = 0; i < rowCombinations.length; i++) {
1756
+ const combo = rowCombinations[i];
1757
+ let match = true;
1758
+ for (const [key, value] of Object.entries(combo)) {
1759
+ if (props[key] !== value) {
1760
+ match = false;
1761
+ break;
1762
+ }
1763
+ }
1764
+ if (match) {
1765
+ rowIdx = i;
1766
+ break;
1767
+ }
1768
+ }
1769
+
1770
+ if (colIdx >= 0 && rowIdx >= 0) {
1771
+ // Calculate cell position
1772
+ const cellX = edgePadding + colIdx * (cellWidth + gap);
1773
+ const cellY = edgePadding + rowIdx * (cellHeight + gap);
1774
+
1775
+ // Center variant within cell
1776
+ const variantX = Math.round(cellX + (cellWidth - variant.width) / 2);
1777
+ const variantY = Math.round(cellY + (cellHeight - variant.height) / 2);
1778
+
1779
+ variant.x = variantX;
1780
+ variant.y = variantY;
1781
+ }
1782
+ }
1783
+
1784
+ // Resize component set to fit grid
1785
+ newComponentSet.resize(csWidth, csHeight);
1786
+
1787
+ // ============================================================================
1788
+ // STEP 4: Create white container frame with proper structure
1789
+ // ============================================================================
1790
+
1791
+ // Load font for labels
1792
+ await figma.loadFontAsync({ family: "Inter", style: "Regular" });
1793
+ await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" });
1794
+
1795
+ // Create the main container frame (white background)
1796
+ const containerFrame = figma.createFrame();
1797
+ containerFrame.name = "Component Container";
1798
+ containerFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; // White
1799
+ containerFrame.cornerRadius = 8;
1800
+ containerFrame.layoutMode = 'VERTICAL';
1801
+ containerFrame.primaryAxisSizingMode = 'AUTO';
1802
+ containerFrame.counterAxisSizingMode = 'AUTO';
1803
+ containerFrame.paddingTop = CONTAINER_PADDING;
1804
+ containerFrame.paddingRight = CONTAINER_PADDING;
1805
+ containerFrame.paddingBottom = CONTAINER_PADDING;
1806
+ containerFrame.paddingLeft = CONTAINER_PADDING;
1807
+ containerFrame.itemSpacing = 24;
1808
+
1809
+ // Add title
1810
+ const titleText = figma.createText();
1811
+ titleText.name = "Title";
1812
+ titleText.characters = csOriginalName;
1813
+ titleText.fontSize = TITLE_FONT_SIZE;
1814
+ titleText.fontName = { family: "Inter", style: "Semi Bold" };
1815
+ titleText.fills = [{ type: 'SOLID', color: TITLE_COLOR }];
1816
+ // Append to parent FIRST, then set layoutSizing
1817
+ containerFrame.appendChild(titleText);
1818
+ titleText.layoutSizingHorizontal = 'HUG';
1819
+ titleText.layoutSizingVertical = 'HUG';
1820
+
1821
+ // Create content row (horizontal: row labels + grid column)
1822
+ const contentRow = figma.createFrame();
1823
+ contentRow.name = "Content Row";
1824
+ contentRow.fills = []; // Transparent
1825
+ contentRow.layoutMode = 'HORIZONTAL';
1826
+ contentRow.primaryAxisSizingMode = 'AUTO';
1827
+ contentRow.counterAxisSizingMode = 'AUTO';
1828
+ contentRow.itemSpacing = LABEL_GAP;
1829
+ contentRow.counterAxisAlignItems = 'MIN'; // Align to top
1830
+ containerFrame.appendChild(contentRow);
1831
+
1832
+ // ============================================================================
1833
+ // STEP 5: Create row labels column (left side)
1834
+ // ============================================================================
1835
+ const rowLabelsFrame = figma.createFrame();
1836
+ rowLabelsFrame.name = "Row Labels";
1837
+ rowLabelsFrame.fills = []; // Transparent
1838
+ rowLabelsFrame.layoutMode = 'VERTICAL';
1839
+ rowLabelsFrame.primaryAxisSizingMode = 'AUTO';
1840
+ rowLabelsFrame.counterAxisSizingMode = 'AUTO';
1841
+ rowLabelsFrame.counterAxisAlignItems = 'MAX'; // Right-align text
1842
+ rowLabelsFrame.itemSpacing = 0; // No spacing - we'll use fixed heights
1843
+
1844
+ // Add spacer for column headers alignment
1845
+ // Must account for: column header height + gap + component set's internal edgePadding
1846
+ const rowLabelSpacer = figma.createFrame();
1847
+ rowLabelSpacer.name = "Spacer";
1848
+ rowLabelSpacer.fills = [];
1849
+ rowLabelSpacer.resize(10, COLUMN_HEADER_HEIGHT + gap + edgePadding); // Align with first row inside component set
1850
+ rowLabelsFrame.appendChild(rowLabelSpacer);
1851
+ // IMPORTANT: Set layoutSizing AFTER appendChild (node must be in auto-layout parent first)
1852
+ rowLabelSpacer.layoutSizingVertical = 'FIXED';
1853
+
1854
+ // Create row labels - each with VERTICAL layout for direct vertical centering
1855
+ // Using VERTICAL layout: primaryAxis = vertical, counterAxis = horizontal
1856
+ // So primaryAxisAlignItems = 'CENTER' directly controls vertical centering
1857
+ for (let i = 0; i < rowCombinations.length; i++) {
1858
+ const combo = rowCombinations[i];
1859
+ const labelText = rowProps.map(p => combo[p]).join(" / ");
1860
+ const isLastRow = (i === rowCombinations.length - 1);
1861
+
1862
+ // Create a frame to hold the label with VERTICAL layout
1863
+ const rowLabelContainer = figma.createFrame();
1864
+ rowLabelContainer.name = "Row: " + labelText;
1865
+ rowLabelContainer.fills = [];
1866
+ rowLabelContainer.layoutMode = 'VERTICAL'; // VERTICAL so primaryAxis controls Y
1867
+ rowLabelContainer.primaryAxisSizingMode = 'FIXED'; // CRITICAL: Don't hug content, maintain fixed height
1868
+ rowLabelContainer.primaryAxisAlignItems = 'CENTER'; // CENTER = vertically centered within fixed height
1869
+ rowLabelContainer.counterAxisAlignItems = 'MAX'; // MAX = right-aligned horizontally
1870
+
1871
+ // Fixed height = cellHeight only (gap handled separately below)
1872
+ rowLabelContainer.resize(10, cellHeight);
1873
+
1874
+ const label = figma.createText();
1875
+ label.characters = labelText;
1876
+ label.fontSize = LABEL_FONT_SIZE;
1877
+ label.fontName = { family: "Inter", style: "Regular" };
1878
+ label.fills = [{ type: 'SOLID', color: LABEL_COLOR }];
1879
+ label.textAlignHorizontal = 'RIGHT';
1880
+ rowLabelContainer.appendChild(label);
1881
+
1882
+ // Append to parent FIRST, then set layoutSizing properties
1883
+ rowLabelsFrame.appendChild(rowLabelContainer);
1884
+ rowLabelContainer.layoutSizingHorizontal = 'HUG';
1885
+ rowLabelContainer.layoutSizingVertical = 'FIXED';
1886
+
1887
+ // Add gap spacer AFTER the row label (except for the last row)
1888
+ // This separates the gap from the centering calculation entirely
1889
+ if (!isLastRow) {
1890
+ const gapSpacer = figma.createFrame();
1891
+ gapSpacer.name = "Row Gap";
1892
+ gapSpacer.fills = [];
1893
+ gapSpacer.resize(1, gap);
1894
+ rowLabelsFrame.appendChild(gapSpacer);
1895
+ // Plain frames can only use FIXED or FILL (not HUG)
1896
+ gapSpacer.layoutSizingHorizontal = 'FIXED';
1897
+ gapSpacer.layoutSizingVertical = 'FIXED';
1898
+ }
1899
+ }
1900
+
1901
+ contentRow.appendChild(rowLabelsFrame);
1902
+
1903
+ // ============================================================================
1904
+ // STEP 6: Create grid column (column headers + component set)
1905
+ // ============================================================================
1906
+ const gridColumn = figma.createFrame();
1907
+ gridColumn.name = "Grid Column";
1908
+ gridColumn.fills = []; // Transparent
1909
+ gridColumn.layoutMode = 'VERTICAL';
1910
+ gridColumn.primaryAxisSizingMode = 'AUTO';
1911
+ gridColumn.counterAxisSizingMode = 'AUTO';
1912
+ gridColumn.itemSpacing = gap;
1913
+
1914
+ // Create column headers row
1915
+ const columnHeadersRow = figma.createFrame();
1916
+ columnHeadersRow.name = "Column Headers";
1917
+ columnHeadersRow.fills = [];
1918
+ columnHeadersRow.layoutMode = 'HORIZONTAL';
1919
+ columnHeadersRow.resize(csWidth, COLUMN_HEADER_HEIGHT);
1920
+ columnHeadersRow.itemSpacing = 0; // No spacing - we control widths precisely
1921
+ columnHeadersRow.paddingLeft = edgePadding; // Match component set edge padding
1922
+ columnHeadersRow.paddingRight = edgePadding;
1923
+
1924
+ // Create column header labels - each with width matching cell + gap
1925
+ for (let i = 0; i < columnValues.length; i++) {
1926
+ const colValue = columnValues[i];
1927
+ const isLastCol = (i === columnValues.length - 1);
1928
+
1929
+ const colHeaderContainer = figma.createFrame();
1930
+ colHeaderContainer.name = "Col: " + colValue;
1931
+ colHeaderContainer.fills = [];
1932
+ colHeaderContainer.layoutMode = 'HORIZONTAL';
1933
+ colHeaderContainer.primaryAxisAlignItems = 'CENTER'; // Center horizontally
1934
+ colHeaderContainer.counterAxisAlignItems = 'MAX'; // Align to bottom
1935
+
1936
+ // Set width to match cell + gap (except last column)
1937
+ // Use paddingRight to push the gap to the RIGHT of the centered text area
1938
+ const colWidth = isLastCol ? cellWidth : cellWidth + gap;
1939
+ colHeaderContainer.resize(colWidth, COLUMN_HEADER_HEIGHT);
1940
+ if (!isLastCol) {
1941
+ colHeaderContainer.paddingRight = gap; // Gap goes right, text centers in cellWidth
1942
+ }
1943
+
1944
+ const label = figma.createText();
1945
+ label.characters = colValue;
1946
+ label.fontSize = LABEL_FONT_SIZE;
1947
+ label.fontName = { family: "Inter", style: "Regular" };
1948
+ label.fills = [{ type: 'SOLID', color: LABEL_COLOR }];
1949
+ label.textAlignHorizontal = 'CENTER';
1950
+ colHeaderContainer.appendChild(label);
1951
+
1952
+ // Append to parent FIRST, then set layoutSizing
1953
+ columnHeadersRow.appendChild(colHeaderContainer);
1954
+ colHeaderContainer.layoutSizingHorizontal = 'FIXED';
1955
+ colHeaderContainer.layoutSizingVertical = 'FILL';
1956
+ }
1957
+
1958
+ // Append to parent FIRST, then set layoutSizing
1959
+ gridColumn.appendChild(columnHeadersRow);
1960
+ columnHeadersRow.layoutSizingHorizontal = 'FIXED';
1961
+ columnHeadersRow.layoutSizingVertical = 'FIXED';
1962
+
1963
+ // Create a wrapper frame to hold the component set (since component sets don't work well in auto-layout)
1964
+ const componentSetWrapper = figma.createFrame();
1965
+ componentSetWrapper.name = "Component Set Wrapper";
1966
+ componentSetWrapper.fills = [];
1967
+ componentSetWrapper.resize(csWidth, csHeight);
1968
+
1969
+ // Move component set inside wrapper (positioned at 0,0)
1970
+ componentSetWrapper.appendChild(newComponentSet);
1971
+ newComponentSet.x = 0;
1972
+ newComponentSet.y = 0;
1973
+
1974
+ // Append to parent FIRST, then set layoutSizing
1975
+ gridColumn.appendChild(componentSetWrapper);
1976
+ componentSetWrapper.layoutSizingHorizontal = 'FIXED';
1977
+ componentSetWrapper.layoutSizingVertical = 'FIXED';
1978
+
1979
+ contentRow.appendChild(gridColumn);
1980
+
1981
+ // Position container at original location
1982
+ containerFrame.x = csOriginalX - CONTAINER_PADDING - 120; // Account for row labels width
1983
+ containerFrame.y = csOriginalY - CONTAINER_PADDING - TITLE_FONT_SIZE - 24 - COLUMN_HEADER_HEIGHT - gap;
1984
+
1985
+ // Select and zoom to show result
1986
+ figma.currentPage.selection = [containerFrame];
1987
+ figma.viewport.scrollAndZoomIntoView([containerFrame]);
1988
+
1989
+ return {
1990
+ success: true,
1991
+ message: "Component set arranged with proper container, labels, and alignment",
1992
+ containerId: containerFrame.id,
1993
+ componentSetId: newComponentSet.id,
1994
+ componentSetName: newComponentSet.name,
1995
+ grid: {
1996
+ rows: totalRows,
1997
+ columns: totalCols,
1998
+ cellWidth: cellWidth,
1999
+ cellHeight: cellHeight,
2000
+ gap: gap,
2001
+ columnProperty: columnProp,
2002
+ columnValues: columnValues,
2003
+ rowProperties: rowProps,
2004
+ rowLabels: rowCombinations.map(combo => rowProps.map(p => combo[p]).join(" / "))
2005
+ },
2006
+ componentSetSize: { width: csWidth, height: csHeight },
2007
+ variantCount: newVariants.length,
2008
+ structure: {
2009
+ container: "White frame with title, row labels, column headers, and component set",
2010
+ rowLabels: "Vertically aligned with each row's center",
2011
+ columnHeaders: "Horizontally aligned with each column's center"
2012
+ }
2013
+ };
2014
+ `;
2015
+ const result = await connector.executeCodeViaUI(code, 25000);
2016
+ if (!result.success) {
2017
+ throw new Error(result.error || "Failed to arrange component set");
2018
+ }
2019
+ return {
2020
+ content: [
2021
+ {
2022
+ type: "text",
2023
+ text: JSON.stringify({
2024
+ ...result.result,
2025
+ hint: result.result?.success
2026
+ ? "Component set arranged in a white container frame with properly aligned row and column labels. The purple dashed border is visible. Use figma_capture_screenshot to validate the layout."
2027
+ : undefined,
2028
+ }),
2029
+ },
2030
+ ],
2031
+ };
2032
+ }
2033
+ catch (error) {
2034
+ logger.error({ error }, "Failed to arrange component set");
2035
+ return {
2036
+ content: [
2037
+ {
2038
+ type: "text",
2039
+ text: JSON.stringify({
2040
+ error: error instanceof Error ? error.message : String(error),
2041
+ hint: "Make sure the Desktop Bridge plugin is running and a component set exists.",
2042
+ }),
2043
+ },
2044
+ ],
2045
+ isError: true,
2046
+ };
2047
+ }
2048
+ });
2049
+ // Tool: Lint Design for accessibility and quality issues
2050
+ server.tool("figma_lint_design", "Run accessibility (WCAG) and design quality checks on the current page or a specific node tree. " +
2051
+ "Checks color contrast ratios, text sizing, touch targets, hardcoded values, detached components, " +
2052
+ "naming conventions, and layout quality. Returns categorized findings with severity levels. " +
2053
+ "Use natural language like 'check my design for accessibility issues' or 'lint this page'. " +
2054
+ "Requires Desktop Bridge plugin.", {
2055
+ nodeId: z.string().optional().describe("Node ID to lint (defaults to current page)"),
2056
+ rules: z.array(z.string()).optional().describe("Rule filter: ['all'] (default), ['wcag'], ['design-system'], ['layout'], or specific rule IDs like ['wcag-contrast', 'detached-component']"),
2057
+ maxDepth: z.number().optional().describe("Maximum tree depth to traverse (default: 10)"),
2058
+ maxFindings: z.number().optional().describe("Maximum findings before stopping (default: 100)"),
2059
+ }, async ({ nodeId, rules, maxDepth, maxFindings }) => {
2060
+ try {
2061
+ const connector = await getDesktopConnector();
2062
+ const result = await connector.lintDesign(nodeId, rules || ['all'], maxDepth || 10, maxFindings || 100);
2063
+ if (!result.success) {
2064
+ throw new Error(result.error || "Lint failed");
2065
+ }
2066
+ return {
2067
+ content: [
2068
+ {
2069
+ type: "text",
2070
+ text: JSON.stringify(result.data || result, null, 2),
2071
+ },
2072
+ ],
2073
+ };
2074
+ }
2075
+ catch (error) {
2076
+ logger.error({ error }, "Failed to lint design");
2077
+ return {
2078
+ content: [
2079
+ {
2080
+ type: "text",
2081
+ text: JSON.stringify({
2082
+ error: error instanceof Error ? error.message : String(error),
2083
+ hint: "Make sure the Desktop Bridge plugin is running in your Figma file.",
2084
+ }),
2085
+ },
2086
+ ],
2087
+ isError: true,
2088
+ };
2089
+ }
2090
+ });
2091
+ }
2092
+ //# sourceMappingURL=write-tools.js.map