@muloka/figma-console-mcp 0.1.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 (443) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +898 -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-manager.js +157 -0
  62. package/dist/cloudflare/core/accessibility-tools.js +361 -0
  63. package/dist/cloudflare/core/annotation-tools.js +230 -0
  64. package/dist/cloudflare/core/cloud-websocket-connector.js +419 -0
  65. package/dist/cloudflare/core/cloud-websocket-relay.js +198 -0
  66. package/dist/cloudflare/core/comment-tools.js +292 -0
  67. package/dist/cloudflare/core/config.js +153 -0
  68. package/dist/cloudflare/core/console-monitor.js +427 -0
  69. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  70. package/dist/cloudflare/core/design-code-tools.js +2793 -0
  71. package/dist/cloudflare/core/design-system-manifest.js +265 -0
  72. package/dist/cloudflare/core/design-system-tools.js +882 -0
  73. package/dist/cloudflare/core/diagnose-tool.js +100 -0
  74. package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
  75. package/dist/cloudflare/core/diff/diff-engine.js +334 -0
  76. package/dist/cloudflare/core/diff/property-compare.js +36 -0
  77. package/dist/cloudflare/core/diff/version-cache.js +74 -0
  78. package/dist/cloudflare/core/enrichment/enrichment-service.js +374 -0
  79. package/dist/cloudflare/core/enrichment/index.js +7 -0
  80. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  81. package/dist/cloudflare/core/enrichment/style-resolver.js +346 -0
  82. package/dist/cloudflare/core/figjam-tools.js +547 -0
  83. package/dist/cloudflare/core/figma-api.js +509 -0
  84. package/dist/cloudflare/core/figma-connector.js +7 -0
  85. package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
  86. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  87. package/dist/cloudflare/core/figma-tools.js +3302 -0
  88. package/dist/cloudflare/core/identity.js +96 -0
  89. package/dist/cloudflare/core/library-tools.js +580 -0
  90. package/dist/cloudflare/core/logger.js +53 -0
  91. package/dist/cloudflare/core/port-discovery.js +935 -0
  92. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  93. package/dist/cloudflare/core/slides-tools.js +714 -0
  94. package/dist/cloudflare/core/slot-tools.js +334 -0
  95. package/dist/cloudflare/core/snippet-injector.js +96 -0
  96. package/dist/cloudflare/core/tokens/alias-resolver.js +205 -0
  97. package/dist/cloudflare/core/tokens/config.js +294 -0
  98. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  99. package/dist/cloudflare/core/tokens/figma-converter.js +323 -0
  100. package/dist/cloudflare/core/tokens/formatters/css-vars.js +338 -0
  101. package/dist/cloudflare/core/tokens/formatters/dtcg.js +376 -0
  102. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  103. package/dist/cloudflare/core/tokens/formatters/json.js +205 -0
  104. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  105. package/dist/cloudflare/core/tokens/formatters/scss.js +258 -0
  106. package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
  107. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +213 -0
  108. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
  109. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +335 -0
  110. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +256 -0
  111. package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
  112. package/dist/cloudflare/core/tokens/index.js +16 -0
  113. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  114. package/dist/cloudflare/core/tokens/parsers/dtcg.js +280 -0
  115. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  116. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  117. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  118. package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
  119. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  120. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  121. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  122. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  123. package/dist/cloudflare/core/tokens/schemas.js +152 -0
  124. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  125. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  126. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  127. package/dist/cloudflare/core/tokens/types.js +18 -0
  128. package/dist/cloudflare/core/tokens-tools.js +1788 -0
  129. package/dist/cloudflare/core/types/design-code.js +4 -0
  130. package/dist/cloudflare/core/types/enriched.js +5 -0
  131. package/dist/cloudflare/core/types/index.js +4 -0
  132. package/dist/cloudflare/core/variable-resolver.js +85 -0
  133. package/dist/cloudflare/core/version-tools.js +1199 -0
  134. package/dist/cloudflare/core/websocket-connector.js +436 -0
  135. package/dist/cloudflare/core/websocket-server.js +956 -0
  136. package/dist/cloudflare/core/write-tools.js +2506 -0
  137. package/dist/cloudflare/index.js +3010 -0
  138. package/dist/core/accessibility-tools.d.ts +21 -0
  139. package/dist/core/accessibility-tools.d.ts.map +1 -0
  140. package/dist/core/accessibility-tools.js +362 -0
  141. package/dist/core/accessibility-tools.js.map +1 -0
  142. package/dist/core/annotation-tools.d.ts +14 -0
  143. package/dist/core/annotation-tools.d.ts.map +1 -0
  144. package/dist/core/annotation-tools.js +231 -0
  145. package/dist/core/annotation-tools.js.map +1 -0
  146. package/dist/core/comment-tools.d.ts +11 -0
  147. package/dist/core/comment-tools.d.ts.map +1 -0
  148. package/dist/core/comment-tools.js +293 -0
  149. package/dist/core/comment-tools.js.map +1 -0
  150. package/dist/core/config.d.ts +17 -0
  151. package/dist/core/config.d.ts.map +1 -0
  152. package/dist/core/config.js +154 -0
  153. package/dist/core/config.js.map +1 -0
  154. package/dist/core/console-monitor.d.ts +82 -0
  155. package/dist/core/console-monitor.d.ts.map +1 -0
  156. package/dist/core/console-monitor.js +428 -0
  157. package/dist/core/console-monitor.js.map +1 -0
  158. package/dist/core/deep-component-tools.d.ts +14 -0
  159. package/dist/core/deep-component-tools.d.ts.map +1 -0
  160. package/dist/core/deep-component-tools.js +129 -0
  161. package/dist/core/deep-component-tools.js.map +1 -0
  162. package/dist/core/design-code-tools.d.ts +116 -0
  163. package/dist/core/design-code-tools.d.ts.map +1 -0
  164. package/dist/core/design-code-tools.js +2794 -0
  165. package/dist/core/design-code-tools.js.map +1 -0
  166. package/dist/core/design-system-manifest.d.ts +273 -0
  167. package/dist/core/design-system-manifest.d.ts.map +1 -0
  168. package/dist/core/design-system-manifest.js +266 -0
  169. package/dist/core/design-system-manifest.js.map +1 -0
  170. package/dist/core/design-system-tools.d.ts +67 -0
  171. package/dist/core/design-system-tools.d.ts.map +1 -0
  172. package/dist/core/design-system-tools.js +883 -0
  173. package/dist/core/design-system-tools.js.map +1 -0
  174. package/dist/core/diagnose-tool.d.ts +41 -0
  175. package/dist/core/diagnose-tool.d.ts.map +1 -0
  176. package/dist/core/diagnose-tool.js +101 -0
  177. package/dist/core/diagnose-tool.js.map +1 -0
  178. package/dist/core/diff/changelog-formatter.d.ts +35 -0
  179. package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
  180. package/dist/core/diff/changelog-formatter.js +276 -0
  181. package/dist/core/diff/changelog-formatter.js.map +1 -0
  182. package/dist/core/diff/diff-engine.d.ts +127 -0
  183. package/dist/core/diff/diff-engine.d.ts.map +1 -0
  184. package/dist/core/diff/diff-engine.js +335 -0
  185. package/dist/core/diff/diff-engine.js.map +1 -0
  186. package/dist/core/diff/property-compare.d.ts +19 -0
  187. package/dist/core/diff/property-compare.d.ts.map +1 -0
  188. package/dist/core/diff/property-compare.js +37 -0
  189. package/dist/core/diff/property-compare.js.map +1 -0
  190. package/dist/core/diff/version-cache.d.ts +40 -0
  191. package/dist/core/diff/version-cache.d.ts.map +1 -0
  192. package/dist/core/diff/version-cache.js +75 -0
  193. package/dist/core/diff/version-cache.js.map +1 -0
  194. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  195. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  196. package/dist/core/enrichment/enrichment-service.js +375 -0
  197. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  198. package/dist/core/enrichment/index.d.ts +8 -0
  199. package/dist/core/enrichment/index.d.ts.map +1 -0
  200. package/dist/core/enrichment/index.js +8 -0
  201. package/dist/core/enrichment/index.js.map +1 -0
  202. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  203. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  204. package/dist/core/enrichment/relationship-mapper.js +352 -0
  205. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  206. package/dist/core/enrichment/style-resolver.d.ts +85 -0
  207. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  208. package/dist/core/enrichment/style-resolver.js +347 -0
  209. package/dist/core/enrichment/style-resolver.js.map +1 -0
  210. package/dist/core/figjam-tools.d.ts +8 -0
  211. package/dist/core/figjam-tools.d.ts.map +1 -0
  212. package/dist/core/figjam-tools.js +548 -0
  213. package/dist/core/figjam-tools.js.map +1 -0
  214. package/dist/core/figma-api.d.ts +254 -0
  215. package/dist/core/figma-api.d.ts.map +1 -0
  216. package/dist/core/figma-api.js +510 -0
  217. package/dist/core/figma-api.js.map +1 -0
  218. package/dist/core/figma-connector.d.ts +214 -0
  219. package/dist/core/figma-connector.d.ts.map +1 -0
  220. package/dist/core/figma-connector.js +8 -0
  221. package/dist/core/figma-connector.js.map +1 -0
  222. package/dist/core/figma-desktop-connector.d.ts +312 -0
  223. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  224. package/dist/core/figma-desktop-connector.js +1298 -0
  225. package/dist/core/figma-desktop-connector.js.map +1 -0
  226. package/dist/core/figma-reconstruction-spec.d.ts +166 -0
  227. package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
  228. package/dist/core/figma-reconstruction-spec.js +403 -0
  229. package/dist/core/figma-reconstruction-spec.js.map +1 -0
  230. package/dist/core/figma-style-extractor.d.ts +76 -0
  231. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  232. package/dist/core/figma-style-extractor.js +312 -0
  233. package/dist/core/figma-style-extractor.js.map +1 -0
  234. package/dist/core/figma-tools.d.ts +22 -0
  235. package/dist/core/figma-tools.d.ts.map +1 -0
  236. package/dist/core/figma-tools.js +3303 -0
  237. package/dist/core/figma-tools.js.map +1 -0
  238. package/dist/core/identity.d.ts +41 -0
  239. package/dist/core/identity.d.ts.map +1 -0
  240. package/dist/core/identity.js +97 -0
  241. package/dist/core/identity.js.map +1 -0
  242. package/dist/core/library-tools.d.ts +17 -0
  243. package/dist/core/library-tools.d.ts.map +1 -0
  244. package/dist/core/library-tools.js +581 -0
  245. package/dist/core/library-tools.js.map +1 -0
  246. package/dist/core/logger.d.ts +22 -0
  247. package/dist/core/logger.d.ts.map +1 -0
  248. package/dist/core/logger.js +54 -0
  249. package/dist/core/logger.js.map +1 -0
  250. package/dist/core/port-discovery.d.ts +211 -0
  251. package/dist/core/port-discovery.d.ts.map +1 -0
  252. package/dist/core/port-discovery.js +936 -0
  253. package/dist/core/port-discovery.js.map +1 -0
  254. package/dist/core/resolve-package-root.d.ts +2 -0
  255. package/dist/core/resolve-package-root.d.ts.map +1 -0
  256. package/dist/core/resolve-package-root.js +12 -0
  257. package/dist/core/resolve-package-root.js.map +1 -0
  258. package/dist/core/slides-tools.d.ts +8 -0
  259. package/dist/core/slides-tools.d.ts.map +1 -0
  260. package/dist/core/slides-tools.js +715 -0
  261. package/dist/core/slides-tools.js.map +1 -0
  262. package/dist/core/slot-tools.d.ts +7 -0
  263. package/dist/core/slot-tools.d.ts.map +1 -0
  264. package/dist/core/slot-tools.js +335 -0
  265. package/dist/core/slot-tools.js.map +1 -0
  266. package/dist/core/snippet-injector.d.ts +24 -0
  267. package/dist/core/snippet-injector.d.ts.map +1 -0
  268. package/dist/core/snippet-injector.js +97 -0
  269. package/dist/core/snippet-injector.js.map +1 -0
  270. package/dist/core/tokens/alias-resolver.d.ts +97 -0
  271. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  272. package/dist/core/tokens/alias-resolver.js +206 -0
  273. package/dist/core/tokens/alias-resolver.js.map +1 -0
  274. package/dist/core/tokens/config.d.ts +380 -0
  275. package/dist/core/tokens/config.d.ts.map +1 -0
  276. package/dist/core/tokens/config.js +295 -0
  277. package/dist/core/tokens/config.js.map +1 -0
  278. package/dist/core/tokens/dialect.d.ts +107 -0
  279. package/dist/core/tokens/dialect.d.ts.map +1 -0
  280. package/dist/core/tokens/dialect.js +233 -0
  281. package/dist/core/tokens/dialect.js.map +1 -0
  282. package/dist/core/tokens/figma-converter.d.ts +102 -0
  283. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  284. package/dist/core/tokens/figma-converter.js +324 -0
  285. package/dist/core/tokens/figma-converter.js.map +1 -0
  286. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  287. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  288. package/dist/core/tokens/formatters/css-vars.js +339 -0
  289. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  290. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  291. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  292. package/dist/core/tokens/formatters/dtcg.js +377 -0
  293. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  294. package/dist/core/tokens/formatters/index.d.ts +30 -0
  295. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  296. package/dist/core/tokens/formatters/index.js +46 -0
  297. package/dist/core/tokens/formatters/index.js.map +1 -0
  298. package/dist/core/tokens/formatters/json.d.ts +37 -0
  299. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  300. package/dist/core/tokens/formatters/json.js +206 -0
  301. package/dist/core/tokens/formatters/json.js.map +1 -0
  302. package/dist/core/tokens/formatters/less.d.ts +4 -0
  303. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  304. package/dist/core/tokens/formatters/less.js +5 -0
  305. package/dist/core/tokens/formatters/less.js.map +1 -0
  306. package/dist/core/tokens/formatters/scss.d.ts +26 -0
  307. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  308. package/dist/core/tokens/formatters/scss.js +259 -0
  309. package/dist/core/tokens/formatters/scss.js.map +1 -0
  310. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  311. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  312. package/dist/core/tokens/formatters/stubs.js +14 -0
  313. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  314. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
  315. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  316. package/dist/core/tokens/formatters/style-dictionary-v3.js +214 -0
  317. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  318. package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
  319. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  320. package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
  321. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  322. package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
  323. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  324. package/dist/core/tokens/formatters/tailwind-v4.js +336 -0
  325. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  326. package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
  327. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  328. package/dist/core/tokens/formatters/tokens-studio.js +257 -0
  329. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  330. package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
  331. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  332. package/dist/core/tokens/formatters/ts-module.js +199 -0
  333. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  334. package/dist/core/tokens/index.d.ts +18 -0
  335. package/dist/core/tokens/index.d.ts.map +1 -0
  336. package/dist/core/tokens/index.js +17 -0
  337. package/dist/core/tokens/index.js.map +1 -0
  338. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  339. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  340. package/dist/core/tokens/parsers/css-vars.js +5 -0
  341. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  342. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  343. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  344. package/dist/core/tokens/parsers/dtcg.js +281 -0
  345. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  346. package/dist/core/tokens/parsers/index.d.ts +37 -0
  347. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  348. package/dist/core/tokens/parsers/index.js +139 -0
  349. package/dist/core/tokens/parsers/index.js.map +1 -0
  350. package/dist/core/tokens/parsers/json.d.ts +4 -0
  351. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  352. package/dist/core/tokens/parsers/json.js +8 -0
  353. package/dist/core/tokens/parsers/json.js.map +1 -0
  354. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  355. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  356. package/dist/core/tokens/parsers/scss.js +5 -0
  357. package/dist/core/tokens/parsers/scss.js.map +1 -0
  358. package/dist/core/tokens/parsers/stubs.d.ts +15 -0
  359. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  360. package/dist/core/tokens/parsers/stubs.js +21 -0
  361. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  362. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  363. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  364. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  365. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  366. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  367. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  368. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  369. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  370. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  371. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  372. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  373. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  374. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  375. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  376. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  377. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  378. package/dist/core/tokens/schemas.d.ts +155 -0
  379. package/dist/core/tokens/schemas.d.ts.map +1 -0
  380. package/dist/core/tokens/schemas.js +153 -0
  381. package/dist/core/tokens/schemas.js.map +1 -0
  382. package/dist/core/tokens/transforms/color.d.ts +9 -0
  383. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  384. package/dist/core/tokens/transforms/color.js +13 -0
  385. package/dist/core/tokens/transforms/color.js.map +1 -0
  386. package/dist/core/tokens/transforms/index.d.ts +36 -0
  387. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  388. package/dist/core/tokens/transforms/index.js +30 -0
  389. package/dist/core/tokens/transforms/index.js.map +1 -0
  390. package/dist/core/tokens/transforms/size.d.ts +7 -0
  391. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  392. package/dist/core/tokens/transforms/size.js +8 -0
  393. package/dist/core/tokens/transforms/size.js.map +1 -0
  394. package/dist/core/tokens/types.d.ts +284 -0
  395. package/dist/core/tokens/types.d.ts.map +1 -0
  396. package/dist/core/tokens/types.js +19 -0
  397. package/dist/core/tokens/types.js.map +1 -0
  398. package/dist/core/tokens-tools.d.ts +285 -0
  399. package/dist/core/tokens-tools.d.ts.map +1 -0
  400. package/dist/core/tokens-tools.js +1789 -0
  401. package/dist/core/tokens-tools.js.map +1 -0
  402. package/dist/core/types/design-code.d.ts +271 -0
  403. package/dist/core/types/design-code.d.ts.map +1 -0
  404. package/dist/core/types/design-code.js +5 -0
  405. package/dist/core/types/design-code.js.map +1 -0
  406. package/dist/core/types/enriched.d.ts +213 -0
  407. package/dist/core/types/enriched.d.ts.map +1 -0
  408. package/dist/core/types/enriched.js +6 -0
  409. package/dist/core/types/enriched.js.map +1 -0
  410. package/dist/core/types/index.d.ts +104 -0
  411. package/dist/core/types/index.d.ts.map +1 -0
  412. package/dist/core/types/index.js +5 -0
  413. package/dist/core/types/index.js.map +1 -0
  414. package/dist/core/variable-resolver.d.ts +45 -0
  415. package/dist/core/variable-resolver.d.ts.map +1 -0
  416. package/dist/core/variable-resolver.js +86 -0
  417. package/dist/core/variable-resolver.js.map +1 -0
  418. package/dist/core/version-tools.d.ts +59 -0
  419. package/dist/core/version-tools.d.ts.map +1 -0
  420. package/dist/core/version-tools.js +1200 -0
  421. package/dist/core/version-tools.js.map +1 -0
  422. package/dist/core/websocket-connector.d.ts +247 -0
  423. package/dist/core/websocket-connector.d.ts.map +1 -0
  424. package/dist/core/websocket-connector.js +437 -0
  425. package/dist/core/websocket-connector.js.map +1 -0
  426. package/dist/core/websocket-server.d.ts +290 -0
  427. package/dist/core/websocket-server.d.ts.map +1 -0
  428. package/dist/core/websocket-server.js +957 -0
  429. package/dist/core/websocket-server.js.map +1 -0
  430. package/dist/core/write-tools.d.ts +7 -0
  431. package/dist/core/write-tools.d.ts.map +1 -0
  432. package/dist/core/write-tools.js +2507 -0
  433. package/dist/core/write-tools.js.map +1 -0
  434. package/dist/local.d.ts +105 -0
  435. package/dist/local.d.ts.map +1 -0
  436. package/dist/local.js +3314 -0
  437. package/dist/local.js.map +1 -0
  438. package/figma-desktop-bridge/README.md +365 -0
  439. package/figma-desktop-bridge/code.js +7359 -0
  440. package/figma-desktop-bridge/icon.png +0 -0
  441. package/figma-desktop-bridge/manifest.json +67 -0
  442. package/figma-desktop-bridge/ui.html +2783 -0
  443. package/package.json +103 -0
@@ -0,0 +1,2506 @@
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
+
22
+ **HOUSEKEEPING (MANDATORY):**
23
+ Before creating: screenshot the target page to see existing content and find clear space.
24
+ When creating: place inside a named Section, positioned BELOW or AWAY from existing content. Never overlap.
25
+ After creating: screenshot to verify clean placement and no overlaps.
26
+ On failure/retry: DELETE any partial artifacts (empty frames, orphaned layers, blank pages) before retrying. Use node.remove() to clean up.
27
+ Pages: NEVER create a new page if one with that name already exists — use the existing one. If you created a blank page during a failed attempt, delete it.
28
+ Layers: If your code creates helper frames, placeholder nodes, or intermediate layers that aren't part of the final result, remove them.`, {
29
+ code: z
30
+ .string()
31
+ .describe("JavaScript code to execute. Has access to the 'figma' global object. " +
32
+ "Example: 'const rect = figma.createRectangle(); rect.resize(100, 100); return { id: rect.id };'"),
33
+ timeout: z
34
+ .number()
35
+ .optional()
36
+ .default(5000)
37
+ .describe("Execution timeout in milliseconds (default: 5000, max: 30000)"),
38
+ }, async ({ code, timeout }) => {
39
+ try {
40
+ const connector = await getDesktopConnector();
41
+ const result = await connector.executeCodeViaUI(code, Math.min(timeout, 30000));
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: JSON.stringify({
47
+ success: result.success,
48
+ result: result.result,
49
+ error: result.error,
50
+ resultAnalysis: result.resultAnalysis,
51
+ fileContext: result.fileContext,
52
+ timestamp: Date.now(),
53
+ }),
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ catch (error) {
59
+ logger.error({ error }, "Failed to execute code in Figma plugin context");
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text: JSON.stringify({
65
+ error: error instanceof Error ? error.message : String(error),
66
+ message: "Failed to execute code in Figma plugin context",
67
+ hint: "Make sure the Desktop Bridge plugin is running in Figma",
68
+ }),
69
+ },
70
+ ],
71
+ isError: true,
72
+ };
73
+ }
74
+ });
75
+ // ============================================================================
76
+ // VARIABLE MANAGEMENT TOOLS
77
+ // ============================================================================
78
+ // Tool: Update a variable's value
79
+ server.tool("figma_update_variable", "Update a single variable's value and/or its description. Pass modeId+value to change the value in a mode; pass description to set the 'How to use this variable' text shown in the Variables panel (and exported as DTCG $description). At least one of {modeId+value} or {description} is required. For many value updates at once, use figma_batch_update_variables (10-50x faster). Use figma_get_variables first for IDs. COLOR: hex '#FF0000', FLOAT: number, STRING: text, BOOLEAN: true/false. Requires Desktop Bridge plugin.", {
80
+ variableId: z
81
+ .string()
82
+ .describe("The variable ID to update (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
83
+ modeId: z
84
+ .string()
85
+ .optional()
86
+ .describe("The mode ID to update the value in (e.g., '1:0'). Required when updating a value. Get this from the variable's collection modes."),
87
+ value: z
88
+ .union([z.string(), z.number(), z.boolean()])
89
+ .optional()
90
+ .describe("The new value (requires modeId). For COLOR: hex string like '#FF0000'. For FLOAT: number. For STRING: text. For BOOLEAN: true/false."),
91
+ description: z
92
+ .string()
93
+ .optional()
94
+ .describe("Set the variable's description (the 'How to use this variable' field; exported as DTCG $description). Pass an empty string to clear it."),
95
+ }, async ({ variableId, modeId, value, description }) => {
96
+ try {
97
+ const connector = await getDesktopConnector();
98
+ const hasValueUpdate = modeId !== undefined && value !== undefined;
99
+ const hasDescriptionUpdate = description !== undefined;
100
+ if ((modeId !== undefined) !== (value !== undefined)) {
101
+ throw new Error("Both modeId and value are required together to update a value.");
102
+ }
103
+ if (!hasValueUpdate && !hasDescriptionUpdate) {
104
+ throw new Error("Nothing to update: provide modeId+value to change the value, and/or description to set the description.");
105
+ }
106
+ let variable;
107
+ const updated = [];
108
+ if (hasValueUpdate) {
109
+ const result = await connector.updateVariable(variableId, modeId, value);
110
+ variable = result.variable;
111
+ updated.push("value");
112
+ }
113
+ if (hasDescriptionUpdate) {
114
+ const result = await connector.setVariableDescription(variableId, description);
115
+ variable = result.variable || variable;
116
+ updated.push("description");
117
+ }
118
+ return {
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: JSON.stringify({
123
+ success: true,
124
+ message: `Variable "${variable?.name ?? variableId}" updated successfully (${updated.join(" + ")})`,
125
+ variable,
126
+ timestamp: Date.now(),
127
+ }),
128
+ },
129
+ ],
130
+ };
131
+ }
132
+ catch (error) {
133
+ logger.error({ error }, "Failed to update variable");
134
+ return {
135
+ content: [
136
+ {
137
+ type: "text",
138
+ text: JSON.stringify({
139
+ error: error instanceof Error ? error.message : String(error),
140
+ message: "Failed to update variable",
141
+ hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
142
+ }),
143
+ },
144
+ ],
145
+ isError: true,
146
+ };
147
+ }
148
+ });
149
+ // Tool: Create a new variable
150
+ 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.", {
151
+ name: z
152
+ .string()
153
+ .describe("Name for the new variable (e.g., 'primary-blue')"),
154
+ collectionId: z
155
+ .string()
156
+ .describe("The collection ID to create the variable in (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
157
+ resolvedType: z
158
+ .enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
159
+ .describe("The variable type: COLOR, FLOAT, STRING, or BOOLEAN"),
160
+ description: z
161
+ .string()
162
+ .optional()
163
+ .describe("Optional description for the variable"),
164
+ valuesByMode: z
165
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
166
+ .optional()
167
+ .describe("Optional initial values by mode ID. Example: { '1:0': '#FF0000', '1:1': '#0000FF' }"),
168
+ }, async ({ name, collectionId, resolvedType, description, valuesByMode, }) => {
169
+ try {
170
+ const connector = await getDesktopConnector();
171
+ const result = await connector.createVariable(name, collectionId, resolvedType, {
172
+ description,
173
+ valuesByMode,
174
+ });
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: JSON.stringify({
180
+ success: true,
181
+ message: `Variable "${name}" created successfully`,
182
+ variable: result.variable,
183
+ timestamp: Date.now(),
184
+ }),
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ catch (error) {
190
+ logger.error({ error }, "Failed to create variable");
191
+ return {
192
+ content: [
193
+ {
194
+ type: "text",
195
+ text: JSON.stringify({
196
+ error: error instanceof Error ? error.message : String(error),
197
+ message: "Failed to create variable",
198
+ hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
199
+ }),
200
+ },
201
+ ],
202
+ isError: true,
203
+ };
204
+ }
205
+ });
206
+ // Tool: Create a new variable collection
207
+ 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.", {
208
+ name: z
209
+ .string()
210
+ .describe("Name for the new collection (e.g., 'Brand Colors')"),
211
+ initialModeName: z
212
+ .string()
213
+ .optional()
214
+ .describe("Name for the initial mode (default mode is created automatically). Example: 'Light'"),
215
+ additionalModes: z
216
+ .array(z.string())
217
+ .optional()
218
+ .describe("Additional mode names to create. Example: ['Dark', 'High Contrast']"),
219
+ }, async ({ name, initialModeName, additionalModes }) => {
220
+ try {
221
+ const connector = await getDesktopConnector();
222
+ const result = await connector.createVariableCollection(name, {
223
+ initialModeName,
224
+ additionalModes,
225
+ });
226
+ return {
227
+ content: [
228
+ {
229
+ type: "text",
230
+ text: JSON.stringify({
231
+ success: true,
232
+ message: `Collection "${name}" created successfully`,
233
+ collection: result.collection,
234
+ timestamp: Date.now(),
235
+ }),
236
+ },
237
+ ],
238
+ };
239
+ }
240
+ catch (error) {
241
+ logger.error({ error }, "Failed to create collection");
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text",
246
+ text: JSON.stringify({
247
+ error: error instanceof Error ? error.message : String(error),
248
+ message: "Failed to create variable collection",
249
+ hint: "Make sure the Desktop Bridge plugin is running in Figma",
250
+ }),
251
+ },
252
+ ],
253
+ isError: true,
254
+ };
255
+ }
256
+ });
257
+ // Tool: Delete a variable
258
+ 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.", {
259
+ variableId: z
260
+ .string()
261
+ .describe("The variable ID to delete (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
262
+ }, async ({ variableId }) => {
263
+ try {
264
+ const connector = await getDesktopConnector();
265
+ const result = await connector.deleteVariable(variableId);
266
+ return {
267
+ content: [
268
+ {
269
+ type: "text",
270
+ text: JSON.stringify({
271
+ success: true,
272
+ message: `Variable "${result.deleted.name}" deleted successfully`,
273
+ deleted: result.deleted,
274
+ timestamp: Date.now(),
275
+ warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
276
+ }),
277
+ },
278
+ ],
279
+ };
280
+ }
281
+ catch (error) {
282
+ logger.error({ error }, "Failed to delete variable");
283
+ return {
284
+ content: [
285
+ {
286
+ type: "text",
287
+ text: JSON.stringify({
288
+ error: error instanceof Error ? error.message : String(error),
289
+ message: "Failed to delete variable",
290
+ hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
291
+ }),
292
+ },
293
+ ],
294
+ isError: true,
295
+ };
296
+ }
297
+ });
298
+ // Tool: Delete a variable collection
299
+ 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.", {
300
+ collectionId: z
301
+ .string()
302
+ .describe("The collection ID to delete (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
303
+ }, async ({ collectionId }) => {
304
+ try {
305
+ const connector = await getDesktopConnector();
306
+ const result = await connector.deleteVariableCollection(collectionId);
307
+ return {
308
+ content: [
309
+ {
310
+ type: "text",
311
+ text: JSON.stringify({
312
+ success: true,
313
+ message: `Collection "${result.deleted.name}" and ${result.deleted.variableCount} variables deleted successfully`,
314
+ deleted: result.deleted,
315
+ timestamp: Date.now(),
316
+ warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
317
+ }),
318
+ },
319
+ ],
320
+ };
321
+ }
322
+ catch (error) {
323
+ logger.error({ error }, "Failed to delete collection");
324
+ return {
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: JSON.stringify({
329
+ error: error instanceof Error ? error.message : String(error),
330
+ message: "Failed to delete variable collection",
331
+ hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
332
+ }),
333
+ },
334
+ ],
335
+ isError: true,
336
+ };
337
+ }
338
+ });
339
+ // Tool: Rename a variable
340
+ 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.", {
341
+ variableId: z
342
+ .string()
343
+ .describe("The variable ID to rename (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
344
+ newName: z
345
+ .string()
346
+ .describe("The new name for the variable. Can include slashes for grouping (e.g., 'colors/primary/background')."),
347
+ }, async ({ variableId, newName }) => {
348
+ try {
349
+ const connector = await getDesktopConnector();
350
+ const result = await connector.renameVariable(variableId, newName);
351
+ return {
352
+ content: [
353
+ {
354
+ type: "text",
355
+ text: JSON.stringify({
356
+ success: true,
357
+ message: `Variable renamed from "${result.oldName}" to "${result.variable.name}"`,
358
+ oldName: result.oldName,
359
+ variable: result.variable,
360
+ timestamp: Date.now(),
361
+ }),
362
+ },
363
+ ],
364
+ };
365
+ }
366
+ catch (error) {
367
+ logger.error({ error }, "Failed to rename variable");
368
+ return {
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: JSON.stringify({
373
+ error: error instanceof Error ? error.message : String(error),
374
+ message: "Failed to rename variable",
375
+ hint: "Make sure the Desktop Bridge plugin is running and the variable ID is correct",
376
+ }),
377
+ },
378
+ ],
379
+ isError: true,
380
+ };
381
+ }
382
+ });
383
+ // Tool: Add a mode to a collection
384
+ 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.", {
385
+ collectionId: z
386
+ .string()
387
+ .describe("The collection ID to add the mode to (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
388
+ modeName: z
389
+ .string()
390
+ .describe("The name for the new mode (e.g., 'Dark', 'Mobile', 'High Contrast')."),
391
+ }, async ({ collectionId, modeName }) => {
392
+ try {
393
+ const connector = await getDesktopConnector();
394
+ const result = await connector.addMode(collectionId, modeName);
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text",
399
+ text: JSON.stringify({
400
+ success: true,
401
+ message: `Mode "${modeName}" added to collection "${result.collection.name}"`,
402
+ newMode: result.newMode,
403
+ collection: result.collection,
404
+ timestamp: Date.now(),
405
+ }),
406
+ },
407
+ ],
408
+ };
409
+ }
410
+ catch (error) {
411
+ logger.error({ error }, "Failed to add mode");
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text",
416
+ text: JSON.stringify({
417
+ error: error instanceof Error ? error.message : String(error),
418
+ message: "Failed to add mode to collection",
419
+ hint: "Make sure the Desktop Bridge plugin is running, the collection ID is correct, and you haven't exceeded Figma's mode limit",
420
+ }),
421
+ },
422
+ ],
423
+ isError: true,
424
+ };
425
+ }
426
+ });
427
+ // Tool: Rename a mode in a collection
428
+ server.tool("figma_rename_mode", "Rename an existing mode in a Figma variable collection. Requires the Desktop Bridge plugin to be running.", {
429
+ collectionId: z
430
+ .string()
431
+ .describe("The collection ID containing the mode (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
432
+ modeId: z
433
+ .string()
434
+ .describe("The mode ID to rename (e.g., '123:0'). Get this from the collection's modes array in figma_get_variables."),
435
+ newName: z
436
+ .string()
437
+ .describe("The new name for the mode (e.g., 'Dark Theme', 'Tablet')."),
438
+ }, async ({ collectionId, modeId, newName }) => {
439
+ try {
440
+ const connector = await getDesktopConnector();
441
+ const result = await connector.renameMode(collectionId, modeId, newName);
442
+ return {
443
+ content: [
444
+ {
445
+ type: "text",
446
+ text: JSON.stringify({
447
+ success: true,
448
+ message: `Mode renamed from "${result.oldName}" to "${newName}"`,
449
+ oldName: result.oldName,
450
+ collection: result.collection,
451
+ timestamp: Date.now(),
452
+ }),
453
+ },
454
+ ],
455
+ };
456
+ }
457
+ catch (error) {
458
+ logger.error({ error }, "Failed to rename mode");
459
+ return {
460
+ content: [
461
+ {
462
+ type: "text",
463
+ text: JSON.stringify({
464
+ error: error instanceof Error ? error.message : String(error),
465
+ message: "Failed to rename mode",
466
+ hint: "Make sure the Desktop Bridge plugin is running, the collection ID and mode ID are correct",
467
+ }),
468
+ },
469
+ ],
470
+ isError: true,
471
+ };
472
+ }
473
+ });
474
+ // ============================================================================
475
+ // BATCH OPERATIONS (Performance-Optimized)
476
+ // ============================================================================
477
+ // Execute multiple variable operations in a single roundtrip,
478
+ // reducing per-operation overhead from ~60-170ms to near-zero.
479
+ // Use these instead of calling individual tools repeatedly.
480
+ // Tool: Batch create variables
481
+ 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.", {
482
+ collectionId: z
483
+ .string()
484
+ .describe("Collection ID to create all variables in (e.g., 'VariableCollectionId:123:456')"),
485
+ variables: z
486
+ .array(z.object({
487
+ name: z.string().describe("Variable name (e.g., 'primary-blue')"),
488
+ resolvedType: z
489
+ .enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
490
+ .describe("Variable type"),
491
+ description: z
492
+ .string()
493
+ .optional()
494
+ .describe("Optional description"),
495
+ valuesByMode: z
496
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
497
+ .optional()
498
+ .describe("Values by mode ID. For COLOR: hex like '#FF0000'. Example: { '1:0': '#FF0000' }"),
499
+ }))
500
+ .min(1)
501
+ .max(100)
502
+ .describe("Array of variables to create (1-100)"),
503
+ }, async ({ collectionId, variables }) => {
504
+ try {
505
+ const connector = await getDesktopConnector();
506
+ const script = `
507
+ const results = [];
508
+ const collectionId = ${JSON.stringify(collectionId)};
509
+ const vars = ${JSON.stringify(variables)};
510
+
511
+ function hexToRgba(hex) {
512
+ hex = hex.replace('#', '');
513
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
514
+ return {
515
+ r: parseInt(hex.substring(0, 2), 16) / 255,
516
+ g: parseInt(hex.substring(2, 4), 16) / 255,
517
+ b: parseInt(hex.substring(4, 6), 16) / 255,
518
+ a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
519
+ };
520
+ }
521
+
522
+ const collection = await figma.variables.getVariableCollectionByIdAsync(collectionId);
523
+ if (!collection) return { created: 0, failed: vars.length, results: vars.map(v => ({ success: false, name: v.name, error: 'Collection not found: ' + collectionId })) };
524
+
525
+ for (const v of vars) {
526
+ try {
527
+ const variable = figma.variables.createVariable(v.name, collection, v.resolvedType);
528
+ if (v.description) variable.description = v.description;
529
+ if (v.valuesByMode) {
530
+ for (const [modeId, value] of Object.entries(v.valuesByMode)) {
531
+ let processed = value;
532
+ if (typeof value === 'string' && value.startsWith('VariableID:')) {
533
+ processed = { type: 'VARIABLE_ALIAS', id: value };
534
+ } else if (v.resolvedType === 'COLOR' && typeof value === 'string') {
535
+ processed = hexToRgba(value);
536
+ }
537
+ variable.setValueForMode(modeId, processed);
538
+ }
539
+ }
540
+ results.push({ success: true, name: v.name, id: variable.id });
541
+ } catch (err) {
542
+ results.push({ success: false, name: v.name, error: String(err) });
543
+ }
544
+ }
545
+
546
+ return {
547
+ created: results.filter(r => r.success).length,
548
+ failed: results.filter(r => !r.success).length,
549
+ results
550
+ };`;
551
+ const timeout = Math.max(5000, variables.length * 200);
552
+ const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
553
+ if (result.error) {
554
+ return {
555
+ content: [
556
+ {
557
+ type: "text",
558
+ text: JSON.stringify({
559
+ error: result.error,
560
+ message: "Batch create failed during execution",
561
+ hint: "Check that the collection ID is valid and the Desktop Bridge plugin is running",
562
+ }),
563
+ },
564
+ ],
565
+ isError: true,
566
+ };
567
+ }
568
+ return {
569
+ content: [
570
+ {
571
+ type: "text",
572
+ text: JSON.stringify({
573
+ success: true,
574
+ message: `Batch created ${result.result?.created ?? 0} variables (${result.result?.failed ?? 0} failed)`,
575
+ ...result.result,
576
+ timestamp: Date.now(),
577
+ }),
578
+ },
579
+ ],
580
+ };
581
+ }
582
+ catch (error) {
583
+ logger.error({ error }, "Failed to batch create variables");
584
+ return {
585
+ content: [
586
+ {
587
+ type: "text",
588
+ text: JSON.stringify({
589
+ error: error instanceof Error
590
+ ? error.message
591
+ : String(error),
592
+ message: "Failed to batch create variables",
593
+ hint: "Make sure the Desktop Bridge plugin is running and the collection ID is correct",
594
+ }),
595
+ },
596
+ ],
597
+ isError: true,
598
+ };
599
+ }
600
+ });
601
+ // Tool: Batch update variables
602
+ 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.", {
603
+ updates: z
604
+ .array(z.object({
605
+ variableId: z
606
+ .string()
607
+ .describe("Variable ID (e.g., 'VariableID:123:456')"),
608
+ modeId: z
609
+ .string()
610
+ .describe("Mode ID (e.g., '1:0')"),
611
+ value: z
612
+ .union([z.string(), z.number(), z.boolean()])
613
+ .describe("New value. COLOR: hex like '#FF0000'. FLOAT: number. STRING: text. BOOLEAN: true/false."),
614
+ }))
615
+ .min(1)
616
+ .max(100)
617
+ .describe("Array of updates to apply (1-100)"),
618
+ }, async ({ updates }) => {
619
+ try {
620
+ const connector = await getDesktopConnector();
621
+ const script = `
622
+ const results = [];
623
+ const updates = ${JSON.stringify(updates)};
624
+
625
+ function hexToRgba(hex) {
626
+ hex = hex.replace('#', '');
627
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
628
+ return {
629
+ r: parseInt(hex.substring(0, 2), 16) / 255,
630
+ g: parseInt(hex.substring(2, 4), 16) / 255,
631
+ b: parseInt(hex.substring(4, 6), 16) / 255,
632
+ a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
633
+ };
634
+ }
635
+
636
+ for (const u of updates) {
637
+ try {
638
+ const variable = await figma.variables.getVariableByIdAsync(u.variableId);
639
+ if (!variable) throw new Error('Variable not found: ' + u.variableId);
640
+ const isColor = variable.resolvedType === 'COLOR';
641
+ const processed = isColor && typeof u.value === 'string' ? hexToRgba(u.value) : u.value;
642
+ variable.setValueForMode(u.modeId, processed);
643
+ results.push({ success: true, variableId: u.variableId, name: variable.name });
644
+ } catch (err) {
645
+ results.push({ success: false, variableId: u.variableId, error: String(err) });
646
+ }
647
+ }
648
+
649
+ return {
650
+ updated: results.filter(r => r.success).length,
651
+ failed: results.filter(r => !r.success).length,
652
+ results
653
+ };`;
654
+ const timeout = Math.max(5000, updates.length * 150);
655
+ const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
656
+ if (result.error) {
657
+ return {
658
+ content: [
659
+ {
660
+ type: "text",
661
+ text: JSON.stringify({
662
+ error: result.error,
663
+ message: "Batch update failed during execution",
664
+ hint: "Check that variable IDs and mode IDs are valid",
665
+ }),
666
+ },
667
+ ],
668
+ isError: true,
669
+ };
670
+ }
671
+ return {
672
+ content: [
673
+ {
674
+ type: "text",
675
+ text: JSON.stringify({
676
+ success: true,
677
+ message: `Batch updated ${result.result?.updated ?? 0} variables (${result.result?.failed ?? 0} failed)`,
678
+ ...result.result,
679
+ timestamp: Date.now(),
680
+ }),
681
+ },
682
+ ],
683
+ };
684
+ }
685
+ catch (error) {
686
+ logger.error({ error }, "Failed to batch update variables");
687
+ return {
688
+ content: [
689
+ {
690
+ type: "text",
691
+ text: JSON.stringify({
692
+ error: error instanceof Error
693
+ ? error.message
694
+ : String(error),
695
+ message: "Failed to batch update variables",
696
+ hint: "Make sure the Desktop Bridge plugin is running and variable/mode IDs are correct",
697
+ }),
698
+ },
699
+ ],
700
+ isError: true,
701
+ };
702
+ }
703
+ });
704
+ // Tool: Setup design tokens (collection + modes + variables atomically)
705
+ 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. Values may be literals OR DTCG-style brace references ('{color.blue.600}', or set-qualified '{primitives.color.blue.600}') that resolve to variable ALIASES — first against variables created in this same call, then against existing local variables. Unresolvable references skip that value with a per-item warning (the rest of the batch still applies), so semantic collections referencing primitives no longer need raw figma_execute. Requires Desktop Bridge plugin.", {
706
+ collectionName: z
707
+ .string()
708
+ .describe("Name for the token collection (e.g., 'Brand Tokens')"),
709
+ modes: z
710
+ .array(z.string())
711
+ .min(1)
712
+ .max(4)
713
+ .describe("Mode names (first becomes default). Example: ['Light', 'Dark']"),
714
+ tokens: z
715
+ .array(z.object({
716
+ name: z
717
+ .string()
718
+ .describe("Token name (e.g., 'color/primary')"),
719
+ resolvedType: z
720
+ .enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"])
721
+ .describe("Token type"),
722
+ description: z
723
+ .string()
724
+ .optional()
725
+ .describe("Optional description"),
726
+ values: z
727
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
728
+ .describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }. A string value wrapped in braces is an ALIAS reference resolved to another variable: '{color.blue.600}' matches variable name 'color/blue/600' — first among variables created in THIS call, then existing local variables (exact match, then case-insensitive). Set-qualified references ('{primitives.color.blue.600}') strip the leading collection name. Unresolvable references skip that value and surface in the response's warnings[]. A string starting with 'VariableID:' (e.g. from figma_get_variables) aliases that variable directly by id."),
729
+ }))
730
+ .min(1)
731
+ .max(100)
732
+ .describe("Token definitions (1-100)"),
733
+ }, async ({ collectionName, modes, tokens }) => {
734
+ try {
735
+ const connector = await getDesktopConnector();
736
+ const script = `
737
+ const collectionName = ${JSON.stringify(collectionName)};
738
+ const modeNames = ${JSON.stringify(modes)};
739
+ const tokenDefs = ${JSON.stringify(tokens)};
740
+
741
+ function hexToRgba(hex) {
742
+ hex = hex.replace('#', '');
743
+ if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
744
+ return {
745
+ r: parseInt(hex.substring(0, 2), 16) / 255,
746
+ g: parseInt(hex.substring(2, 4), 16) / 255,
747
+ b: parseInt(hex.substring(4, 6), 16) / 255,
748
+ a: hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1
749
+ };
750
+ }
751
+
752
+ // Step 1: Create collection
753
+ const collection = figma.variables.createVariableCollection(collectionName);
754
+ const modeMap = {};
755
+
756
+ // Step 2: Set up modes - first mode uses the default mode that was auto-created
757
+ const defaultModeId = collection.modes[0].modeId;
758
+ collection.renameMode(defaultModeId, modeNames[0]);
759
+ modeMap[modeNames[0]] = defaultModeId;
760
+
761
+ for (let i = 1; i < modeNames.length; i++) {
762
+ const newModeId = collection.addMode(modeNames[i]);
763
+ modeMap[modeNames[i]] = newModeId;
764
+ }
765
+
766
+ // Step 3: Create all variables FIRST (values apply in a second pass so
767
+ // brace-reference values can alias variables created later in this call).
768
+ const results = [];
769
+ const warnings = [];
770
+ const resultByName = {}; // ONE results entry per token name — value-phase
771
+ // problems attach to it as valueErrors instead of
772
+ // pushing extra entries (which inflated created+failed
773
+ // past the token count).
774
+ const createdByName = {}; // exact name -> variable (this call)
775
+ const createdByLower = {}; // lowercased name -> variable (this call)
776
+ const createdDefs = []; // { def, variable } for the value pass
777
+ for (const t of tokenDefs) {
778
+ try {
779
+ const variable = figma.variables.createVariable(t.name, collection, t.resolvedType);
780
+ if (t.description) variable.description = t.description;
781
+ createdByName[t.name] = variable;
782
+ createdByLower[t.name.toLowerCase()] = variable;
783
+ createdDefs.push({ def: t, variable: variable });
784
+ const entry = { success: true, name: t.name, id: variable.id };
785
+ resultByName[t.name] = entry;
786
+ results.push(entry);
787
+ } catch (err) {
788
+ results.push({ success: false, name: t.name, error: String(err) });
789
+ }
790
+ }
791
+
792
+ // Reference resolution: '{color.blue.600}' -> variable named 'color/blue/600'.
793
+ // Priority: created-in-this-call (exact, then case-insensitive; optionally
794
+ // set-qualified by THIS collection's name), then existing local variables
795
+ // (exact, then case-insensitive; optionally set-qualified by an existing
796
+ // collection's name).
797
+ function isReference(value) {
798
+ return typeof value === 'string' && /^\\{[^{}]+\\}$/.test(value.trim());
799
+ }
800
+ let existingVars = null;
801
+ let existingCollections = null;
802
+ async function ensureExistingLoaded() {
803
+ if (existingVars === null) {
804
+ existingVars = await figma.variables.getLocalVariablesAsync();
805
+ existingCollections = await figma.variables.getLocalVariableCollectionsAsync();
806
+ }
807
+ }
808
+ function findCreated(name) {
809
+ return createdByName[name] || createdByLower[name.toLowerCase()] || null;
810
+ }
811
+ async function resolveReference(raw) {
812
+ const inner = raw.trim().slice(1, -1);
813
+ const segments = inner.split('.').filter(function (s) { return s.length > 0; });
814
+ if (segments.length === 0) return null;
815
+ const fullName = segments.join('/');
816
+
817
+ // 1. Variables created in THIS call.
818
+ let match = findCreated(fullName);
819
+ if (match) return match;
820
+ if (segments.length > 1 && segments[0].toLowerCase() === collectionName.toLowerCase()) {
821
+ match = findCreated(segments.slice(1).join('/'));
822
+ if (match) return match;
823
+ }
824
+
825
+ // 2. Existing local variables.
826
+ await ensureExistingLoaded();
827
+ match = existingVars.find(function (v) { return v.name === fullName; });
828
+ if (match) return match;
829
+ const lowerName = fullName.toLowerCase();
830
+ match = existingVars.find(function (v) { return v.name.toLowerCase() === lowerName; });
831
+ if (match) return match;
832
+ // 2b. Set-qualified: first segment names an existing collection.
833
+ if (segments.length > 1) {
834
+ const restName = segments.slice(1).join('/');
835
+ const restLower = restName.toLowerCase();
836
+ const setLower = segments[0].toLowerCase();
837
+ for (const c of existingCollections) {
838
+ if (c.name.toLowerCase() !== setLower) continue;
839
+ let m = existingVars.find(function (v) { return v.variableCollectionId === c.id && v.name === restName; });
840
+ if (!m) m = existingVars.find(function (v) { return v.variableCollectionId === c.id && v.name.toLowerCase() === restLower; });
841
+ if (m) return m;
842
+ }
843
+ }
844
+ return null;
845
+ }
846
+
847
+ // Step 4: Apply values — literals directly, brace references as aliases.
848
+ // Value-phase problems attach to the token's EXISTING results entry (as
849
+ // valueErrors) + warnings; they never add a second entry for the same name.
850
+ function noteValueProblem(name, message) {
851
+ const entry = resultByName[name];
852
+ if (entry) {
853
+ entry.valueErrors = entry.valueErrors || [];
854
+ entry.valueErrors.push(message);
855
+ }
856
+ warnings.push('Token "' + name + '": ' + message);
857
+ }
858
+ for (const entry of createdDefs) {
859
+ const t = entry.def;
860
+ const variable = entry.variable;
861
+ for (const [modeName, value] of Object.entries(t.values)) {
862
+ const modeId = modeMap[modeName];
863
+ if (!modeId) { noteValueProblem(t.name, 'Unknown mode: ' + modeName + ' — value skipped.'); continue; }
864
+ try {
865
+ if (isReference(value)) {
866
+ const target = await resolveReference(value);
867
+ if (!target) {
868
+ warnings.push('Unresolvable reference ' + value + ' for token "' + t.name + '" (mode "' + modeName + '") — no matching variable in this call or the local file. Value skipped.');
869
+ continue;
870
+ }
871
+ variable.setValueForMode(modeId, figma.variables.createVariableAlias(target));
872
+ } else if (typeof value === 'string' && value.startsWith('VariableID:')) {
873
+ variable.setValueForMode(modeId, { type: 'VARIABLE_ALIAS', id: value });
874
+ } else {
875
+ const processed = t.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
876
+ variable.setValueForMode(modeId, processed);
877
+ }
878
+ } catch (err) {
879
+ noteValueProblem(t.name, 'mode "' + modeName + '": ' + String(err));
880
+ }
881
+ }
882
+ }
883
+
884
+ return {
885
+ collectionId: collection.id,
886
+ collectionName: collectionName,
887
+ modes: modeMap,
888
+ created: results.filter(r => r.success).length,
889
+ failed: results.filter(r => !r.success).length,
890
+ results,
891
+ warnings
892
+ };`;
893
+ const timeout = Math.max(10000, tokens.length * 200 + modes.length * 500);
894
+ const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
895
+ if (result.error) {
896
+ return {
897
+ content: [
898
+ {
899
+ type: "text",
900
+ text: JSON.stringify({
901
+ error: result.error,
902
+ message: "Design token setup failed during execution",
903
+ hint: "Check the token definitions and ensure the Desktop Bridge plugin is running",
904
+ }),
905
+ },
906
+ ],
907
+ isError: true,
908
+ };
909
+ }
910
+ return {
911
+ content: [
912
+ {
913
+ type: "text",
914
+ text: JSON.stringify({
915
+ success: true,
916
+ message: `Created collection "${collectionName}" with ${modes.length} mode(s) and ${result.result?.created ?? 0} tokens`,
917
+ ...result.result,
918
+ timestamp: Date.now(),
919
+ }),
920
+ },
921
+ ],
922
+ };
923
+ }
924
+ catch (error) {
925
+ logger.error({ error }, "Failed to setup design tokens");
926
+ return {
927
+ content: [
928
+ {
929
+ type: "text",
930
+ text: JSON.stringify({
931
+ error: error instanceof Error
932
+ ? error.message
933
+ : String(error),
934
+ message: "Failed to setup design tokens",
935
+ hint: "Make sure the Desktop Bridge plugin is running in Figma",
936
+ }),
937
+ },
938
+ ],
939
+ isError: true,
940
+ };
941
+ }
942
+ });
943
+ // ============================================================================
944
+ // COMPONENT INSTANTIATION & PROPERTY TOOLS
945
+ // ============================================================================
946
+ // Tool: Instantiate Component
947
+ server.tool("figma_instantiate_component", `Create an instance of a component from the design system.
948
+
949
+ **CRITICAL: Always pass BOTH componentKey AND nodeId together!**
950
+ 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.
951
+
952
+ **IMPORTANT: Always re-search before instantiating!**
953
+ 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.
954
+
955
+ **VISUAL VALIDATION WORKFLOW:**
956
+ After instantiating components, use figma_take_screenshot to verify the result looks correct. Check placement, sizing, and visual balance.`, {
957
+ componentKey: z
958
+ .string()
959
+ .optional()
960
+ .describe("The component key from search results. Pass this WITH nodeId for automatic fallback."),
961
+ nodeId: z
962
+ .string()
963
+ .optional()
964
+ .describe("The node ID from search results. ALWAYS pass this alongside componentKey - most local components need it."),
965
+ variant: z
966
+ .record(z.string())
967
+ .optional()
968
+ .describe("Variant properties to set (e.g., { Type: 'Simple', State: 'Active' })"),
969
+ overrides: z
970
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
971
+ .optional()
972
+ .describe("Property overrides (e.g., { 'Button Label': 'Click Me' })"),
973
+ position: z
974
+ .object({
975
+ x: z.number(),
976
+ y: z.number(),
977
+ })
978
+ .optional()
979
+ .describe("Position on canvas (default: 0, 0)"),
980
+ parentId: z
981
+ .string()
982
+ .optional()
983
+ .describe("Parent node ID to append the instance to"),
984
+ }, async ({ componentKey, nodeId, variant, overrides, position, parentId, }) => {
985
+ try {
986
+ if (!componentKey && !nodeId) {
987
+ throw new Error("Either componentKey or nodeId is required");
988
+ }
989
+ const connector = await getDesktopConnector();
990
+ const result = await connector.instantiateComponent(componentKey || "", {
991
+ nodeId,
992
+ position,
993
+ overrides,
994
+ variant,
995
+ parentId,
996
+ });
997
+ if (!result.success) {
998
+ throw new Error(result.error || "Failed to instantiate component");
999
+ }
1000
+ return {
1001
+ content: [
1002
+ {
1003
+ type: "text",
1004
+ text: JSON.stringify({
1005
+ success: true,
1006
+ message: result.warnings && result.warnings.length
1007
+ ? "Component instantiated, but some overrides did not apply (see warnings)"
1008
+ : "Component instantiated successfully",
1009
+ instance: result.instance,
1010
+ ...(result.warnings && result.warnings.length
1011
+ ? { warnings: result.warnings }
1012
+ : {}),
1013
+ timestamp: Date.now(),
1014
+ }),
1015
+ },
1016
+ ],
1017
+ };
1018
+ }
1019
+ catch (error) {
1020
+ logger.error({ error }, "Failed to instantiate component");
1021
+ return {
1022
+ content: [
1023
+ {
1024
+ type: "text",
1025
+ text: JSON.stringify({
1026
+ error: error instanceof Error ? error.message : String(error),
1027
+ message: "Failed to instantiate component",
1028
+ hint: "Make sure the component key is correct and the Desktop Bridge plugin is running",
1029
+ }),
1030
+ },
1031
+ ],
1032
+ isError: true,
1033
+ };
1034
+ }
1035
+ });
1036
+ // ============================================================================
1037
+ // Component Property Management Tools
1038
+ // ============================================================================
1039
+ // Tool: Set Node Description
1040
+ 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.", {
1041
+ nodeId: z
1042
+ .string()
1043
+ .describe("The node ID of the component or style to update (e.g., '123:456')"),
1044
+ description: z.string().describe("The plain text description to set"),
1045
+ descriptionMarkdown: z
1046
+ .string()
1047
+ .optional()
1048
+ .describe("Optional rich text description using markdown formatting"),
1049
+ }, async ({ nodeId, description, descriptionMarkdown }) => {
1050
+ try {
1051
+ const connector = await getDesktopConnector();
1052
+ const result = await connector.setNodeDescription(nodeId, description, descriptionMarkdown);
1053
+ if (!result.success) {
1054
+ throw new Error(result.error || "Failed to set description");
1055
+ }
1056
+ return {
1057
+ content: [
1058
+ {
1059
+ type: "text",
1060
+ text: JSON.stringify({
1061
+ success: true,
1062
+ message: "Description set successfully",
1063
+ node: result.node,
1064
+ }),
1065
+ },
1066
+ ],
1067
+ };
1068
+ }
1069
+ catch (error) {
1070
+ logger.error({ error }, "Failed to set description");
1071
+ return {
1072
+ content: [
1073
+ {
1074
+ type: "text",
1075
+ text: JSON.stringify({
1076
+ error: error instanceof Error ? error.message : String(error),
1077
+ hint: "Make sure the node supports descriptions (components, component sets, styles)",
1078
+ }),
1079
+ },
1080
+ ],
1081
+ isError: true,
1082
+ };
1083
+ }
1084
+ });
1085
+ // Tool: Add Component Property
1086
+ 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), SLOT (freeform slot — prefer figma_create_slot for new slots).", {
1087
+ nodeId: z.string().describe("The component or component set node ID"),
1088
+ propertyName: z
1089
+ .string()
1090
+ .describe("Name for the new property (e.g., 'Show Icon', 'Button Label', 'Content')"),
1091
+ type: z
1092
+ .enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT", "SLOT"])
1093
+ .describe("Property type: BOOLEAN for toggles, TEXT for strings, INSTANCE_SWAP for component swaps, VARIANT for variant selection, SLOT for freeform slot areas"),
1094
+ defaultValue: z
1095
+ .union([z.string(), z.number(), z.boolean()])
1096
+ .optional()
1097
+ .describe("Default value. Required for TEXT (string), INSTANCE_SWAP (component id), and VARIANT (a non-empty variant option name — Figma rejects empty). Optional for BOOLEAN (defaults false). Omit for SLOT."),
1098
+ description: z
1099
+ .string()
1100
+ .optional()
1101
+ .describe("Property description (SLOT properties only)"),
1102
+ preferredValues: z
1103
+ .array(z.object({
1104
+ type: z.enum(["COMPONENT", "COMPONENT_SET"]),
1105
+ key: z.string(),
1106
+ }))
1107
+ .optional()
1108
+ .describe("Preferred components (INSTANCE_SWAP and SLOT only)"),
1109
+ }, async ({ nodeId, propertyName, type, defaultValue, description, preferredValues }) => {
1110
+ try {
1111
+ const connector = await getDesktopConnector();
1112
+ const options = {};
1113
+ if (description)
1114
+ options.description = description;
1115
+ if (preferredValues)
1116
+ options.preferredValues = preferredValues;
1117
+ // SLOT is the only type that takes no default. VARIANT requires a
1118
+ // non-empty default (Figma rejects ''), and TEXT/INSTANCE_SWAP fail
1119
+ // deep in the plugin with opaque errors when the default is missing —
1120
+ // validate here so the caller gets an actionable message.
1121
+ if ((type === "TEXT" || type === "INSTANCE_SWAP" || type === "VARIANT") &&
1122
+ (defaultValue === undefined || defaultValue === "")) {
1123
+ throw new Error(`defaultValue is required for ${type} properties` +
1124
+ (type === "INSTANCE_SWAP"
1125
+ ? " (a component id)"
1126
+ : type === "VARIANT"
1127
+ ? " (a non-empty variant option name)"
1128
+ : ""));
1129
+ }
1130
+ const resolvedDefault = type === "SLOT" ? "" : (defaultValue ?? (type === "BOOLEAN" ? false : ""));
1131
+ const result = await connector.addComponentProperty(nodeId, propertyName, type, resolvedDefault, Object.keys(options).length > 0 ? options : undefined);
1132
+ if (!result.success) {
1133
+ throw new Error(result.error || "Failed to add property");
1134
+ }
1135
+ return {
1136
+ content: [
1137
+ {
1138
+ type: "text",
1139
+ text: JSON.stringify({
1140
+ success: true,
1141
+ message: "Component property added",
1142
+ propertyName: result.propertyName,
1143
+ hint: "The property name includes a unique suffix (e.g., 'Show Icon#123:456'). Use the full name for editing/deleting.",
1144
+ }),
1145
+ },
1146
+ ],
1147
+ };
1148
+ }
1149
+ catch (error) {
1150
+ logger.error({ error }, "Failed to add component property");
1151
+ return {
1152
+ content: [
1153
+ {
1154
+ type: "text",
1155
+ text: JSON.stringify({
1156
+ error: error instanceof Error ? error.message : String(error),
1157
+ hint: "Cannot add properties to variant components. Add to the parent component set instead.",
1158
+ }),
1159
+ },
1160
+ ],
1161
+ isError: true,
1162
+ };
1163
+ }
1164
+ });
1165
+ // Tool: Edit Component Property
1166
+ 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.", {
1167
+ nodeId: z.string().describe("The component or component set node ID"),
1168
+ propertyName: z
1169
+ .string()
1170
+ .describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
1171
+ newValue: z
1172
+ .object({
1173
+ name: z.string().optional().describe("New name for the property"),
1174
+ defaultValue: z
1175
+ .union([z.string(), z.number(), z.boolean()])
1176
+ .optional()
1177
+ .describe("New default value"),
1178
+ preferredValues: z
1179
+ .array(z.object({
1180
+ type: z
1181
+ .enum(["COMPONENT", "COMPONENT_SET"])
1182
+ .describe("Type of preferred value"),
1183
+ key: z.string().describe("Component or component set key"),
1184
+ }))
1185
+ .optional()
1186
+ .describe("Preferred values (INSTANCE_SWAP only)"),
1187
+ })
1188
+ .describe("Object with the values to update"),
1189
+ }, async ({ nodeId, propertyName, newValue }) => {
1190
+ try {
1191
+ const connector = await getDesktopConnector();
1192
+ const result = await connector.editComponentProperty(nodeId, propertyName, newValue);
1193
+ if (!result.success) {
1194
+ throw new Error(result.error || "Failed to edit property");
1195
+ }
1196
+ return {
1197
+ content: [
1198
+ {
1199
+ type: "text",
1200
+ text: JSON.stringify({
1201
+ success: true,
1202
+ message: "Component property updated",
1203
+ propertyName: result.propertyName,
1204
+ }),
1205
+ },
1206
+ ],
1207
+ };
1208
+ }
1209
+ catch (error) {
1210
+ logger.error({ error }, "Failed to edit component property");
1211
+ return {
1212
+ content: [
1213
+ {
1214
+ type: "text",
1215
+ text: JSON.stringify({
1216
+ error: error instanceof Error ? error.message : String(error),
1217
+ }),
1218
+ },
1219
+ ],
1220
+ isError: true,
1221
+ };
1222
+ }
1223
+ });
1224
+ // Tool: Delete Component Property
1225
+ server.tool("figma_delete_component_property", "Delete a component property. Works with BOOLEAN, TEXT, INSTANCE_SWAP, and SLOT properties (not VARIANT). This is a destructive operation.", {
1226
+ nodeId: z.string().describe("The component or component set node ID"),
1227
+ propertyName: z
1228
+ .string()
1229
+ .describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
1230
+ }, async ({ nodeId, propertyName }) => {
1231
+ try {
1232
+ const connector = await getDesktopConnector();
1233
+ const result = await connector.deleteComponentProperty(nodeId, propertyName);
1234
+ if (!result.success) {
1235
+ throw new Error(result.error || "Failed to delete property");
1236
+ }
1237
+ return {
1238
+ content: [
1239
+ {
1240
+ type: "text",
1241
+ text: JSON.stringify({
1242
+ success: true,
1243
+ message: "Component property deleted",
1244
+ }),
1245
+ },
1246
+ ],
1247
+ };
1248
+ }
1249
+ catch (error) {
1250
+ logger.error({ error }, "Failed to delete component property");
1251
+ return {
1252
+ content: [
1253
+ {
1254
+ type: "text",
1255
+ text: JSON.stringify({
1256
+ error: error instanceof Error ? error.message : String(error),
1257
+ hint: "Cannot delete VARIANT properties. BOOLEAN, TEXT, INSTANCE_SWAP, and SLOT can be deleted.",
1258
+ }),
1259
+ },
1260
+ ],
1261
+ isError: true,
1262
+ };
1263
+ }
1264
+ });
1265
+ // ============================================================================
1266
+ // Node Manipulation Tools
1267
+ // ============================================================================
1268
+ // Tool: Resize Node
1269
+ server.tool("figma_resize_node", "Resize a node to specific dimensions. By default respects child constraints; use withConstraints=false to ignore them.", {
1270
+ nodeId: z.string().describe("The node ID to resize"),
1271
+ width: z.number().describe("New width in pixels"),
1272
+ height: z.number().describe("New height in pixels"),
1273
+ withConstraints: z
1274
+ .boolean()
1275
+ .optional()
1276
+ .default(true)
1277
+ .describe("Whether to apply child constraints during resize (default: true)"),
1278
+ }, async ({ nodeId, width, height, withConstraints }) => {
1279
+ try {
1280
+ const connector = await getDesktopConnector();
1281
+ const result = await connector.resizeNode(nodeId, width, height, withConstraints);
1282
+ if (!result.success) {
1283
+ throw new Error(result.error || "Failed to resize node");
1284
+ }
1285
+ return {
1286
+ content: [
1287
+ {
1288
+ type: "text",
1289
+ text: JSON.stringify({
1290
+ success: true,
1291
+ message: `Node resized to ${width}x${height}`,
1292
+ node: result.node,
1293
+ }),
1294
+ },
1295
+ ],
1296
+ };
1297
+ }
1298
+ catch (error) {
1299
+ logger.error({ error }, "Failed to resize node");
1300
+ return {
1301
+ content: [
1302
+ {
1303
+ type: "text",
1304
+ text: JSON.stringify({
1305
+ error: error instanceof Error ? error.message : String(error),
1306
+ }),
1307
+ },
1308
+ ],
1309
+ isError: true,
1310
+ };
1311
+ }
1312
+ });
1313
+ // Tool: Move Node
1314
+ server.tool("figma_move_node", "Move a node to a new position within its parent.", {
1315
+ nodeId: z.string().describe("The node ID to move"),
1316
+ x: z.number().describe("New X position"),
1317
+ y: z.number().describe("New Y position"),
1318
+ }, async ({ nodeId, x, y }) => {
1319
+ try {
1320
+ const connector = await getDesktopConnector();
1321
+ const result = await connector.moveNode(nodeId, x, y);
1322
+ if (!result.success) {
1323
+ throw new Error(result.error || "Failed to move node");
1324
+ }
1325
+ return {
1326
+ content: [
1327
+ {
1328
+ type: "text",
1329
+ text: JSON.stringify({
1330
+ success: true,
1331
+ message: `Node moved to (${x}, ${y})`,
1332
+ node: result.node,
1333
+ }),
1334
+ },
1335
+ ],
1336
+ };
1337
+ }
1338
+ catch (error) {
1339
+ logger.error({ error }, "Failed to move node");
1340
+ return {
1341
+ content: [
1342
+ {
1343
+ type: "text",
1344
+ text: JSON.stringify({
1345
+ error: error instanceof Error ? error.message : String(error),
1346
+ }),
1347
+ },
1348
+ ],
1349
+ isError: true,
1350
+ };
1351
+ }
1352
+ });
1353
+ // Tool: Set Node Fills
1354
+ server.tool("figma_set_fills", "Set the fill colors on a node. Accepts hex color strings (e.g., '#FF0000'). To bind a fill to a design token / color variable, pass that fill's variableId — the variable drives the color and this works on any Figma plan via the bridge (no raw figma_execute needed).", {
1355
+ nodeId: z.string().describe("The node ID to modify"),
1356
+ fills: z
1357
+ .array(z.object({
1358
+ type: z
1359
+ .literal("SOLID")
1360
+ .describe("Fill type (currently only SOLID supported)"),
1361
+ color: z
1362
+ .string()
1363
+ .optional()
1364
+ .describe("Hex color string (e.g., '#FF0000', '#FF000080' for transparency). Optional when variableId is provided."),
1365
+ opacity: z
1366
+ .number()
1367
+ .optional()
1368
+ .describe("Opacity 0-1 (default: 1)"),
1369
+ variableId: z
1370
+ .string()
1371
+ .optional()
1372
+ .describe("Bind this fill's color to a Figma variable by id (e.g. 'VariableID:1:23' from figma_get_variables). When set, the variable drives the color. Import library variables first via figma_import_library_variable."),
1373
+ }))
1374
+ .describe("Array of fill objects"),
1375
+ }, async ({ nodeId, fills }) => {
1376
+ try {
1377
+ const connector = await getDesktopConnector();
1378
+ const result = await connector.setNodeFills(nodeId, fills);
1379
+ if (!result.success) {
1380
+ throw new Error(result.error || "Failed to set fills");
1381
+ }
1382
+ return {
1383
+ content: [
1384
+ {
1385
+ type: "text",
1386
+ text: JSON.stringify({
1387
+ success: true,
1388
+ message: "Fills updated",
1389
+ node: result.node,
1390
+ }),
1391
+ },
1392
+ ],
1393
+ };
1394
+ }
1395
+ catch (error) {
1396
+ logger.error({ error }, "Failed to set fills");
1397
+ return {
1398
+ content: [
1399
+ {
1400
+ type: "text",
1401
+ text: JSON.stringify({
1402
+ error: error instanceof Error ? error.message : String(error),
1403
+ }),
1404
+ },
1405
+ ],
1406
+ isError: true,
1407
+ };
1408
+ }
1409
+ });
1410
+ // Tool: Set Image Fill on nodes
1411
+ server.tool("figma_set_image_fill", "Set an image fill on one or more Figma nodes. The imageData parameter accepts a base64-encoded " +
1412
+ "image string (JPEG/PNG). The image is decoded in the browser bridge and passed " +
1413
+ "as raw bytes to the Figma plugin. Requires Desktop Bridge plugin.", {
1414
+ nodeIds: z.array(z.string()).describe("Array of node IDs to apply the image fill to"),
1415
+ imageData: z.string().describe("Base64-encoded image data (JPEG/PNG)"),
1416
+ scaleMode: z.enum(["FILL", "FIT", "CROP", "TILE"]).optional().describe("How the image fills the node (default: FILL)"),
1417
+ }, async ({ nodeIds, imageData, scaleMode }) => {
1418
+ try {
1419
+ const connector = await getDesktopConnector();
1420
+ const result = await connector.setImageFill(nodeIds, imageData, scaleMode || "FILL");
1421
+ if (!result.success) {
1422
+ throw new Error(result.error || "Failed to set image fill");
1423
+ }
1424
+ return {
1425
+ content: [
1426
+ {
1427
+ type: "text",
1428
+ text: JSON.stringify({
1429
+ success: true,
1430
+ message: `Image fill applied to ${result.updatedCount || 0} node(s)`,
1431
+ imageHash: result.imageHash,
1432
+ nodes: result.nodes,
1433
+ }),
1434
+ },
1435
+ ],
1436
+ };
1437
+ }
1438
+ catch (error) {
1439
+ logger.error({ error }, "Failed to set image fill");
1440
+ return {
1441
+ content: [
1442
+ {
1443
+ type: "text",
1444
+ text: JSON.stringify({
1445
+ error: error instanceof Error ? error.message : String(error),
1446
+ }),
1447
+ },
1448
+ ],
1449
+ isError: true,
1450
+ };
1451
+ }
1452
+ });
1453
+ // Tool: Set Node Strokes
1454
+ server.tool("figma_set_strokes", "Set the stroke (border) on a node. Accepts hex color strings and optional stroke weight. To bind a stroke to a design token / color variable, pass that stroke's variableId — works on any Figma plan via the bridge.", {
1455
+ nodeId: z.string().describe("The node ID to modify"),
1456
+ strokes: z
1457
+ .array(z.object({
1458
+ type: z.literal("SOLID").describe("Stroke type"),
1459
+ color: z
1460
+ .string()
1461
+ .optional()
1462
+ .describe("Hex color string. Optional when variableId is provided."),
1463
+ opacity: z.number().optional().describe("Opacity 0-1"),
1464
+ variableId: z
1465
+ .string()
1466
+ .optional()
1467
+ .describe("Bind this stroke's color to a Figma variable by id (e.g. 'VariableID:1:23' from figma_get_variables). When set, the variable drives the color."),
1468
+ }))
1469
+ .describe("Array of stroke objects"),
1470
+ strokeWeight: z
1471
+ .number()
1472
+ .optional()
1473
+ .describe("Stroke thickness in pixels"),
1474
+ }, async ({ nodeId, strokes, strokeWeight }) => {
1475
+ try {
1476
+ const connector = await getDesktopConnector();
1477
+ const result = await connector.setNodeStrokes(nodeId, strokes, strokeWeight);
1478
+ if (!result.success) {
1479
+ throw new Error(result.error || "Failed to set strokes");
1480
+ }
1481
+ return {
1482
+ content: [
1483
+ {
1484
+ type: "text",
1485
+ text: JSON.stringify({
1486
+ success: true,
1487
+ message: "Strokes updated",
1488
+ node: result.node,
1489
+ }),
1490
+ },
1491
+ ],
1492
+ };
1493
+ }
1494
+ catch (error) {
1495
+ logger.error({ error }, "Failed to set strokes");
1496
+ return {
1497
+ content: [
1498
+ {
1499
+ type: "text",
1500
+ text: JSON.stringify({
1501
+ error: error instanceof Error ? error.message : String(error),
1502
+ }),
1503
+ },
1504
+ ],
1505
+ isError: true,
1506
+ };
1507
+ }
1508
+ });
1509
+ // Tool: Clone Node
1510
+ server.tool("figma_clone_node", "Duplicate a node. The clone is placed at a slight offset from the original.", {
1511
+ nodeId: z.string().describe("The node ID to clone"),
1512
+ }, async ({ nodeId }) => {
1513
+ try {
1514
+ const connector = await getDesktopConnector();
1515
+ const result = await connector.cloneNode(nodeId);
1516
+ if (!result.success) {
1517
+ throw new Error(result.error || "Failed to clone node");
1518
+ }
1519
+ return {
1520
+ content: [
1521
+ {
1522
+ type: "text",
1523
+ text: JSON.stringify({
1524
+ success: true,
1525
+ message: "Node cloned",
1526
+ clonedNode: result.node,
1527
+ }),
1528
+ },
1529
+ ],
1530
+ };
1531
+ }
1532
+ catch (error) {
1533
+ logger.error({ error }, "Failed to clone node");
1534
+ return {
1535
+ content: [
1536
+ {
1537
+ type: "text",
1538
+ text: JSON.stringify({
1539
+ error: error instanceof Error ? error.message : String(error),
1540
+ }),
1541
+ },
1542
+ ],
1543
+ isError: true,
1544
+ };
1545
+ }
1546
+ });
1547
+ // Tool: Delete Node
1548
+ server.tool("figma_delete_node", "Delete a node from the canvas. WARNING: This is a destructive operation (can be undone with Figma's undo).", {
1549
+ nodeId: z.string().describe("The node ID to delete"),
1550
+ }, async ({ nodeId }) => {
1551
+ try {
1552
+ const connector = await getDesktopConnector();
1553
+ const result = await connector.deleteNode(nodeId);
1554
+ if (!result.success) {
1555
+ throw new Error(result.error || "Failed to delete node");
1556
+ }
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: "text",
1561
+ text: JSON.stringify({
1562
+ success: true,
1563
+ message: "Node deleted",
1564
+ deleted: result.deleted,
1565
+ }),
1566
+ },
1567
+ ],
1568
+ };
1569
+ }
1570
+ catch (error) {
1571
+ logger.error({ error }, "Failed to delete node");
1572
+ return {
1573
+ content: [
1574
+ {
1575
+ type: "text",
1576
+ text: JSON.stringify({
1577
+ error: error instanceof Error ? error.message : String(error),
1578
+ }),
1579
+ },
1580
+ ],
1581
+ isError: true,
1582
+ };
1583
+ }
1584
+ });
1585
+ // Tool: Rename Node
1586
+ server.tool("figma_rename_node", "Rename a node in the layer panel.", {
1587
+ nodeId: z.string().describe("The node ID to rename"),
1588
+ newName: z.string().describe("The new name for the node"),
1589
+ }, async ({ nodeId, newName }) => {
1590
+ try {
1591
+ const connector = await getDesktopConnector();
1592
+ const result = await connector.renameNode(nodeId, newName);
1593
+ if (!result.success) {
1594
+ throw new Error(result.error || "Failed to rename node");
1595
+ }
1596
+ return {
1597
+ content: [
1598
+ {
1599
+ type: "text",
1600
+ text: JSON.stringify({
1601
+ success: true,
1602
+ message: `Node renamed to "${newName}"`,
1603
+ node: result.node,
1604
+ }),
1605
+ },
1606
+ ],
1607
+ };
1608
+ }
1609
+ catch (error) {
1610
+ logger.error({ error }, "Failed to rename node");
1611
+ return {
1612
+ content: [
1613
+ {
1614
+ type: "text",
1615
+ text: JSON.stringify({
1616
+ error: error instanceof Error ? error.message : String(error),
1617
+ }),
1618
+ },
1619
+ ],
1620
+ isError: true,
1621
+ };
1622
+ }
1623
+ });
1624
+ // Tool: Set Text Content
1625
+ server.tool("figma_set_text", "Set the text content of a text node. Optionally adjust font size and the font family/style. Font style names are space-sensitive ('Semi Bold', not 'SemiBold'), but this tool auto-corrects common no-space variants and falls back gracefully — so you don't need raw figma_execute to change typography.", {
1626
+ nodeId: z.string().describe("The text node ID"),
1627
+ text: z.string().describe("The new text content"),
1628
+ fontSize: z.number().optional().describe("Optional font size to set"),
1629
+ fontFamily: z
1630
+ .string()
1631
+ .optional()
1632
+ .describe("Optional font family to apply (e.g., 'Inter')"),
1633
+ fontStyle: z
1634
+ .string()
1635
+ .optional()
1636
+ .describe("Optional font style/weight to apply (e.g., 'Bold', 'Semi Bold'). No-space variants like 'SemiBold' are auto-corrected."),
1637
+ }, async ({ nodeId, text, fontSize, fontFamily, fontStyle }) => {
1638
+ try {
1639
+ const connector = await getDesktopConnector();
1640
+ const result = await connector.setTextContent(nodeId, text, fontSize || fontFamily || fontStyle
1641
+ ? { fontSize, fontFamily, fontStyle }
1642
+ : undefined);
1643
+ if (!result.success) {
1644
+ throw new Error(result.error || "Failed to set text");
1645
+ }
1646
+ return {
1647
+ content: [
1648
+ {
1649
+ type: "text",
1650
+ text: JSON.stringify({
1651
+ success: true,
1652
+ message: "Text content updated",
1653
+ node: result.node,
1654
+ }),
1655
+ },
1656
+ ],
1657
+ };
1658
+ }
1659
+ catch (error) {
1660
+ logger.error({ error }, "Failed to set text content");
1661
+ return {
1662
+ content: [
1663
+ {
1664
+ type: "text",
1665
+ text: JSON.stringify({
1666
+ error: error instanceof Error ? error.message : String(error),
1667
+ hint: "Make sure the node is a TEXT node",
1668
+ }),
1669
+ },
1670
+ ],
1671
+ isError: true,
1672
+ };
1673
+ }
1674
+ });
1675
+ // Tool: Create Child Node
1676
+ server.tool("figma_create_child", "Create a new child node inside a parent container. Always place inside an existing Section or Frame — never on a bare page. If no suitable parent exists, create a Section first. Clean up any empty or orphaned nodes if the operation fails.", {
1677
+ parentId: z.string().describe("The parent node ID"),
1678
+ nodeType: z
1679
+ .enum(["RECTANGLE", "ELLIPSE", "FRAME", "TEXT", "LINE"])
1680
+ .describe("Type of node to create"),
1681
+ properties: z
1682
+ .object({
1683
+ name: z.string().optional().describe("Name for the new node"),
1684
+ x: z.number().optional().describe("X position within parent"),
1685
+ y: z.number().optional().describe("Y position within parent"),
1686
+ width: z.number().optional().describe("Width (default: 100)"),
1687
+ height: z.number().optional().describe("Height (default: 100)"),
1688
+ fills: z
1689
+ .array(z.object({
1690
+ type: z.literal("SOLID"),
1691
+ color: z.string(),
1692
+ }))
1693
+ .optional()
1694
+ .describe("Fill colors (hex strings)"),
1695
+ text: z
1696
+ .string()
1697
+ .optional()
1698
+ .describe("Text content (for TEXT nodes only)"),
1699
+ })
1700
+ .optional()
1701
+ .describe("Properties for the new node"),
1702
+ }, async ({ parentId, nodeType, properties }) => {
1703
+ try {
1704
+ const connector = await getDesktopConnector();
1705
+ const result = await connector.createChildNode(parentId, nodeType, properties);
1706
+ if (!result.success) {
1707
+ throw new Error(result.error || "Failed to create node");
1708
+ }
1709
+ return {
1710
+ content: [
1711
+ {
1712
+ type: "text",
1713
+ text: JSON.stringify({
1714
+ success: true,
1715
+ message: `Created ${nodeType} node`,
1716
+ node: result.node,
1717
+ }),
1718
+ },
1719
+ ],
1720
+ };
1721
+ }
1722
+ catch (error) {
1723
+ logger.error({ error }, "Failed to create child node");
1724
+ return {
1725
+ content: [
1726
+ {
1727
+ type: "text",
1728
+ text: JSON.stringify({
1729
+ error: error instanceof Error ? error.message : String(error),
1730
+ hint: "Make sure the parent node supports children (frames, groups, etc.)",
1731
+ }),
1732
+ },
1733
+ ],
1734
+ isError: true,
1735
+ };
1736
+ }
1737
+ });
1738
+ // ============================================================================
1739
+ // Component Set Arrangement Tool
1740
+ // ============================================================================
1741
+ // Builds the in-place grid arrangement script. Shared by
1742
+ // figma_arrange_component_set and figma_create_component_set (autoArrange).
1743
+ const buildArrangeComponentSetCode = (componentSetId, componentSetName, options) => `
1744
+ // ============================================================================
1745
+ // COMPONENT SET ARRANGEMENT WITH PROPER LABELS AND CONTAINER
1746
+ // Creates: White container frame -> Row labels (left) -> Column headers (top) -> Component set (center)
1747
+ // Uses auto-layout for proper alignment of labels with grid cells
1748
+ // NON-DESTRUCTIVE: variants are repositioned in place on the existing set, so
1749
+ // the set's node ID is preserved and placed instances are unaffected.
1750
+ // ============================================================================
1751
+
1752
+ // Configuration
1753
+ const config = ${JSON.stringify(options || {})};
1754
+ const gap = config.gap ?? 24;
1755
+ const cellPadding = config.cellPadding ?? 20;
1756
+ const columnProperty = config.columnProperty || null;
1757
+
1758
+ // Layout constants
1759
+ const LABEL_FONT_SIZE = 12;
1760
+ const LABEL_COLOR = { r: 0.4, g: 0.4, b: 0.4 }; // Gray text
1761
+ const TITLE_FONT_SIZE = 24;
1762
+ const TITLE_COLOR = { r: 0.1, g: 0.1, b: 0.1 }; // Dark text
1763
+ const CONTAINER_PADDING = 40;
1764
+ const LABEL_GAP = 16; // Gap between labels and component set
1765
+ const COLUMN_HEADER_HEIGHT = 32;
1766
+
1767
+ // Find the component set
1768
+ let componentSet = null;
1769
+ const csId = ${JSON.stringify(componentSetId || null)};
1770
+ const csName = ${JSON.stringify(componentSetName || null)};
1771
+
1772
+ if (csId) {
1773
+ componentSet = await figma.getNodeByIdAsync(csId);
1774
+ } else if (csName) {
1775
+ const allNodes = figma.currentPage.findAll(n => n.type === "COMPONENT_SET" && n.name === csName);
1776
+ componentSet = allNodes[0];
1777
+ } else {
1778
+ const selection = figma.currentPage.selection;
1779
+ componentSet = selection.find(n => n.type === "COMPONENT_SET");
1780
+ }
1781
+
1782
+ if (!componentSet || componentSet.type !== "COMPONENT_SET") {
1783
+ return { error: "Component set not found. Provide componentSetId, componentSetName, or select a component set." };
1784
+ }
1785
+
1786
+ const page = figma.currentPage;
1787
+ // Absolute page coordinates — the set may be nested inside a previous
1788
+ // arrangement container, so relative x/y would be wrong for placement
1789
+ const csAbsX = componentSet.absoluteTransform[0][2];
1790
+ const csAbsY = componentSet.absoluteTransform[1][2];
1791
+ const csOriginalName = componentSet.name;
1792
+
1793
+ // Get all variant components
1794
+ const variants = componentSet.children.filter(n => n.type === "COMPONENT");
1795
+ if (variants.length === 0) {
1796
+ return { error: "No variants found in component set" };
1797
+ }
1798
+
1799
+ // Parse variant properties from names
1800
+ const parseVariantName = (name) => {
1801
+ const props = {};
1802
+ const parts = name.split(", ");
1803
+ for (const part of parts) {
1804
+ const [key, value] = part.split("=");
1805
+ if (key && value) {
1806
+ props[key.trim()] = value.trim();
1807
+ }
1808
+ }
1809
+ return props;
1810
+ };
1811
+
1812
+ // Collect all properties and their unique values (preserving order)
1813
+ const propertyValues = {};
1814
+ const propertyOrder = [];
1815
+ for (const variant of variants) {
1816
+ const props = parseVariantName(variant.name);
1817
+ for (const [key, value] of Object.entries(props)) {
1818
+ if (!propertyValues[key]) {
1819
+ propertyValues[key] = new Set();
1820
+ propertyOrder.push(key);
1821
+ }
1822
+ propertyValues[key].add(value);
1823
+ }
1824
+ }
1825
+ for (const key of Object.keys(propertyValues)) {
1826
+ propertyValues[key] = Array.from(propertyValues[key]);
1827
+ }
1828
+
1829
+ // Determine grid structure: columns = last property (usually State), rows = other properties
1830
+ const columnProp = columnProperty || propertyOrder[propertyOrder.length - 1];
1831
+ const columnValues = propertyValues[columnProp] || [];
1832
+ const rowProps = propertyOrder.filter(p => p !== columnProp);
1833
+
1834
+ // Generate all row combinations
1835
+ const generateRowCombinations = (props, values) => {
1836
+ if (props.length === 0) return [{}];
1837
+ if (props.length === 1) {
1838
+ return values[props[0]].map(v => ({ [props[0]]: v }));
1839
+ }
1840
+ const result = [];
1841
+ const firstProp = props[0];
1842
+ const restProps = props.slice(1);
1843
+ const restCombos = generateRowCombinations(restProps, values);
1844
+ for (const value of values[firstProp]) {
1845
+ for (const combo of restCombos) {
1846
+ result.push({ [firstProp]: value, ...combo });
1847
+ }
1848
+ }
1849
+ return result;
1850
+ };
1851
+ const rowCombinations = generateRowCombinations(rowProps, propertyValues);
1852
+
1853
+ const totalCols = columnValues.length;
1854
+ const totalRows = rowCombinations.length;
1855
+
1856
+ // Calculate max variant dimensions
1857
+ let maxVariantWidth = 0;
1858
+ let maxVariantHeight = 0;
1859
+ for (const v of variants) {
1860
+ if (v.width > maxVariantWidth) maxVariantWidth = v.width;
1861
+ if (v.height > maxVariantHeight) maxVariantHeight = v.height;
1862
+ }
1863
+
1864
+ // Calculate cell dimensions (each cell in the grid)
1865
+ const cellWidth = Math.ceil(maxVariantWidth + cellPadding);
1866
+ const cellHeight = Math.ceil(maxVariantHeight + cellPadding);
1867
+
1868
+ // Calculate component set dimensions
1869
+ const edgePadding = 24; // Padding inside component set
1870
+ const csWidth = (totalCols * cellWidth) + ((totalCols - 1) * gap) + (edgePadding * 2);
1871
+ const csHeight = (totalRows * cellHeight) + ((totalRows - 1) * gap) + (edgePadding * 2);
1872
+
1873
+ // ============================================================================
1874
+ // STEP 1: Remove old labels and container frames from previous arrangements
1875
+ // ============================================================================
1876
+ const isOldArrangementElement = (n) =>
1877
+ (n.type === "TEXT" && (n.name.startsWith("Row: ") || n.name.startsWith("Col: "))) ||
1878
+ (n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"));
1879
+
1880
+ // If the set lives inside a previous arrangement container (re-run case),
1881
+ // pull it out to the page first so cleanup can't delete it
1882
+ let ancestor = componentSet.parent;
1883
+ while (ancestor && ancestor.type !== "PAGE") {
1884
+ if (isOldArrangementElement(ancestor)) {
1885
+ page.appendChild(componentSet);
1886
+ break;
1887
+ }
1888
+ ancestor = ancestor.parent;
1889
+ }
1890
+
1891
+ const oldElements = page.children.filter(isOldArrangementElement);
1892
+ for (const el of oldElements) {
1893
+ el.remove();
1894
+ }
1895
+
1896
+ // ============================================================================
1897
+ // STEP 2: Prepare the EXISTING component set for manual grid positioning.
1898
+ // No clone/remove/combineAsVariants — the set keeps its node ID, so all
1899
+ // placed instances survive.
1900
+ // ============================================================================
1901
+
1902
+ // Auto-layout would ignore manual x/y on children — disable it if present
1903
+ if ("layoutMode" in componentSet && componentSet.layoutMode !== "NONE") {
1904
+ componentSet.layoutMode = "NONE";
1905
+ }
1906
+
1907
+ // Apply purple dashed border (Figma's native component set styling)
1908
+ componentSet.strokes = [{
1909
+ type: 'SOLID',
1910
+ color: { r: 151/255, g: 71/255, b: 255/255 } // Figma's purple: #9747FF
1911
+ }];
1912
+ componentSet.dashPattern = [10, 5];
1913
+ componentSet.strokeWeight = 1;
1914
+ componentSet.strokeAlign = "INSIDE";
1915
+
1916
+ // ============================================================================
1917
+ // STEP 3: Arrange variants in grid pattern inside component set (in place)
1918
+ // ============================================================================
1919
+ for (const variant of variants) {
1920
+ const props = parseVariantName(variant.name);
1921
+ const colValue = props[columnProp];
1922
+ const colIdx = columnValues.indexOf(colValue);
1923
+
1924
+ // Find matching row
1925
+ let rowIdx = -1;
1926
+ for (let i = 0; i < rowCombinations.length; i++) {
1927
+ const combo = rowCombinations[i];
1928
+ let match = true;
1929
+ for (const [key, value] of Object.entries(combo)) {
1930
+ if (props[key] !== value) {
1931
+ match = false;
1932
+ break;
1933
+ }
1934
+ }
1935
+ if (match) {
1936
+ rowIdx = i;
1937
+ break;
1938
+ }
1939
+ }
1940
+
1941
+ if (colIdx >= 0 && rowIdx >= 0) {
1942
+ // Calculate cell position
1943
+ const cellX = edgePadding + colIdx * (cellWidth + gap);
1944
+ const cellY = edgePadding + rowIdx * (cellHeight + gap);
1945
+
1946
+ // Center variant within cell
1947
+ const variantX = Math.round(cellX + (cellWidth - variant.width) / 2);
1948
+ const variantY = Math.round(cellY + (cellHeight - variant.height) / 2);
1949
+
1950
+ variant.x = variantX;
1951
+ variant.y = variantY;
1952
+ }
1953
+ }
1954
+
1955
+ // Resize component set to fit grid
1956
+ componentSet.resize(csWidth, csHeight);
1957
+
1958
+ // ============================================================================
1959
+ // STEP 4: Create white container frame with proper structure
1960
+ // ============================================================================
1961
+
1962
+ // Load font for labels
1963
+ await figma.loadFontAsync({ family: "Inter", style: "Regular" });
1964
+ await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" });
1965
+
1966
+ // Create the main container frame (white background)
1967
+ const containerFrame = figma.createFrame();
1968
+ containerFrame.name = "Component Container";
1969
+ containerFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; // White
1970
+ containerFrame.cornerRadius = 8;
1971
+ containerFrame.layoutMode = 'VERTICAL';
1972
+ containerFrame.primaryAxisSizingMode = 'AUTO';
1973
+ containerFrame.counterAxisSizingMode = 'AUTO';
1974
+ containerFrame.paddingTop = CONTAINER_PADDING;
1975
+ containerFrame.paddingRight = CONTAINER_PADDING;
1976
+ containerFrame.paddingBottom = CONTAINER_PADDING;
1977
+ containerFrame.paddingLeft = CONTAINER_PADDING;
1978
+ containerFrame.itemSpacing = 24;
1979
+
1980
+ // Add title
1981
+ const titleText = figma.createText();
1982
+ titleText.name = "Title";
1983
+ titleText.characters = csOriginalName;
1984
+ titleText.fontSize = TITLE_FONT_SIZE;
1985
+ titleText.fontName = { family: "Inter", style: "Semi Bold" };
1986
+ titleText.fills = [{ type: 'SOLID', color: TITLE_COLOR }];
1987
+ // Append to parent FIRST, then set layoutSizing
1988
+ containerFrame.appendChild(titleText);
1989
+ titleText.layoutSizingHorizontal = 'HUG';
1990
+ titleText.layoutSizingVertical = 'HUG';
1991
+
1992
+ // Create content row (horizontal: row labels + grid column)
1993
+ const contentRow = figma.createFrame();
1994
+ contentRow.name = "Content Row";
1995
+ contentRow.fills = []; // Transparent
1996
+ contentRow.layoutMode = 'HORIZONTAL';
1997
+ contentRow.primaryAxisSizingMode = 'AUTO';
1998
+ contentRow.counterAxisSizingMode = 'AUTO';
1999
+ contentRow.itemSpacing = LABEL_GAP;
2000
+ contentRow.counterAxisAlignItems = 'MIN'; // Align to top
2001
+ containerFrame.appendChild(contentRow);
2002
+
2003
+ // ============================================================================
2004
+ // STEP 5: Create row labels column (left side)
2005
+ // ============================================================================
2006
+ const rowLabelsFrame = figma.createFrame();
2007
+ rowLabelsFrame.name = "Row Labels";
2008
+ rowLabelsFrame.fills = []; // Transparent
2009
+ rowLabelsFrame.layoutMode = 'VERTICAL';
2010
+ rowLabelsFrame.primaryAxisSizingMode = 'AUTO';
2011
+ rowLabelsFrame.counterAxisSizingMode = 'AUTO';
2012
+ rowLabelsFrame.counterAxisAlignItems = 'MAX'; // Right-align text
2013
+ rowLabelsFrame.itemSpacing = 0; // No spacing - we'll use fixed heights
2014
+
2015
+ // Add spacer for column headers alignment
2016
+ // Must account for: column header height + gap + component set's internal edgePadding
2017
+ const rowLabelSpacer = figma.createFrame();
2018
+ rowLabelSpacer.name = "Spacer";
2019
+ rowLabelSpacer.fills = [];
2020
+ rowLabelSpacer.resize(10, COLUMN_HEADER_HEIGHT + gap + edgePadding); // Align with first row inside component set
2021
+ rowLabelsFrame.appendChild(rowLabelSpacer);
2022
+ // IMPORTANT: Set layoutSizing AFTER appendChild (node must be in auto-layout parent first)
2023
+ rowLabelSpacer.layoutSizingVertical = 'FIXED';
2024
+
2025
+ // Create row labels - each with VERTICAL layout for direct vertical centering
2026
+ // Using VERTICAL layout: primaryAxis = vertical, counterAxis = horizontal
2027
+ // So primaryAxisAlignItems = 'CENTER' directly controls vertical centering
2028
+ for (let i = 0; i < rowCombinations.length; i++) {
2029
+ const combo = rowCombinations[i];
2030
+ const labelText = rowProps.map(p => combo[p]).join(" / ");
2031
+ const isLastRow = (i === rowCombinations.length - 1);
2032
+
2033
+ // Create a frame to hold the label with VERTICAL layout
2034
+ const rowLabelContainer = figma.createFrame();
2035
+ rowLabelContainer.name = "Row: " + labelText;
2036
+ rowLabelContainer.fills = [];
2037
+ rowLabelContainer.layoutMode = 'VERTICAL'; // VERTICAL so primaryAxis controls Y
2038
+ rowLabelContainer.primaryAxisSizingMode = 'FIXED'; // CRITICAL: Don't hug content, maintain fixed height
2039
+ rowLabelContainer.primaryAxisAlignItems = 'CENTER'; // CENTER = vertically centered within fixed height
2040
+ rowLabelContainer.counterAxisAlignItems = 'MAX'; // MAX = right-aligned horizontally
2041
+
2042
+ // Fixed height = cellHeight only (gap handled separately below)
2043
+ rowLabelContainer.resize(10, cellHeight);
2044
+
2045
+ const label = figma.createText();
2046
+ label.characters = labelText;
2047
+ label.fontSize = LABEL_FONT_SIZE;
2048
+ label.fontName = { family: "Inter", style: "Regular" };
2049
+ label.fills = [{ type: 'SOLID', color: LABEL_COLOR }];
2050
+ label.textAlignHorizontal = 'RIGHT';
2051
+ rowLabelContainer.appendChild(label);
2052
+
2053
+ // Append to parent FIRST, then set layoutSizing properties
2054
+ rowLabelsFrame.appendChild(rowLabelContainer);
2055
+ rowLabelContainer.layoutSizingHorizontal = 'HUG';
2056
+ rowLabelContainer.layoutSizingVertical = 'FIXED';
2057
+
2058
+ // Add gap spacer AFTER the row label (except for the last row)
2059
+ // This separates the gap from the centering calculation entirely
2060
+ if (!isLastRow) {
2061
+ const gapSpacer = figma.createFrame();
2062
+ gapSpacer.name = "Row Gap";
2063
+ gapSpacer.fills = [];
2064
+ gapSpacer.resize(1, gap);
2065
+ rowLabelsFrame.appendChild(gapSpacer);
2066
+ // Plain frames can only use FIXED or FILL (not HUG)
2067
+ gapSpacer.layoutSizingHorizontal = 'FIXED';
2068
+ gapSpacer.layoutSizingVertical = 'FIXED';
2069
+ }
2070
+ }
2071
+
2072
+ contentRow.appendChild(rowLabelsFrame);
2073
+
2074
+ // ============================================================================
2075
+ // STEP 6: Create grid column (column headers + component set)
2076
+ // ============================================================================
2077
+ const gridColumn = figma.createFrame();
2078
+ gridColumn.name = "Grid Column";
2079
+ gridColumn.fills = []; // Transparent
2080
+ gridColumn.layoutMode = 'VERTICAL';
2081
+ gridColumn.primaryAxisSizingMode = 'AUTO';
2082
+ gridColumn.counterAxisSizingMode = 'AUTO';
2083
+ gridColumn.itemSpacing = gap;
2084
+
2085
+ // Create column headers row
2086
+ const columnHeadersRow = figma.createFrame();
2087
+ columnHeadersRow.name = "Column Headers";
2088
+ columnHeadersRow.fills = [];
2089
+ columnHeadersRow.layoutMode = 'HORIZONTAL';
2090
+ columnHeadersRow.resize(csWidth, COLUMN_HEADER_HEIGHT);
2091
+ columnHeadersRow.itemSpacing = 0; // No spacing - we control widths precisely
2092
+ columnHeadersRow.paddingLeft = edgePadding; // Match component set edge padding
2093
+ columnHeadersRow.paddingRight = edgePadding;
2094
+
2095
+ // Create column header labels - each with width matching cell + gap
2096
+ for (let i = 0; i < columnValues.length; i++) {
2097
+ const colValue = columnValues[i];
2098
+ const isLastCol = (i === columnValues.length - 1);
2099
+
2100
+ const colHeaderContainer = figma.createFrame();
2101
+ colHeaderContainer.name = "Col: " + colValue;
2102
+ colHeaderContainer.fills = [];
2103
+ colHeaderContainer.layoutMode = 'HORIZONTAL';
2104
+ colHeaderContainer.primaryAxisAlignItems = 'CENTER'; // Center horizontally
2105
+ colHeaderContainer.counterAxisAlignItems = 'MAX'; // Align to bottom
2106
+
2107
+ // Set width to match cell + gap (except last column)
2108
+ // Use paddingRight to push the gap to the RIGHT of the centered text area
2109
+ const colWidth = isLastCol ? cellWidth : cellWidth + gap;
2110
+ colHeaderContainer.resize(colWidth, COLUMN_HEADER_HEIGHT);
2111
+ if (!isLastCol) {
2112
+ colHeaderContainer.paddingRight = gap; // Gap goes right, text centers in cellWidth
2113
+ }
2114
+
2115
+ const label = figma.createText();
2116
+ label.characters = colValue;
2117
+ label.fontSize = LABEL_FONT_SIZE;
2118
+ label.fontName = { family: "Inter", style: "Regular" };
2119
+ label.fills = [{ type: 'SOLID', color: LABEL_COLOR }];
2120
+ label.textAlignHorizontal = 'CENTER';
2121
+ colHeaderContainer.appendChild(label);
2122
+
2123
+ // Append to parent FIRST, then set layoutSizing
2124
+ columnHeadersRow.appendChild(colHeaderContainer);
2125
+ colHeaderContainer.layoutSizingHorizontal = 'FIXED';
2126
+ colHeaderContainer.layoutSizingVertical = 'FILL';
2127
+ }
2128
+
2129
+ // Append to parent FIRST, then set layoutSizing
2130
+ gridColumn.appendChild(columnHeadersRow);
2131
+ columnHeadersRow.layoutSizingHorizontal = 'FIXED';
2132
+ columnHeadersRow.layoutSizingVertical = 'FIXED';
2133
+
2134
+ // Create a wrapper frame to hold the component set (since component sets don't work well in auto-layout)
2135
+ const componentSetWrapper = figma.createFrame();
2136
+ componentSetWrapper.name = "Component Set Wrapper";
2137
+ componentSetWrapper.fills = [];
2138
+ componentSetWrapper.resize(csWidth, csHeight);
2139
+
2140
+ // Move the existing component set inside wrapper (reparenting preserves the
2141
+ // node ID — placed instances are unaffected)
2142
+ componentSetWrapper.appendChild(componentSet);
2143
+ componentSet.x = 0;
2144
+ componentSet.y = 0;
2145
+
2146
+ // Append to parent FIRST, then set layoutSizing
2147
+ gridColumn.appendChild(componentSetWrapper);
2148
+ componentSetWrapper.layoutSizingHorizontal = 'FIXED';
2149
+ componentSetWrapper.layoutSizingVertical = 'FIXED';
2150
+
2151
+ contentRow.appendChild(gridColumn);
2152
+
2153
+ // Position container at original location
2154
+ containerFrame.x = csAbsX - CONTAINER_PADDING - 120; // Account for row labels width
2155
+ containerFrame.y = csAbsY - CONTAINER_PADDING - TITLE_FONT_SIZE - 24 - COLUMN_HEADER_HEIGHT - gap;
2156
+
2157
+ // Select and zoom to show result
2158
+ figma.currentPage.selection = [containerFrame];
2159
+ figma.viewport.scrollAndZoomIntoView([containerFrame]);
2160
+
2161
+ return {
2162
+ success: true,
2163
+ message: "Component set arranged in place with proper container, labels, and alignment. Set identity preserved — placed instances unaffected.",
2164
+ containerId: containerFrame.id,
2165
+ componentSetId: componentSet.id,
2166
+ componentSetName: componentSet.name,
2167
+ grid: {
2168
+ rows: totalRows,
2169
+ columns: totalCols,
2170
+ cellWidth: cellWidth,
2171
+ cellHeight: cellHeight,
2172
+ gap: gap,
2173
+ columnProperty: columnProp,
2174
+ columnValues: columnValues,
2175
+ rowProperties: rowProps,
2176
+ rowLabels: rowCombinations.map(combo => rowProps.map(p => combo[p]).join(" / "))
2177
+ },
2178
+ componentSetSize: { width: csWidth, height: csHeight },
2179
+ variantCount: variants.length,
2180
+ structure: {
2181
+ container: "White frame with title, row labels, column headers, and component set",
2182
+ rowLabels: "Vertically aligned with each row's center",
2183
+ columnHeaders: "Horizontally aligned with each column's center"
2184
+ }
2185
+ };
2186
+ `;
2187
+ // Tool: Arrange Component Set (Professional Layout with Native Visualization)
2188
+ // Rearranges variants IN PLACE (sets x/y on existing children) so the component
2189
+ // set's identity is preserved and placed instances are unaffected
2190
+ 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.
2191
+
2192
+ Non-destructive: rearranges the existing variants in place (grid positions on the existing set's children), so the component set keeps its node ID and all placed instances remain intact. Arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size) and wraps the set in a white container with title, row/column labels. Safe to run on component sets with placed instances, and safe to re-run.`, {
2193
+ componentSetId: z
2194
+ .string()
2195
+ .optional()
2196
+ .describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
2197
+ componentSetName: z
2198
+ .string()
2199
+ .optional()
2200
+ .describe("Name of the component set to find. Used if componentSetId not provided."),
2201
+ options: z
2202
+ .object({
2203
+ gap: z
2204
+ .number()
2205
+ .optional()
2206
+ .default(24)
2207
+ .describe("Gap between grid cells in pixels (default: 24)"),
2208
+ cellPadding: z
2209
+ .number()
2210
+ .optional()
2211
+ .default(20)
2212
+ .describe("Padding inside each cell around the variant (default: 20)"),
2213
+ columnProperty: z
2214
+ .string()
2215
+ .optional()
2216
+ .describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
2217
+ })
2218
+ .optional()
2219
+ .describe("Layout options"),
2220
+ }, async ({ componentSetId, componentSetName, options }) => {
2221
+ try {
2222
+ const connector = await getDesktopConnector();
2223
+ const code = buildArrangeComponentSetCode(componentSetId ?? null, componentSetName ?? null, options);
2224
+ const result = await connector.executeCodeViaUI(code, 25000);
2225
+ if (!result.success) {
2226
+ throw new Error(result.error || "Failed to arrange component set");
2227
+ }
2228
+ return {
2229
+ content: [
2230
+ {
2231
+ type: "text",
2232
+ text: JSON.stringify({
2233
+ ...result.result,
2234
+ hint: result.result?.success
2235
+ ? "Component set arranged in place (same node ID, placed instances unaffected) inside 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."
2236
+ : undefined,
2237
+ }),
2238
+ },
2239
+ ],
2240
+ };
2241
+ }
2242
+ catch (error) {
2243
+ logger.error({ error }, "Failed to arrange component set");
2244
+ return {
2245
+ content: [
2246
+ {
2247
+ type: "text",
2248
+ text: JSON.stringify({
2249
+ error: error instanceof Error ? error.message : String(error),
2250
+ hint: "Make sure the Desktop Bridge plugin is running and a component set exists.",
2251
+ }),
2252
+ },
2253
+ ],
2254
+ isError: true,
2255
+ };
2256
+ }
2257
+ });
2258
+ // Tool: Create Component Set with variants (structured command — no hand-written
2259
+ // figma_execute scripts, no 30s execution-cap juggling for callers)
2260
+ server.tool("figma_create_component_set", `Create a component set with variants in one call — replaces hand-written figma.combineAsVariants scripts.
2261
+
2262
+ Two modes:
2263
+ 1. **Generate from a base component**: pass baseComponentId + properties (variant axes). The base is cloned for every combination of the axes ({ State: ['default','hover','disabled'], Size: ['sm','lg'] } → 6 variants), each named 'Prop=Value' comma-joined (e.g. 'State=hover, Size=sm'), then combined into a set. The base component itself becomes the FIRST variant (same node ID), so existing instances of the base survive as instances of that variant.
2264
+ 2. **Combine existing components**: pass componentIds, optionally with variantProperties (aligned 1:1) to rename each component to Prop=Value form before combining.
2265
+
2266
+ Figma derives the variant property definitions from the names; they live on the SET (componentPropertyDefinitions), not on individual variants. The result includes each variant's key — instantiate with a VARIANT's key/nodeId via figma_instantiate_component, not the set's key.
2267
+
2268
+ Set autoArrange:true to lay the new set out as a labeled grid inside a white container (same layout as figma_arrange_component_set). Requires Desktop Bridge plugin.
2269
+
2270
+ SIZE GUIDANCE: hard cap 100 variants. The timeout auto-scales with variant count (~1.2s/variant, 30s floor / 2min cap), but above ~40 variants the single-pass clone+combine gets slow and heavy base components may still push the limit — prefer splitting large matrices into multiple sets (e.g. one set per Size).`, {
2271
+ baseComponentId: z
2272
+ .string()
2273
+ .optional()
2274
+ .describe("Node ID of an existing COMPONENT to use as the base. Cloned per property combination; becomes the set's first variant (keeps its node ID, so placed instances survive). Mutually exclusive with componentIds."),
2275
+ properties: z
2276
+ .record(z.string(), z.array(z.string()))
2277
+ .optional()
2278
+ .describe("Variant property axes — required with baseComponentId. Example: { State: ['default','hover','disabled'], Size: ['sm','lg'] } creates 6 variants. Max 100 combinations. Names and values must not contain '=' or ','."),
2279
+ componentIds: z
2280
+ .array(z.string())
2281
+ .optional()
2282
+ .describe("Node IDs of existing COMPONENT nodes to combine as variants. Mutually exclusive with baseComponentId. Components already inside a component set are rejected."),
2283
+ variantProperties: z
2284
+ .array(z.record(z.string()))
2285
+ .optional()
2286
+ .describe("Only with componentIds: one property map per component, aligned by index — e.g. [{ State: 'default' }, { State: 'hover' }]. Each component is renamed to 'Prop=Value, ...' before combining. Without this, existing names are kept (names lacking '=' become 'Property 1=<name>')."),
2287
+ name: z
2288
+ .string()
2289
+ .optional()
2290
+ .describe("Name for the component set (e.g., 'Button'). Defaults to Figma's derived name."),
2291
+ parentId: z
2292
+ .string()
2293
+ .optional()
2294
+ .describe("Node ID of the container (frame/section) to create the set in. Defaults to the current page."),
2295
+ position: z
2296
+ .object({ x: z.number(), y: z.number() })
2297
+ .optional()
2298
+ .describe("Position of the set within its parent."),
2299
+ autoArrange: z
2300
+ .boolean()
2301
+ .optional()
2302
+ .default(false)
2303
+ .describe("If true, arrange the new set in a labeled grid (columns = last property, rows = other properties) inside a white container — same in-place layout as figma_arrange_component_set."),
2304
+ arrangeOptions: z
2305
+ .object({
2306
+ gap: z.number().optional().describe("Gap between grid cells in pixels (default: 24)"),
2307
+ cellPadding: z.number().optional().describe("Padding inside each cell around the variant (default: 20)"),
2308
+ columnProperty: z.string().optional().describe("Property to use for columns (default: last property)"),
2309
+ })
2310
+ .optional()
2311
+ .describe("Grid layout options, used when autoArrange is true."),
2312
+ }, async ({ baseComponentId, properties, componentIds, variantProperties, name, parentId, position, autoArrange, arrangeOptions, }) => {
2313
+ try {
2314
+ if (!baseComponentId && (!componentIds || componentIds.length === 0)) {
2315
+ throw new Error("Provide either baseComponentId + properties (generate variants from a base) or componentIds (combine existing components)");
2316
+ }
2317
+ if (baseComponentId && componentIds && componentIds.length > 0) {
2318
+ throw new Error("baseComponentId and componentIds are mutually exclusive — pick one mode");
2319
+ }
2320
+ if (baseComponentId &&
2321
+ (!properties || Object.keys(properties).length === 0)) {
2322
+ throw new Error("properties is required with baseComponentId. Example: { State: ['default','hover'], Size: ['sm','lg'] }");
2323
+ }
2324
+ // Variant count drives the connector's scaled timeout; above ~40 the
2325
+ // single-pass clone+combine gets slow, so surface a heads-up.
2326
+ let requestedVariantCount = componentIds?.length ?? 1;
2327
+ if (properties && !componentIds?.length) {
2328
+ requestedVariantCount = 1;
2329
+ for (const values of Object.values(properties)) {
2330
+ requestedVariantCount *= Math.max(1, values?.length ?? 1);
2331
+ }
2332
+ }
2333
+ const sizeWarning = requestedVariantCount > 40
2334
+ ? `${requestedVariantCount} variants requested — large sets are slow to build (timeout scales automatically, ~1.2s/variant, 2min cap) and heavy base components may still time out. Consider splitting into multiple sets (e.g. one per Size).`
2335
+ : undefined;
2336
+ const connector = await getDesktopConnector();
2337
+ const result = await connector.createComponentSet({
2338
+ baseComponentId,
2339
+ properties,
2340
+ componentIds,
2341
+ variantProperties,
2342
+ name,
2343
+ parentId,
2344
+ position,
2345
+ });
2346
+ if (!result.success) {
2347
+ throw new Error(result.error || "Failed to create component set");
2348
+ }
2349
+ const data = result.data || {};
2350
+ // Optional in-place grid arrangement — reuses the same script as
2351
+ // figma_arrange_component_set. Creation already succeeded, so an
2352
+ // arrange failure is reported as a warning, not an error.
2353
+ let arrange;
2354
+ if (autoArrange && data.componentSet?.id) {
2355
+ try {
2356
+ const arrangeResult = await connector.executeCodeViaUI(buildArrangeComponentSetCode(data.componentSet.id, null, arrangeOptions), 25000);
2357
+ if (arrangeResult.success &&
2358
+ arrangeResult.result &&
2359
+ !arrangeResult.result.error) {
2360
+ arrange = {
2361
+ arranged: true,
2362
+ containerId: arrangeResult.result.containerId,
2363
+ grid: arrangeResult.result.grid,
2364
+ };
2365
+ }
2366
+ else {
2367
+ arrange = {
2368
+ arranged: false,
2369
+ error: arrangeResult.error ||
2370
+ arrangeResult.result?.error ||
2371
+ "Arrange failed",
2372
+ };
2373
+ }
2374
+ }
2375
+ catch (arrangeError) {
2376
+ arrange = {
2377
+ arranged: false,
2378
+ error: arrangeError instanceof Error
2379
+ ? arrangeError.message
2380
+ : String(arrangeError),
2381
+ };
2382
+ }
2383
+ }
2384
+ return {
2385
+ content: [
2386
+ {
2387
+ type: "text",
2388
+ text: JSON.stringify({
2389
+ success: true,
2390
+ message: `Created component set "${data.componentSet?.name ?? name ?? ""}" with ${data.variantCount ?? "?"} variants`,
2391
+ ...data,
2392
+ ...(sizeWarning ? { sizeWarning } : {}),
2393
+ ...(arrange ? { arrange } : {}),
2394
+ hint: "To place instances, pass a VARIANT's key/nodeId from variants[] to figma_instantiate_component — not the set's key. Property definitions live on the set (componentPropertyDefinitions). Use figma_capture_screenshot to verify the result.",
2395
+ }),
2396
+ },
2397
+ ],
2398
+ };
2399
+ }
2400
+ catch (error) {
2401
+ logger.error({ error }, "Failed to create component set");
2402
+ return {
2403
+ content: [
2404
+ {
2405
+ type: "text",
2406
+ text: JSON.stringify({
2407
+ error: error instanceof Error ? error.message : String(error),
2408
+ hint: "Make sure the Desktop Bridge plugin is running. baseComponentId/componentIds must reference COMPONENT nodes that are not already inside a component set. Node IDs are session-specific — re-search components if they may be stale.",
2409
+ }),
2410
+ },
2411
+ ],
2412
+ isError: true,
2413
+ };
2414
+ }
2415
+ });
2416
+ // Tool: Lint Design for accessibility and quality issues
2417
+ server.tool("figma_lint_design", "Run comprehensive accessibility (WCAG 2.2) and design quality checks on the current page or a specific node tree. " +
2418
+ "WCAG conformance checks (10 rules): color contrast (AA), non-text contrast (1.4.11), color-only differentiation (1.4.1), " +
2419
+ "focus indicators (2.4.7), touch targets (2.5.8), image alt text (1.1.1), heading hierarchy (1.3.1), " +
2420
+ "reflow/responsive (1.4.10), reading order (1.3.2), and disabled context (4.1.2). " +
2421
+ "Best-practice readability hints (opt-in via rules: ['best-practice'] or ['all']): text sizing, line height, letter spacing, paragraph spacing. " +
2422
+ "Note: line/paragraph spacing below 1.5x/2x is NOT a WCAG 1.4.12 failure — 1.4.12 requires supporting user spacing overrides without breaking (a code concern, see figma_scan_code_accessibility), not specific design values — so these are non-normative hints scoped to multi-line text only. " +
2423
+ "Design system checks: hardcoded colors, missing text styles, default names, detached components. " +
2424
+ "Layout checks: missing auto-layout, empty containers. " +
2425
+ "Default audit runs WCAG + design-system + layout (best-practice hints excluded). " +
2426
+ "Returns categorized findings with severity levels (critical/warning/info) and WCAG conformance level (a/aa/aaa/best-practice) so teams can filter by target level. " +
2427
+ "Use natural language like 'check my design for accessibility issues' or 'lint this page'. " +
2428
+ "Requires Desktop Bridge plugin.", {
2429
+ nodeId: z.string().optional().describe("Node ID to lint (defaults to current page)"),
2430
+ rules: z.array(z.string()).optional().describe("Rule filter. Default (omitted) = ['wcag','design-system','layout'] — real WCAG conformance plus quality checks, with best-practice hints excluded. Groups: ['wcag'] (10 conformance rules), ['best-practice'] (text-size, line-height, letter-spacing, paragraph-spacing), ['all'] (everything incl. best-practice), ['design-system'], ['layout']. Or specific rule IDs like ['wcag-contrast', 'wcag-focus-indicator', 'wcag-disabled-no-context']."),
2431
+ maxDepth: z.number().optional().describe("Maximum tree depth to traverse (default: 10)"),
2432
+ maxFindings: z.number().optional().describe("Maximum findings before stopping (default: 100)"),
2433
+ }, async ({ nodeId, rules, maxDepth, maxFindings }) => {
2434
+ try {
2435
+ const connector = await getDesktopConnector();
2436
+ const result = await connector.lintDesign(nodeId, rules || ['wcag', 'design-system', 'layout'], maxDepth || 10, maxFindings || 100);
2437
+ if (!result.success) {
2438
+ throw new Error(result.error || "Lint failed");
2439
+ }
2440
+ return {
2441
+ content: [
2442
+ {
2443
+ type: "text",
2444
+ text: JSON.stringify(result.data || result, null, 2),
2445
+ },
2446
+ ],
2447
+ };
2448
+ }
2449
+ catch (error) {
2450
+ logger.error({ error }, "Failed to lint design");
2451
+ return {
2452
+ content: [
2453
+ {
2454
+ type: "text",
2455
+ text: JSON.stringify({
2456
+ error: error instanceof Error ? error.message : String(error),
2457
+ hint: "Make sure the Desktop Bridge plugin is running in your Figma file.",
2458
+ }),
2459
+ },
2460
+ ],
2461
+ isError: true,
2462
+ };
2463
+ }
2464
+ });
2465
+ // Tool: Audit Component Accessibility
2466
+ server.tool("figma_audit_component_accessibility", "Deep accessibility audit for a specific component or component set. Produces a scorecard covering: " +
2467
+ "state coverage (default/hover/focus/disabled/error/active/loading), focus indicator quality and contrast, " +
2468
+ "non-color differentiation (WCAG 1.4.1), target size consistency (WCAG 2.5.8), annotation completeness, " +
2469
+ "and color-blind simulation (protanopia/deuteranopia/tritanopia). Returns per-category scores (0-100) " +
2470
+ "and prioritized recommendations. Use after designing a component to validate accessibility before handoff. " +
2471
+ "Requires Desktop Bridge plugin.", {
2472
+ nodeId: z.string().optional().describe("Node ID of a COMPONENT_SET, COMPONENT, or INSTANCE to audit. Falls back to current selection if omitted."),
2473
+ targetSize: z.number().optional().describe("Minimum touch target size in px (default: 24 per WCAG 2.5.8). Use 44 for iOS or 48 for Android guidelines."),
2474
+ }, async ({ nodeId, targetSize }) => {
2475
+ try {
2476
+ const connector = await getDesktopConnector();
2477
+ const result = await connector.auditComponentAccessibility(nodeId, targetSize);
2478
+ if (!result.success) {
2479
+ throw new Error(result.error || "Audit failed");
2480
+ }
2481
+ return {
2482
+ content: [
2483
+ {
2484
+ type: "text",
2485
+ text: JSON.stringify(result.data || result, null, 2),
2486
+ },
2487
+ ],
2488
+ };
2489
+ }
2490
+ catch (error) {
2491
+ logger.error({ error }, "Failed to audit component accessibility");
2492
+ return {
2493
+ content: [
2494
+ {
2495
+ type: "text",
2496
+ text: JSON.stringify({
2497
+ error: error instanceof Error ? error.message : String(error),
2498
+ hint: "Make sure the Desktop Bridge plugin is running. Provide a COMPONENT_SET nodeId or select one in Figma.",
2499
+ }),
2500
+ },
2501
+ ],
2502
+ isError: true,
2503
+ };
2504
+ }
2505
+ });
2506
+ }