@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,3010 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Figma Console MCP Server
4
+ * Entry point for the MCP server that enables AI assistants to access
5
+ * Figma plugin console logs and screenshots.
6
+ *
7
+ * This implementation uses Cloudflare's McpAgent pattern for deployment
8
+ * on Cloudflare Workers with Browser Rendering API support.
9
+ */
10
+ import { McpAgent } from "agents/mcp";
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
13
+ import { z } from "zod";
14
+ import { BrowserManager } from "./browser-manager.js";
15
+ import { ConsoleMonitor } from "./core/console-monitor.js";
16
+ import { getConfig } from "./core/config.js";
17
+ import { createChildLogger } from "./core/logger.js";
18
+ import { FigmaAPI, extractFileKey } from "./core/figma-api.js";
19
+ import { registerFigmaAPITools } from "./core/figma-tools.js";
20
+ import { registerDesignCodeTools } from "./core/design-code-tools.js";
21
+ import { registerCommentTools } from "./core/comment-tools.js";
22
+ import { registerVersionTools } from "./core/version-tools.js";
23
+ import { registerAnnotationTools } from "./core/annotation-tools.js";
24
+ import { registerDeepComponentTools } from "./core/deep-component-tools.js";
25
+ import { registerDesignSystemTools } from "./core/design-system-tools.js";
26
+ import { registerLibraryTools, registerLibraryVariableTools } from "./core/library-tools.js";
27
+ import { registerAccessibilityTools } from "./core/accessibility-tools.js";
28
+ import { registerDiagnoseTool } from "./core/diagnose-tool.js";
29
+ import { wrapServerForIdentity } from "./core/identity.js";
30
+ import { generatePairingCode } from "./core/cloud-websocket-relay.js";
31
+ import { CloudWebSocketConnector } from "./core/cloud-websocket-connector.js";
32
+ import { registerWriteTools } from "./core/write-tools.js";
33
+ import { registerTokensTools } from "./core/tokens-tools.js";
34
+ import { registerFigJamTools } from "./core/figjam-tools.js";
35
+ import { registerSlidesTools } from "./core/slides-tools.js";
36
+ import { registerSlotTools } from "./core/slot-tools.js";
37
+ // Re-export PluginRelayDO so Cloudflare Workers can bind it as a Durable Object
38
+ export { PluginRelayDO } from "./core/cloud-websocket-relay.js";
39
+ // Note: MCP Apps (Token Browser, Dashboard) are only available in local mode
40
+ // They require Node.js file system APIs for serving HTML that don't work in Cloudflare Workers
41
+ const logger = createChildLogger({ component: "mcp-server" });
42
+ /**
43
+ * Validate a Figma Personal Access Token (PAT) by calling the Figma API.
44
+ * PATs start with 'figd_' and require the X-Figma-Token header (not Bearer).
45
+ * Returns the user info if valid, null if invalid/expired.
46
+ */
47
+ async function validateFigmaPAT(token) {
48
+ try {
49
+ const response = await fetch("https://api.figma.com/v1/me", {
50
+ headers: { "X-Figma-Token": token },
51
+ });
52
+ if (!response.ok)
53
+ return null;
54
+ const data = await response.json();
55
+ return data?.id ? data : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Check if a token is a Figma Personal Access Token.
63
+ * PATs start with 'figd_' — OAuth tokens start with 'figu_'.
64
+ */
65
+ function isFigmaPAT(token) {
66
+ return token.startsWith("figd_");
67
+ }
68
+ /**
69
+ * Figma Console MCP Agent
70
+ * Extends McpAgent to provide Figma-specific debugging tools
71
+ */
72
+ export class FigmaConsoleMCPv3 extends McpAgent {
73
+ constructor() {
74
+ super(...arguments);
75
+ this.server = (() => {
76
+ const s = new McpServer({
77
+ name: "Figma Console MCP",
78
+ version: "1.35.0",
79
+ });
80
+ // Identity wrap — every tool's response and thrown error gets stamped
81
+ // with our MCP name so cross-MCP attribution is unambiguous.
82
+ wrapServerForIdentity(s);
83
+ return s;
84
+ })();
85
+ this.browserManager = null;
86
+ this.consoleMonitor = null;
87
+ this.figmaAPI = null;
88
+ this.config = getConfig();
89
+ this.sessionId = null;
90
+ }
91
+ /**
92
+ * Refresh an expired OAuth token using the refresh token
93
+ */
94
+ async refreshOAuthToken(sessionId, refreshToken) {
95
+ const env = this.env;
96
+ if (!env.FIGMA_OAUTH_CLIENT_ID || !env.FIGMA_OAUTH_CLIENT_SECRET) {
97
+ throw new Error("OAuth not configured on server");
98
+ }
99
+ logger.info({ sessionId }, "Attempting to refresh OAuth token");
100
+ const credentials = btoa(`${env.FIGMA_OAUTH_CLIENT_ID}:${env.FIGMA_OAUTH_CLIENT_SECRET}`);
101
+ const tokenParams = new URLSearchParams({
102
+ grant_type: "refresh_token",
103
+ refresh_token: refreshToken
104
+ });
105
+ const tokenResponse = await fetch("https://api.figma.com/v1/oauth/token", {
106
+ method: "POST",
107
+ headers: {
108
+ "Content-Type": "application/x-www-form-urlencoded",
109
+ "Authorization": `Basic ${credentials}`
110
+ },
111
+ body: tokenParams.toString()
112
+ });
113
+ if (!tokenResponse.ok) {
114
+ const errorData = await tokenResponse.json().catch(() => ({}));
115
+ logger.error({ errorData, status: tokenResponse.status }, "Token refresh failed");
116
+ throw new Error(`Token refresh failed: ${JSON.stringify(errorData)}`);
117
+ }
118
+ const tokenData = await tokenResponse.json();
119
+ // Store refreshed token in KV
120
+ const tokenKey = `oauth_token:${sessionId}`;
121
+ const storedToken = {
122
+ accessToken: tokenData.access_token,
123
+ refreshToken: tokenData.refresh_token || refreshToken, // Use new refresh token or keep existing
124
+ expiresAt: Date.now() + (tokenData.expires_in * 1000)
125
+ };
126
+ await env.OAUTH_TOKENS.put(tokenKey, JSON.stringify(storedToken), {
127
+ expirationTtl: tokenData.expires_in
128
+ });
129
+ // Store reverse lookup for Bearer token validation on SSE endpoint
130
+ const bearerKey = `bearer_token:${tokenData.access_token}`;
131
+ await env.OAUTH_TOKENS.put(bearerKey, JSON.stringify({
132
+ sessionId,
133
+ expiresAt: storedToken.expiresAt
134
+ }), {
135
+ expirationTtl: tokenData.expires_in
136
+ });
137
+ logger.info({ sessionId }, "OAuth token refreshed successfully");
138
+ return storedToken;
139
+ }
140
+ /**
141
+ * Generate a cryptographically secure random state token for CSRF protection
142
+ */
143
+ static generateStateToken() {
144
+ const array = new Uint8Array(32);
145
+ crypto.getRandomValues(array);
146
+ return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
147
+ }
148
+ /**
149
+ * Load or create persistent session ID from Durable Object storage
150
+ * Uses a fixed session ID for the MCP server to ensure OAuth tokens persist across reconnections
151
+ */
152
+ async ensureSessionId() {
153
+ if (this.sessionId) {
154
+ return; // Already loaded
155
+ }
156
+ // IMPORTANT: Use a fixed session ID for all MCP connections
157
+ // This ensures OAuth tokens persist across MCP server reconnections
158
+ // Each user of this MCP server will share the same OAuth token
159
+ const FIXED_SESSION_ID = "figma-console-mcp-default-session";
160
+ // Try to load from Durable Object storage
161
+ // @ts-ignore - this.ctx is available in Durable Object context
162
+ const storage = this.ctx?.storage;
163
+ if (storage) {
164
+ try {
165
+ const storedSessionId = await storage.get('sessionId');
166
+ if (storedSessionId) {
167
+ this.sessionId = storedSessionId;
168
+ logger.info({ sessionId: this.sessionId }, "Loaded persistent session ID from storage");
169
+ return;
170
+ }
171
+ else {
172
+ // Store the fixed session ID
173
+ this.sessionId = FIXED_SESSION_ID;
174
+ await storage.put('sessionId', this.sessionId);
175
+ logger.info({ sessionId: this.sessionId }, "Initialized fixed session ID");
176
+ return;
177
+ }
178
+ }
179
+ catch (e) {
180
+ logger.warn({ error: e }, "Failed to access Durable Object storage for session ID");
181
+ }
182
+ }
183
+ // Fallback: use fixed session ID directly
184
+ this.sessionId = FIXED_SESSION_ID;
185
+ logger.info({ sessionId: this.sessionId }, "Using fixed session ID (storage unavailable)");
186
+ }
187
+ /**
188
+ * Get session ID for this Durable Object instance
189
+ * Returns the session ID loaded by ensureSessionId()
190
+ */
191
+ getSessionId() {
192
+ if (!this.sessionId) {
193
+ // This shouldn't happen if ensureSessionId() was called, but provide fallback
194
+ this.sessionId = FigmaConsoleMCPv3.generateStateToken();
195
+ logger.warn({ sessionId: this.sessionId }, "Session ID not initialized, generated ephemeral ID");
196
+ }
197
+ return this.sessionId;
198
+ }
199
+ /**
200
+ * Get or create Figma API client with OAuth token from session
201
+ */
202
+ async getFigmaAPI() {
203
+ // Ensure session ID is loaded from storage
204
+ await this.ensureSessionId();
205
+ // @ts-ignore - this.env is available in Agent/Durable Object context
206
+ const env = this.env;
207
+ // Try OAuth first (per-user authentication)
208
+ try {
209
+ const sessionId = this.getSessionId();
210
+ logger.info({ sessionId }, "Attempting to retrieve OAuth token from KV");
211
+ // Retrieve token from KV (accessible across all Durable Object instances)
212
+ const tokenKey = `oauth_token:${sessionId}`;
213
+ const tokenJson = await env.OAUTH_TOKENS.get(tokenKey);
214
+ if (!tokenJson) {
215
+ logger.warn({ sessionId, tokenKey }, "No OAuth token found in KV");
216
+ throw new Error("No token found");
217
+ }
218
+ let tokenData = JSON.parse(tokenJson);
219
+ logger.info({
220
+ sessionId,
221
+ hasToken: !!tokenData?.accessToken,
222
+ expiresAt: tokenData?.expiresAt,
223
+ isExpired: tokenData?.expiresAt ? Date.now() > tokenData.expiresAt : null
224
+ }, "Token retrieval result from KV");
225
+ if (tokenData?.accessToken) {
226
+ // Check if token is expired or will expire soon (within 5 minutes)
227
+ const isExpired = tokenData.expiresAt && Date.now() > tokenData.expiresAt;
228
+ const willExpireSoon = tokenData.expiresAt && Date.now() > (tokenData.expiresAt - 5 * 60 * 1000);
229
+ if (isExpired || willExpireSoon) {
230
+ if (tokenData.refreshToken) {
231
+ try {
232
+ // Attempt to refresh the token
233
+ tokenData = await this.refreshOAuthToken(sessionId, tokenData.refreshToken);
234
+ logger.info({ sessionId }, "Successfully refreshed expired/expiring token");
235
+ }
236
+ catch (refreshError) {
237
+ logger.error({ sessionId, refreshError }, "Failed to refresh token");
238
+ throw new Error("Token expired and refresh failed. Please re-authenticate.");
239
+ }
240
+ }
241
+ else {
242
+ logger.warn({ sessionId }, "Token expired but no refresh token available");
243
+ throw new Error("Token expired. Please re-authenticate.");
244
+ }
245
+ }
246
+ logger.info({ sessionId }, "Using OAuth token from KV for Figma API");
247
+ return new FigmaAPI({ accessToken: tokenData.accessToken });
248
+ }
249
+ logger.warn({ sessionId }, "OAuth token exists in KV but missing accessToken");
250
+ throw new Error("Invalid token data");
251
+ }
252
+ catch (error) {
253
+ const errorMessage = error instanceof Error ? error.message : String(error);
254
+ const sessionId = this.getSessionId();
255
+ // Check if this is a "no token found" error (user hasn't authenticated yet)
256
+ if (errorMessage.includes("No token found")) {
257
+ logger.info({ sessionId }, "No OAuth token found - user needs to authenticate");
258
+ // No authentication available - direct user to OAuth flow
259
+ const authUrl = `https://figma-console-mcp.southleft.com/oauth/authorize?session_id=${sessionId}`;
260
+ // Only use PAT fallback if explicitly configured AND no OAuth token exists
261
+ if (env?.FIGMA_ACCESS_TOKEN) {
262
+ logger.warn("FIGMA_ACCESS_TOKEN fallback is deprecated. User should authenticate via OAuth for proper per-user authentication.");
263
+ return new FigmaAPI({ accessToken: env.FIGMA_ACCESS_TOKEN });
264
+ }
265
+ throw new Error(JSON.stringify({
266
+ error: "authentication_required",
267
+ message: "Please authenticate with Figma to use API features",
268
+ auth_url: authUrl,
269
+ instructions: "Your browser will open automatically to complete authentication. If it doesn't, copy the auth_url and open it manually."
270
+ }));
271
+ }
272
+ // For other OAuth errors (expired token, refresh failed, etc.), do NOT fall back to PAT
273
+ logger.error({ error, sessionId }, "OAuth token retrieval failed - re-authentication required");
274
+ const authUrl = `https://figma-console-mcp.southleft.com/oauth/authorize?session_id=${sessionId}`;
275
+ throw new Error(JSON.stringify({
276
+ error: "oauth_error",
277
+ message: errorMessage,
278
+ auth_url: authUrl,
279
+ instructions: "Please re-authenticate with Figma. Your browser will open automatically."
280
+ }));
281
+ }
282
+ }
283
+ /**
284
+ * Initialize browser and console monitoring
285
+ */
286
+ async ensureInitialized() {
287
+ try {
288
+ // Ensure session ID is loaded from storage first
289
+ await this.ensureSessionId();
290
+ if (!this.browserManager) {
291
+ logger.info("Initializing BrowserManager");
292
+ // Access env from Durable Object context
293
+ // @ts-ignore - this.env is available in Agent/Durable Object context
294
+ const env = this.env;
295
+ if (!env) {
296
+ throw new Error("Environment not available - this.env is undefined");
297
+ }
298
+ if (!env.BROWSER) {
299
+ throw new Error("BROWSER binding not found in environment. Check wrangler.jsonc configuration.");
300
+ }
301
+ logger.info("Creating BrowserManager with BROWSER binding");
302
+ this.browserManager = new BrowserManager(env, this.config.browser);
303
+ }
304
+ if (!this.consoleMonitor) {
305
+ logger.info("Initializing ConsoleMonitor");
306
+ this.consoleMonitor = new ConsoleMonitor(this.config.console);
307
+ // Start browser and begin monitoring
308
+ logger.info("Getting browser page");
309
+ const page = await this.browserManager.getPage();
310
+ logger.info("Starting console monitoring");
311
+ await this.consoleMonitor.startMonitoring(page);
312
+ logger.info("Browser and console monitor initialized successfully");
313
+ }
314
+ }
315
+ catch (error) {
316
+ logger.error({ error }, "Failed to initialize browser/monitor");
317
+ throw new Error(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`);
318
+ }
319
+ }
320
+ async init() {
321
+ // Tool 1: Get Console Logs
322
+ this.server.tool("figma_get_console_logs", "Retrieve console logs from a Cloudflare Browser Rendering session opened with figma_navigate. Captures plugin console output ([Main], [Swapper], etc. prefixes) and page-level logs. NOTE: cloud mode does NOT currently capture logs from a paired Desktop Bridge plugin — for plugin sandbox logs, use Local mode. Call figma_navigate first to open the file and start browser monitoring.", {
323
+ count: z.number().optional().default(100).describe("Number of recent logs to retrieve"),
324
+ level: z
325
+ .enum(["log", "info", "warn", "error", "debug", "all"])
326
+ .optional()
327
+ .default("all")
328
+ .describe("Filter by log level"),
329
+ since: z
330
+ .number()
331
+ .optional()
332
+ .describe("Only logs after this timestamp (Unix ms)"),
333
+ }, async ({ count, level, since }) => {
334
+ try {
335
+ await this.ensureInitialized();
336
+ if (!this.consoleMonitor) {
337
+ throw new Error("Console monitor not initialized");
338
+ }
339
+ const logs = this.consoleMonitor.getLogs({
340
+ count,
341
+ level,
342
+ since,
343
+ });
344
+ // Add AI instruction when no logs are found
345
+ const responseData = {
346
+ logs,
347
+ totalCount: logs.length,
348
+ oldestTimestamp: logs[0]?.timestamp,
349
+ newestTimestamp: logs[logs.length - 1]?.timestamp,
350
+ status: this.consoleMonitor.getStatus(),
351
+ };
352
+ // If no logs found, add helpful AI instruction
353
+ if (logs.length === 0) {
354
+ responseData.ai_instruction = "No console logs found. This usually means the Figma plugin hasn't run since monitoring started. Please inform the user: 'No console logs found yet. Try running your Figma plugin now, then I'll check for logs again.' The MCP only captures logs AFTER monitoring starts - it cannot retrieve historical logs from before the browser connected.";
355
+ }
356
+ return {
357
+ content: [
358
+ {
359
+ type: "text",
360
+ text: JSON.stringify(responseData, null, 2),
361
+ },
362
+ ],
363
+ };
364
+ }
365
+ catch (error) {
366
+ logger.error({ error }, "Failed to get console logs");
367
+ const errorMessage = error instanceof Error ? error.message : String(error);
368
+ return {
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: JSON.stringify({
373
+ error: errorMessage,
374
+ message: "Failed to retrieve console logs. Make sure to call figma_navigate first to initialize the browser.",
375
+ hint: "Try: figma_navigate({ url: 'https://www.figma.com/design/your-file' })",
376
+ }, null, 2),
377
+ },
378
+ ],
379
+ isError: true,
380
+ };
381
+ }
382
+ });
383
+ // Tool 2: Take Screenshot (using Figma REST API)
384
+ // Note: For screenshots of specific components, use figma_get_component_image instead
385
+ this.server.tool("figma_take_screenshot", "Export an image of the currently viewed Figma page or specific node using Figma's REST API. Returns an image URL (valid for 30 days). For specific components, use figma_get_component_image instead.", {
386
+ nodeId: z
387
+ .string()
388
+ .optional()
389
+ .describe("Optional node ID to screenshot (e.g., '123:456'). If omitted, uses the node-id query param from the Cloudflare Browser Rendering page's current URL."),
390
+ scale: z
391
+ .number()
392
+ .min(0.01)
393
+ .max(4)
394
+ .optional()
395
+ .default(2)
396
+ .describe("Image scale factor (0.01-4, default: 2 for high quality)"),
397
+ format: z
398
+ .enum(["png", "jpg", "svg", "pdf"])
399
+ .optional()
400
+ .default("png")
401
+ .describe("Image format (default: png)"),
402
+ }, async ({ nodeId, scale, format }) => {
403
+ try {
404
+ const api = await this.getFigmaAPI();
405
+ // Get current URL to extract file key and node ID if not provided
406
+ const currentUrl = this.browserManager?.getCurrentUrl() || null;
407
+ if (!currentUrl) {
408
+ throw new Error("No Figma file open. Either provide a nodeId parameter or call figma_navigate first to open a Figma file.");
409
+ }
410
+ const fileKey = extractFileKey(currentUrl);
411
+ if (!fileKey) {
412
+ throw new Error(`Invalid Figma URL: ${currentUrl}`);
413
+ }
414
+ // Extract node ID from URL if not provided
415
+ let targetNodeId = nodeId;
416
+ if (!targetNodeId) {
417
+ const urlObj = new URL(currentUrl);
418
+ const nodeIdParam = urlObj.searchParams.get('node-id');
419
+ if (nodeIdParam) {
420
+ // Convert 123-456 to 123:456
421
+ targetNodeId = nodeIdParam.replace(/-/g, ':');
422
+ }
423
+ else {
424
+ throw new Error("No node ID found. Either provide nodeId parameter or ensure the Figma URL contains a node-id parameter (e.g., ?node-id=123-456)");
425
+ }
426
+ }
427
+ logger.info({ fileKey, nodeId: targetNodeId, scale, format }, "Rendering image via Figma API");
428
+ // Use Figma REST API to get image
429
+ const result = await api.getImages(fileKey, targetNodeId, {
430
+ scale,
431
+ format: format === 'jpg' ? 'jpg' : format, // normalize jpeg -> jpg
432
+ contents_only: true,
433
+ });
434
+ const imageUrl = result.images[targetNodeId];
435
+ if (!imageUrl) {
436
+ throw new Error(`Failed to render image for node ${targetNodeId}. The node may not exist or may not be renderable.`);
437
+ }
438
+ return {
439
+ content: [
440
+ {
441
+ type: "text",
442
+ text: JSON.stringify({
443
+ fileKey,
444
+ nodeId: targetNodeId,
445
+ imageUrl,
446
+ scale,
447
+ format,
448
+ expiresIn: "30 days",
449
+ note: "Image URL provided above. Use this URL to view or download the screenshot. URLs expire after 30 days.",
450
+ }, null, 2),
451
+ },
452
+ ],
453
+ };
454
+ }
455
+ catch (error) {
456
+ logger.error({ error }, "Failed to capture screenshot");
457
+ const errorMessage = error instanceof Error ? error.message : String(error);
458
+ return {
459
+ content: [
460
+ {
461
+ type: "text",
462
+ text: JSON.stringify({
463
+ error: errorMessage,
464
+ message: "Failed to capture screenshot via Figma API",
465
+ hint: "Make sure you've called figma_navigate to open a file, or provide a valid nodeId parameter",
466
+ }, null, 2),
467
+ },
468
+ ],
469
+ isError: true,
470
+ };
471
+ }
472
+ });
473
+ // Tool 3: Watch Console (Real-time streaming)
474
+ this.server.tool("figma_watch_console", {
475
+ duration: z
476
+ .number()
477
+ .optional()
478
+ .default(30)
479
+ .describe("How long to watch in seconds"),
480
+ level: z
481
+ .enum(["log", "info", "warn", "error", "debug", "all"])
482
+ .optional()
483
+ .default("all")
484
+ .describe("Filter by log level"),
485
+ }, async ({ duration, level }) => {
486
+ await this.ensureInitialized();
487
+ if (!this.consoleMonitor) {
488
+ throw new Error("Console monitor not initialized. Call figma_navigate first.");
489
+ }
490
+ const consoleMonitor = this.consoleMonitor;
491
+ if (!consoleMonitor.getStatus().isMonitoring) {
492
+ throw new Error("Console monitoring not active. Call figma_navigate first.");
493
+ }
494
+ const startTime = Date.now();
495
+ const endTime = startTime + duration * 1000;
496
+ const startLogCount = consoleMonitor.getStatus().logCount;
497
+ // Wait for the specified duration while collecting logs
498
+ await new Promise(resolve => setTimeout(resolve, duration * 1000));
499
+ // Get logs captured during watch period
500
+ const watchedLogs = consoleMonitor.getLogs({
501
+ level: level === 'all' ? undefined : level,
502
+ since: startTime,
503
+ });
504
+ const endLogCount = consoleMonitor.getStatus().logCount;
505
+ const newLogsCount = endLogCount - startLogCount;
506
+ return {
507
+ content: [
508
+ {
509
+ type: "text",
510
+ text: JSON.stringify({
511
+ status: "completed",
512
+ duration: `${duration} seconds`,
513
+ startTime: new Date(startTime).toISOString(),
514
+ endTime: new Date(endTime).toISOString(),
515
+ filter: level,
516
+ statistics: {
517
+ totalLogsInBuffer: endLogCount,
518
+ logsAddedDuringWatch: newLogsCount,
519
+ logsMatchingFilter: watchedLogs.length,
520
+ },
521
+ logs: watchedLogs,
522
+ }, null, 2),
523
+ },
524
+ ],
525
+ };
526
+ });
527
+ // Tool 4: Reload Plugin
528
+ this.server.tool("figma_reload_plugin", {
529
+ clearConsole: z
530
+ .boolean()
531
+ .optional()
532
+ .default(true)
533
+ .describe("Clear console logs before reload"),
534
+ }, async ({ clearConsole: clearConsoleBefore }) => {
535
+ try {
536
+ await this.ensureInitialized();
537
+ if (!this.browserManager) {
538
+ throw new Error("Browser manager not initialized");
539
+ }
540
+ // Clear console buffer if requested
541
+ let clearedCount = 0;
542
+ if (clearConsoleBefore && this.consoleMonitor) {
543
+ clearedCount = this.consoleMonitor.clear();
544
+ }
545
+ // Reload the page
546
+ await this.browserManager.reload();
547
+ const currentUrl = this.browserManager.getCurrentUrl();
548
+ return {
549
+ content: [
550
+ {
551
+ type: "text",
552
+ text: JSON.stringify({
553
+ status: "reloaded",
554
+ timestamp: Date.now(),
555
+ url: currentUrl,
556
+ consoleCleared: clearConsoleBefore,
557
+ clearedCount: clearConsoleBefore ? clearedCount : 0,
558
+ }, null, 2),
559
+ },
560
+ ],
561
+ };
562
+ }
563
+ catch (error) {
564
+ logger.error({ error }, "Failed to reload plugin");
565
+ return {
566
+ content: [
567
+ {
568
+ type: "text",
569
+ text: JSON.stringify({
570
+ error: String(error),
571
+ message: "Failed to reload plugin",
572
+ }, null, 2),
573
+ },
574
+ ],
575
+ isError: true,
576
+ };
577
+ }
578
+ });
579
+ // Tool 5: Clear Console
580
+ this.server.tool("figma_clear_console", {}, async () => {
581
+ try {
582
+ await this.ensureInitialized();
583
+ if (!this.consoleMonitor) {
584
+ throw new Error("Console monitor not initialized");
585
+ }
586
+ const clearedCount = this.consoleMonitor.clear();
587
+ return {
588
+ content: [
589
+ {
590
+ type: "text",
591
+ text: JSON.stringify({
592
+ status: "cleared",
593
+ clearedCount,
594
+ timestamp: Date.now(),
595
+ ai_instruction: "CRITICAL: Console cleared successfully, but this operation disrupts the monitoring connection. You MUST reconnect the MCP server using `/mcp reconnect figma-console` before calling figma_get_console_logs again. Best practice: Avoid clearing console - filter/parse logs instead to maintain monitoring connection.",
596
+ }, null, 2),
597
+ },
598
+ ],
599
+ };
600
+ }
601
+ catch (error) {
602
+ logger.error({ error }, "Failed to clear console");
603
+ return {
604
+ content: [
605
+ {
606
+ type: "text",
607
+ text: JSON.stringify({
608
+ error: String(error),
609
+ message: "Failed to clear console buffer",
610
+ }, null, 2),
611
+ },
612
+ ],
613
+ isError: true,
614
+ };
615
+ }
616
+ });
617
+ // Tool 6: Navigate to Figma
618
+ this.server.tool("figma_navigate", {
619
+ url: z
620
+ .string()
621
+ .url()
622
+ .describe("Figma URL to navigate to (e.g., https://www.figma.com/design/abc123)"),
623
+ }, async ({ url }) => {
624
+ try {
625
+ await this.ensureInitialized();
626
+ if (!this.browserManager) {
627
+ throw new Error("Browser manager not initialized");
628
+ }
629
+ // Navigate to the URL (may switch to existing tab in local mode)
630
+ const result = await this.browserManager.navigateToFigma(url);
631
+ if (result.action === 'switched_to_existing') {
632
+ // Switch console monitor to the page
633
+ if (this.consoleMonitor) {
634
+ this.consoleMonitor.stopMonitoring();
635
+ await this.consoleMonitor.startMonitoring(result.page);
636
+ }
637
+ const currentUrl = this.browserManager.getCurrentUrl();
638
+ return {
639
+ content: [
640
+ {
641
+ type: "text",
642
+ text: JSON.stringify({
643
+ status: "switched_to_existing",
644
+ url: currentUrl,
645
+ timestamp: Date.now(),
646
+ message: "Switched to existing tab for this Figma file. Console monitoring is active.",
647
+ }, null, 2),
648
+ },
649
+ ],
650
+ };
651
+ }
652
+ // Give page time to load and start capturing logs
653
+ await new Promise((resolve) => setTimeout(resolve, 2000));
654
+ const currentUrl = this.browserManager.getCurrentUrl();
655
+ return {
656
+ content: [
657
+ {
658
+ type: "text",
659
+ text: JSON.stringify({
660
+ status: "navigated",
661
+ url: currentUrl,
662
+ timestamp: Date.now(),
663
+ message: "Browser navigated to Figma. Console monitoring is active.",
664
+ }, null, 2),
665
+ },
666
+ ],
667
+ };
668
+ }
669
+ catch (error) {
670
+ logger.error({ error }, "Failed to navigate to Figma");
671
+ const errorMessage = error instanceof Error ? error.message : String(error);
672
+ return {
673
+ content: [
674
+ {
675
+ type: "text",
676
+ text: JSON.stringify({
677
+ error: errorMessage,
678
+ message: "Failed to navigate to Figma URL",
679
+ details: errorMessage.includes("BROWSER")
680
+ ? "Browser Rendering API binding is missing. This is a configuration issue."
681
+ : "Unable to launch browser or navigate to URL.",
682
+ troubleshooting: [
683
+ "Verify the Figma URL is valid and accessible",
684
+ "Check that the Browser Rendering API is properly configured in wrangler.jsonc",
685
+ "Try again in a few moments if this is a temporary issue"
686
+ ]
687
+ }, null, 2),
688
+ },
689
+ ],
690
+ isError: true,
691
+ };
692
+ }
693
+ });
694
+ // Tool 7: Get Status
695
+ this.server.tool("figma_get_status", {}, async () => {
696
+ try {
697
+ const browserRunning = this.browserManager?.isRunning() ?? false;
698
+ const monitorStatus = this.consoleMonitor?.getStatus() ?? null;
699
+ const currentUrl = this.browserManager?.getCurrentUrl() ?? null;
700
+ return {
701
+ content: [
702
+ {
703
+ type: "text",
704
+ text: JSON.stringify({
705
+ browser: {
706
+ running: browserRunning,
707
+ currentUrl,
708
+ },
709
+ consoleMonitor: monitorStatus,
710
+ initialized: this.browserManager !== null && this.consoleMonitor !== null,
711
+ timestamp: Date.now(),
712
+ }, null, 2),
713
+ },
714
+ ],
715
+ };
716
+ }
717
+ catch (error) {
718
+ logger.error({ error }, "Failed to get status");
719
+ return {
720
+ content: [
721
+ {
722
+ type: "text",
723
+ text: JSON.stringify({
724
+ error: String(error),
725
+ message: "Failed to retrieve status",
726
+ }, null, 2),
727
+ },
728
+ ],
729
+ isError: true,
730
+ };
731
+ }
732
+ });
733
+ // ================================================================
734
+ // Cloud Write Relay — Pairing Tool
735
+ // ================================================================
736
+ this.server.tool("figma_pair_plugin", "Pair the Figma Desktop Bridge plugin to this cloud session for write access. Returns a 6-character code the user enters in the plugin's Cloud Mode section.", {}, async () => {
737
+ try {
738
+ const env = this.env;
739
+ const code = generatePairingCode();
740
+ // Create a unique DO ID for this relay session
741
+ const relayDoId = env.PLUGIN_RELAY.newUniqueId().toString();
742
+ // Store pairing code → relay DO ID in KV (5-min TTL, one-time use)
743
+ await env.OAUTH_TOKENS.put(`pairing:${code}`, relayDoId, {
744
+ expirationTtl: 300,
745
+ });
746
+ // Store relay DO ID in this MCP DO's storage for session persistence
747
+ await this.ctx.storage.put('relayDoId', relayDoId);
748
+ return {
749
+ content: [{
750
+ type: "text",
751
+ text: JSON.stringify({
752
+ pairingCode: code,
753
+ expiresIn: "5 minutes",
754
+ instructions: [
755
+ "1. Open the Desktop Bridge plugin in Figma Desktop",
756
+ "2. Click the 'Cloud Mode' toggle in the plugin UI",
757
+ `3. Enter pairing code: ${code}`,
758
+ "4. Click 'Connect' — the plugin will connect to the cloud relay",
759
+ "5. Once paired, write tools (variables, components, nodes) work through the cloud"
760
+ ],
761
+ }, null, 2),
762
+ }],
763
+ };
764
+ }
765
+ catch (error) {
766
+ const errorMessage = error instanceof Error ? error.message : String(error);
767
+ return {
768
+ content: [{ type: "text", text: JSON.stringify({ error: errorMessage }) }],
769
+ isError: true,
770
+ };
771
+ }
772
+ });
773
+ // ================================================================
774
+ // Cloud Desktop Connector factory
775
+ // ================================================================
776
+ const getCloudDesktopConnector = async () => {
777
+ const env = this.env;
778
+ const relayDoId = await this.ctx.storage.get('relayDoId');
779
+ if (!relayDoId) {
780
+ throw new Error('No cloud relay session. Call figma_pair_plugin first to pair the Desktop Bridge plugin.');
781
+ }
782
+ const doId = env.PLUGIN_RELAY.idFromString(relayDoId);
783
+ const stub = env.PLUGIN_RELAY.get(doId);
784
+ const connector = new CloudWebSocketConnector(stub);
785
+ await connector.initialize();
786
+ return connector;
787
+ };
788
+ // Register all write/manipulation tools via shared function
789
+ registerWriteTools(this.server, getCloudDesktopConnector);
790
+ // Register token sync tools — figma_export_tokens and figma_import_tokens.
791
+ registerTokensTools(this.server, getCloudDesktopConnector, { isRemoteMode: true });
792
+ // Register FigJam-specific tools (sticky notes, connectors, tables, etc.)
793
+ registerFigJamTools(this.server, getCloudDesktopConnector);
794
+ // Register Annotation tools (read/write design annotations via Desktop Bridge)
795
+ registerAnnotationTools(this.server, getCloudDesktopConnector);
796
+ // Register Deep Component tools (Plugin API tree extraction for code generation)
797
+ registerDeepComponentTools(this.server, getCloudDesktopConnector);
798
+ // Register Figma Slides tools (slide management, transitions, content)
799
+ registerSlidesTools(this.server, getCloudDesktopConnector);
800
+ registerSlotTools(this.server, getCloudDesktopConnector);
801
+ // Register Figma API tools (Tools 8-14)
802
+ // Pass isRemoteMode: true to suppress Desktop Bridge mentions in tool descriptions
803
+ registerFigmaAPITools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, undefined, // variablesCache
804
+ { isRemoteMode: true }, getCloudDesktopConnector);
805
+ // Register Design-Code Parity & Documentation tools
806
+ registerDesignCodeTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, undefined, // variablesCache
807
+ { isRemoteMode: true }, getCloudDesktopConnector);
808
+ // Register Comment tools
809
+ registerCommentTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, { isRemoteMode: true });
810
+ // Register Version History tools
811
+ registerVersionTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, { isRemoteMode: true });
812
+ // Register Design System Kit tool
813
+ registerDesignSystemTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, undefined, // variablesCache
814
+ { isRemoteMode: true }, getCloudDesktopConnector);
815
+ // Register Library Tools (key-based component inspection across shared libraries)
816
+ registerLibraryTools(this.server, async () => await this.getFigmaAPI());
817
+ // Register Library Variable Tools (Plugin-API based — list + import variables
818
+ // from subscribed team libraries; routes through the cloud Desktop Bridge)
819
+ registerLibraryVariableTools(this.server, getCloudDesktopConnector);
820
+ // Register code-side accessibility scanning (axe-core + JSDOM)
821
+ // Note: May not work in Cloudflare Workers due to JSDOM dependency
822
+ try {
823
+ registerAccessibilityTools(this.server);
824
+ }
825
+ catch (e) {
826
+ // Silently skip if axe-core/jsdom not available in Workers environment
827
+ }
828
+ // Register figma_diagnose for cloud mode. Plugin state isn't directly
829
+ // observable from here (the paired plugin's WS lives in the relay DO),
830
+ // so we report mode and let the cross-MCP disclaimer do most of the work.
831
+ registerDiagnoseTool(this.server, {
832
+ mode: "cloud",
833
+ getServerVersion: () => "cloud",
834
+ getPluginState: () => null,
835
+ getTokenState: () => ({ hasToken: false }),
836
+ });
837
+ // Note: MCP Apps (Token Browser, Dashboard) are registered in local.ts only
838
+ // They require Node.js file system APIs that don't work in Cloudflare Workers
839
+ }
840
+ }
841
+ /**
842
+ * Cloudflare Workers fetch handler
843
+ * Routes requests to appropriate MCP endpoints
844
+ */
845
+ export default {
846
+ async fetch(request, env, ctx) {
847
+ const url = new URL(request.url);
848
+ // Use canonical origin for OAuth redirect URIs so they match the Figma OAuth app config
849
+ // regardless of whether the request comes via workers.dev or custom domain
850
+ const oauthOrigin = env.CANONICAL_ORIGIN || url.origin;
851
+ // Redirect /docs to subdomain
852
+ if (url.pathname === "/docs" || url.pathname.startsWith("/docs/")) {
853
+ const newPath = url.pathname.replace(/^\/docs\/?/, "/");
854
+ const redirectUrl = `https://docs.figma-console-mcp.southleft.com${newPath}${url.search}`;
855
+ return Response.redirect(redirectUrl, 301);
856
+ }
857
+ // ================================================================
858
+ // Cloud Write Relay — Plugin WebSocket pairing endpoint
859
+ // ================================================================
860
+ if (url.pathname === "/ws/pair") {
861
+ const code = url.searchParams.get("code");
862
+ if (!code) {
863
+ return new Response(JSON.stringify({ error: "Missing pairing code" }), {
864
+ status: 400,
865
+ headers: { "Content-Type": "application/json" },
866
+ });
867
+ }
868
+ // Look up pairing code in KV
869
+ const pairingKey = `pairing:${code.toUpperCase()}`;
870
+ const relayDoId = await env.OAUTH_TOKENS.get(pairingKey);
871
+ if (!relayDoId) {
872
+ return new Response(JSON.stringify({ error: "Invalid or expired pairing code" }), {
873
+ status: 404,
874
+ headers: { "Content-Type": "application/json" },
875
+ });
876
+ }
877
+ // Delete used code (one-time use)
878
+ await env.OAUTH_TOKENS.delete(pairingKey);
879
+ // Forward WebSocket upgrade to the relay DO
880
+ const doId = env.PLUGIN_RELAY.idFromString(relayDoId);
881
+ const stub = env.PLUGIN_RELAY.get(doId);
882
+ // Rewrite URL to the relay DO's /ws/connect path
883
+ const relayUrl = new URL(request.url);
884
+ relayUrl.pathname = "/ws/connect";
885
+ const relayRequest = new Request(relayUrl.toString(), request);
886
+ return stub.fetch(relayRequest);
887
+ }
888
+ // SSE endpoint for remote MCP clients
889
+ // Per MCP spec, we MUST validate Bearer tokens on every HTTP request
890
+ if (url.pathname === "/sse" || url.pathname === "/sse/message") {
891
+ // Validate Authorization header per MCP OAuth 2.1 spec
892
+ const authHeader = request.headers.get("Authorization");
893
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
894
+ logger.warn({ pathname: url.pathname }, "SSE request missing Authorization header - returning 401 with resource_metadata");
895
+ // MCP spec requires resource_metadata URL in WWW-Authenticate header (RFC9728)
896
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
897
+ return new Response(JSON.stringify({
898
+ error: "unauthorized",
899
+ error_description: "Authorization header with Bearer token is required"
900
+ }), {
901
+ status: 401,
902
+ headers: {
903
+ "Content-Type": "application/json",
904
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`
905
+ }
906
+ });
907
+ }
908
+ const bearerToken = authHeader.substring(7); // Remove "Bearer " prefix
909
+ // PAT support: Figma Personal Access Tokens (figd_*) are passed as Bearer
910
+ // tokens by MCP clients like Lovable, but they aren't stored in our OAuth KV.
911
+ // Validate them directly against Figma's API instead.
912
+ if (isFigmaPAT(bearerToken)) {
913
+ logger.info({ pathname: url.pathname }, "SSE request with Figma PAT — validating against Figma API");
914
+ const patUser = await validateFigmaPAT(bearerToken);
915
+ if (!patUser) {
916
+ logger.warn({ pathname: url.pathname }, "SSE request with invalid Figma PAT");
917
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
918
+ return new Response(JSON.stringify({
919
+ error: "invalid_token",
920
+ error_description: "Figma Personal Access Token is invalid or expired"
921
+ }), {
922
+ status: 401,
923
+ headers: {
924
+ "Content-Type": "application/json",
925
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token"`
926
+ }
927
+ });
928
+ }
929
+ // Store PAT in KV so the Durable Object's getFigmaAPI() can retrieve it
930
+ const patSessionId = "figma-console-mcp-default-session";
931
+ const patTokenKey = `oauth_token:${patSessionId}`;
932
+ await env.OAUTH_TOKENS.put(patTokenKey, JSON.stringify({
933
+ accessToken: bearerToken,
934
+ expiresAt: Date.now() + 3600_000, // 1-hour TTL for PAT session
935
+ }), { expirationTtl: 3600 });
936
+ logger.info({ pathname: url.pathname, user: patUser.handle }, "SSE request authenticated via Figma PAT");
937
+ // Proceed with SSE connection
938
+ return FigmaConsoleMCPv3.serveSSE("/sse").fetch(request, env, ctx);
939
+ }
940
+ // OAuth token path: look up in KV store
941
+ const bearerKey = `bearer_token:${bearerToken}`;
942
+ try {
943
+ const tokenDataJson = await env.OAUTH_TOKENS.get(bearerKey);
944
+ if (!tokenDataJson) {
945
+ logger.warn({ pathname: url.pathname }, "SSE request with invalid Bearer token");
946
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
947
+ return new Response(JSON.stringify({
948
+ error: "invalid_token",
949
+ error_description: "Bearer token is invalid or expired"
950
+ }), {
951
+ status: 401,
952
+ headers: {
953
+ "Content-Type": "application/json",
954
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token"`
955
+ }
956
+ });
957
+ }
958
+ const tokenData = JSON.parse(tokenDataJson);
959
+ // Check if token is expired
960
+ if (tokenData.expiresAt < Date.now()) {
961
+ logger.warn({ pathname: url.pathname, sessionId: tokenData.sessionId }, "SSE request with expired Bearer token");
962
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
963
+ return new Response(JSON.stringify({
964
+ error: "invalid_token",
965
+ error_description: "Bearer token has expired"
966
+ }), {
967
+ status: 401,
968
+ headers: {
969
+ "Content-Type": "application/json",
970
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token"`
971
+ }
972
+ });
973
+ }
974
+ logger.info({ pathname: url.pathname, sessionId: tokenData.sessionId }, "SSE request authenticated successfully");
975
+ }
976
+ catch (error) {
977
+ logger.error({ error, pathname: url.pathname }, "Error validating Bearer token");
978
+ return new Response(JSON.stringify({
979
+ error: "server_error",
980
+ error_description: "Failed to validate authorization"
981
+ }), {
982
+ status: 500,
983
+ headers: { "Content-Type": "application/json" }
984
+ });
985
+ }
986
+ // Token is valid, proceed with SSE connection
987
+ return FigmaConsoleMCPv3.serveSSE("/sse").fetch(request, env, ctx);
988
+ }
989
+ // Streamable HTTP endpoint for MCP communication (current spec)
990
+ // Supports POST (client→server) and optional GET (server→client SSE)
991
+ if (url.pathname === "/mcp") {
992
+ // Validate Authorization header per MCP OAuth 2.1 spec
993
+ const authHeader = request.headers.get("Authorization");
994
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
995
+ logger.warn({ pathname: url.pathname }, "MCP request missing Authorization header - returning 401 with resource_metadata");
996
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
997
+ return new Response(JSON.stringify({
998
+ error: "unauthorized",
999
+ error_description: "Authorization header with Bearer token is required"
1000
+ }), {
1001
+ status: 401,
1002
+ headers: {
1003
+ "Content-Type": "application/json",
1004
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`
1005
+ }
1006
+ });
1007
+ }
1008
+ const bearerToken = authHeader.substring(7);
1009
+ // PAT support: Figma Personal Access Tokens (figd_*) are passed as Bearer
1010
+ // tokens by MCP clients like Lovable, v0, and Replit. They bypass OAuth
1011
+ // and aren't stored in KV — validate directly against Figma's API.
1012
+ if (isFigmaPAT(bearerToken)) {
1013
+ logger.info({ pathname: url.pathname }, "MCP request with Figma PAT — validating against Figma API");
1014
+ const patUser = await validateFigmaPAT(bearerToken);
1015
+ if (!patUser) {
1016
+ logger.warn({ pathname: url.pathname }, "MCP request with invalid Figma PAT");
1017
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
1018
+ return new Response(JSON.stringify({
1019
+ error: "invalid_token",
1020
+ error_description: "Figma Personal Access Token is invalid or expired. Ensure your PAT (figd_...) is valid and has not been revoked."
1021
+ }), {
1022
+ status: 401,
1023
+ headers: {
1024
+ "Content-Type": "application/json",
1025
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token"`
1026
+ }
1027
+ });
1028
+ }
1029
+ logger.info({ pathname: url.pathname, user: patUser.handle }, "MCP request authenticated via Figma PAT");
1030
+ }
1031
+ else {
1032
+ // OAuth token path: look up in KV store
1033
+ const bearerKey = `bearer_token:${bearerToken}`;
1034
+ try {
1035
+ const tokenDataJson = await env.OAUTH_TOKENS.get(bearerKey);
1036
+ if (!tokenDataJson) {
1037
+ logger.warn({ pathname: url.pathname }, "MCP request with invalid Bearer token");
1038
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
1039
+ return new Response(JSON.stringify({
1040
+ error: "invalid_token",
1041
+ error_description: "Bearer token is invalid or expired"
1042
+ }), {
1043
+ status: 401,
1044
+ headers: {
1045
+ "Content-Type": "application/json",
1046
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token"`
1047
+ }
1048
+ });
1049
+ }
1050
+ const tokenData = JSON.parse(tokenDataJson);
1051
+ if (tokenData.expiresAt < Date.now()) {
1052
+ logger.warn({ pathname: url.pathname, sessionId: tokenData.sessionId }, "MCP request with expired Bearer token");
1053
+ const resourceMetadataUrl = `${url.origin}/.well-known/oauth-protected-resource`;
1054
+ return new Response(JSON.stringify({
1055
+ error: "invalid_token",
1056
+ error_description: "Bearer token has expired"
1057
+ }), {
1058
+ status: 401,
1059
+ headers: {
1060
+ "Content-Type": "application/json",
1061
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", error="invalid_token"`
1062
+ }
1063
+ });
1064
+ }
1065
+ logger.info({ pathname: url.pathname, sessionId: tokenData.sessionId }, "MCP request authenticated successfully");
1066
+ }
1067
+ catch (error) {
1068
+ logger.error({ error, pathname: url.pathname }, "Error validating Bearer token for MCP endpoint");
1069
+ return new Response(JSON.stringify({
1070
+ error: "server_error",
1071
+ error_description: "Failed to validate authorization"
1072
+ }), {
1073
+ status: 500,
1074
+ headers: { "Content-Type": "application/json" }
1075
+ });
1076
+ }
1077
+ }
1078
+ // Token is valid — use stateless transport (no Durable Objects)
1079
+ // The Bearer token IS the Figma access token, so we use it directly
1080
+ // FigmaAPI handles PAT vs OAuth header selection internally
1081
+ const figmaAccessToken = bearerToken;
1082
+ const statelessApi = new FigmaAPI({ accessToken: figmaAccessToken });
1083
+ const transport = new WebStandardStreamableHTTPServerTransport({
1084
+ sessionIdGenerator: undefined, // Stateless — no session persistence needed
1085
+ });
1086
+ const statelessServer = new McpServer({
1087
+ name: "Figma Console MCP",
1088
+ version: "1.35.0",
1089
+ });
1090
+ wrapServerForIdentity(statelessServer);
1091
+ // ================================================================
1092
+ // Cloud Write Relay — Pairing Tool (stateless /mcp path)
1093
+ // Uses KV keyed by bearer token instead of DO storage
1094
+ // ================================================================
1095
+ statelessServer.tool("figma_pair_plugin", "Pair the Figma Desktop Bridge plugin to this cloud session for write access. Returns a 6-character code the user enters in the plugin's Cloud Mode section.", {}, async () => {
1096
+ try {
1097
+ const code = generatePairingCode();
1098
+ const relayDoId = env.PLUGIN_RELAY.newUniqueId().toString();
1099
+ // Store pairing code → relay DO ID in KV (5-min TTL, one-time use)
1100
+ await env.OAUTH_TOKENS.put(`pairing:${code}`, relayDoId, {
1101
+ expirationTtl: 300,
1102
+ });
1103
+ // Store relay DO ID keyed by bearer token for session persistence
1104
+ await env.OAUTH_TOKENS.put(`relay:${bearerToken}`, relayDoId, {
1105
+ expirationTtl: 86400, // 24h — matches typical session length
1106
+ });
1107
+ return {
1108
+ content: [{
1109
+ type: "text",
1110
+ text: JSON.stringify({
1111
+ pairingCode: code,
1112
+ expiresIn: "5 minutes",
1113
+ instructions: [
1114
+ "1. Open the MCP Bridge plugin in Figma Desktop",
1115
+ "2. Click the '▶ Cloud Mode' toggle in the plugin UI",
1116
+ `3. Enter pairing code: ${code}`,
1117
+ "4. Click 'Connect' — the plugin will connect to the cloud relay",
1118
+ "5. Once paired, write tools (variables, components, nodes) work through the cloud"
1119
+ ],
1120
+ }, null, 2),
1121
+ }],
1122
+ };
1123
+ }
1124
+ catch (error) {
1125
+ const errorMessage = error instanceof Error ? error.message : String(error);
1126
+ return {
1127
+ content: [{ type: "text", text: JSON.stringify({ error: errorMessage }) }],
1128
+ isError: true,
1129
+ };
1130
+ }
1131
+ });
1132
+ // Cloud Desktop Connector factory (stateless /mcp path)
1133
+ const getCloudDesktopConnector = async () => {
1134
+ const relayDoId = await env.OAUTH_TOKENS.get(`relay:${bearerToken}`);
1135
+ if (!relayDoId) {
1136
+ throw new Error('No cloud relay session. Call figma_pair_plugin first to pair the Desktop Bridge plugin.');
1137
+ }
1138
+ const doId = env.PLUGIN_RELAY.idFromString(relayDoId);
1139
+ const stub = env.PLUGIN_RELAY.get(doId);
1140
+ const connector = new CloudWebSocketConnector(stub);
1141
+ await connector.initialize();
1142
+ return connector;
1143
+ };
1144
+ // Build a getCurrentUrl that resolves from the relay DO's file info
1145
+ const getCloudFileUrl = () => {
1146
+ // This is synchronous — we cache the file URL after first relay status check
1147
+ return cloudFileUrlCache;
1148
+ };
1149
+ let cloudFileUrlCache = null;
1150
+ // Pre-fetch file info from relay if paired
1151
+ try {
1152
+ const relayDoId = await env.OAUTH_TOKENS.get(`relay:${bearerToken}`);
1153
+ if (relayDoId) {
1154
+ const doId = env.PLUGIN_RELAY.idFromString(relayDoId);
1155
+ const stub = env.PLUGIN_RELAY.get(doId);
1156
+ const statusRes = await stub.fetch('https://relay/relay/status');
1157
+ const status = await statusRes.json();
1158
+ if (status.connected && status.fileInfo?.fileKey) {
1159
+ cloudFileUrlCache = `https://www.figma.com/design/${status.fileInfo.fileKey}`;
1160
+ }
1161
+ }
1162
+ }
1163
+ catch {
1164
+ // No relay session or not paired — cloudFileUrlCache stays null
1165
+ }
1166
+ // Register all write/manipulation tools via shared function
1167
+ registerWriteTools(statelessServer, getCloudDesktopConnector);
1168
+ registerTokensTools(statelessServer, getCloudDesktopConnector, { isRemoteMode: true });
1169
+ // Register FigJam-specific tools
1170
+ registerFigJamTools(statelessServer, getCloudDesktopConnector);
1171
+ // Register Annotation tools
1172
+ registerAnnotationTools(statelessServer, getCloudDesktopConnector);
1173
+ // Register Deep Component tools
1174
+ registerDeepComponentTools(statelessServer, getCloudDesktopConnector);
1175
+ // Register Figma Slides tools
1176
+ registerSlidesTools(statelessServer, getCloudDesktopConnector);
1177
+ registerSlotTools(statelessServer, getCloudDesktopConnector);
1178
+ // Register REST API tools with the authenticated Figma API
1179
+ registerFigmaAPITools(statelessServer, async () => statelessApi, getCloudFileUrl, new Map(), // Fresh variables cache per request
1180
+ { isRemoteMode: true }, getCloudDesktopConnector);
1181
+ registerDesignCodeTools(statelessServer, async () => statelessApi, getCloudFileUrl, new Map(), // Fresh variables cache per request
1182
+ { isRemoteMode: true }, getCloudDesktopConnector);
1183
+ registerCommentTools(statelessServer, async () => statelessApi, getCloudFileUrl);
1184
+ registerVersionTools(statelessServer, async () => statelessApi, getCloudFileUrl);
1185
+ registerDesignSystemTools(statelessServer, async () => statelessApi, getCloudFileUrl, new Map(), // Fresh variables cache per request
1186
+ { isRemoteMode: true }, getCloudDesktopConnector);
1187
+ registerLibraryTools(statelessServer, async () => statelessApi);
1188
+ registerLibraryVariableTools(statelessServer, getCloudDesktopConnector);
1189
+ await statelessServer.connect(transport);
1190
+ const response = await transport.handleRequest(request);
1191
+ if (response) {
1192
+ return response;
1193
+ }
1194
+ return new Response("No response from MCP transport", { status: 500 });
1195
+ }
1196
+ // ============================================================
1197
+ // MCP OAuth 2.1 Spec-Compliant Endpoints
1198
+ // These endpoints follow the MCP Authorization specification
1199
+ // for compatibility with mcp-remote and Claude Code
1200
+ // ============================================================
1201
+ // Protected Resource Metadata (RFC9728)
1202
+ // Required by MCP spec for OAuth discovery - tells clients where to find authorization server
1203
+ if (url.pathname === "/.well-known/oauth-protected-resource" ||
1204
+ url.pathname.startsWith("/.well-known/oauth-protected-resource/")) {
1205
+ const metadata = {
1206
+ resource: url.origin,
1207
+ authorization_servers: [`${url.origin}/`],
1208
+ scopes_supported: ["file_content:read", "file_versions:read", "file_variables:read", "file_comments:read", "file_comments:write", "library_content:read"],
1209
+ bearer_methods_supported: ["header"],
1210
+ resource_signing_alg_values_supported: ["RS256"]
1211
+ };
1212
+ return new Response(JSON.stringify(metadata, null, 2), {
1213
+ headers: {
1214
+ "Content-Type": "application/json",
1215
+ "Cache-Control": "public, max-age=3600"
1216
+ }
1217
+ });
1218
+ }
1219
+ // OAuth 2.0 Authorization Server Metadata (RFC8414)
1220
+ // Required by MCP spec for client discovery
1221
+ if (url.pathname === "/.well-known/oauth-authorization-server") {
1222
+ const metadata = {
1223
+ issuer: url.origin,
1224
+ authorization_endpoint: `${url.origin}/authorize`,
1225
+ token_endpoint: `${url.origin}/token`,
1226
+ registration_endpoint: `${url.origin}/oauth/register`,
1227
+ scopes_supported: ["file_content:read", "file_versions:read", "file_variables:read", "file_comments:read", "file_comments:write", "library_content:read"],
1228
+ response_types_supported: ["code"],
1229
+ grant_types_supported: ["authorization_code", "refresh_token"],
1230
+ token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
1231
+ code_challenge_methods_supported: ["S256"],
1232
+ service_documentation: "https://docs.figma-console-mcp.southleft.com",
1233
+ };
1234
+ return new Response(JSON.stringify(metadata, null, 2), {
1235
+ headers: {
1236
+ "Content-Type": "application/json",
1237
+ "Cache-Control": "public, max-age=3600"
1238
+ }
1239
+ });
1240
+ }
1241
+ // MCP-compliant /authorize endpoint
1242
+ // Handles authorization requests from MCP clients
1243
+ if (url.pathname === "/authorize") {
1244
+ const clientId = url.searchParams.get("client_id");
1245
+ const redirectUri = url.searchParams.get("redirect_uri");
1246
+ const state = url.searchParams.get("state");
1247
+ const codeChallenge = url.searchParams.get("code_challenge");
1248
+ const codeChallengeMethod = url.searchParams.get("code_challenge_method");
1249
+ const scope = url.searchParams.get("scope");
1250
+ // For MCP clients, use the client_id as the session identifier
1251
+ // This allows token retrieval after the OAuth flow completes
1252
+ const sessionId = clientId || FigmaConsoleMCPv3.generateStateToken();
1253
+ // Store the MCP client's redirect_uri and state for the callback
1254
+ if (redirectUri && state) {
1255
+ const mcpAuthData = {
1256
+ redirectUri,
1257
+ state,
1258
+ codeChallenge,
1259
+ codeChallengeMethod,
1260
+ scope,
1261
+ clientId,
1262
+ sessionId
1263
+ };
1264
+ // Store with 10 minute expiration
1265
+ const mcpStateKey = `mcp_auth:${sessionId}`;
1266
+ await env.OAUTH_STATE.put(mcpStateKey, JSON.stringify(mcpAuthData), {
1267
+ expirationTtl: 600
1268
+ });
1269
+ }
1270
+ // Check if OAuth credentials are configured
1271
+ if (!env.FIGMA_OAUTH_CLIENT_ID) {
1272
+ return new Response(JSON.stringify({
1273
+ error: "server_error",
1274
+ error_description: "OAuth not configured on server"
1275
+ }), {
1276
+ status: 500,
1277
+ headers: { "Content-Type": "application/json" }
1278
+ });
1279
+ }
1280
+ // Generate CSRF protection token
1281
+ const stateToken = FigmaConsoleMCPv3.generateStateToken();
1282
+ // Store state token with sessionId (10 minute expiration)
1283
+ await env.OAUTH_STATE.put(stateToken, sessionId, {
1284
+ expirationTtl: 600
1285
+ });
1286
+ // Redirect to Figma OAuth
1287
+ const figmaAuthUrl = new URL("https://www.figma.com/oauth");
1288
+ figmaAuthUrl.searchParams.set("client_id", env.FIGMA_OAUTH_CLIENT_ID);
1289
+ figmaAuthUrl.searchParams.set("redirect_uri", `${oauthOrigin}/oauth/callback`);
1290
+ figmaAuthUrl.searchParams.set("scope", "file_content:read,file_versions:read,file_variables:read,file_comments:read,file_comments:write,library_content:read");
1291
+ figmaAuthUrl.searchParams.set("state", stateToken);
1292
+ figmaAuthUrl.searchParams.set("response_type", "code");
1293
+ return Response.redirect(figmaAuthUrl.toString(), 302);
1294
+ }
1295
+ // MCP-compliant /token endpoint
1296
+ // Handles token exchange and refresh requests
1297
+ if (url.pathname === "/token" && request.method === "POST") {
1298
+ const contentType = request.headers.get("content-type") || "";
1299
+ let params;
1300
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1301
+ params = new URLSearchParams(await request.text());
1302
+ }
1303
+ else if (contentType.includes("application/json")) {
1304
+ const body = await request.json();
1305
+ params = new URLSearchParams(body);
1306
+ }
1307
+ else {
1308
+ params = new URLSearchParams(await request.text());
1309
+ }
1310
+ const grantType = params.get("grant_type");
1311
+ const clientId = params.get("client_id");
1312
+ const code = params.get("code");
1313
+ const refreshToken = params.get("refresh_token");
1314
+ // For authorization_code grant, exchange the code for tokens
1315
+ if (grantType === "authorization_code" && code) {
1316
+ // The code here is actually our session-based token
1317
+ // Look up the stored token by session/client ID
1318
+ const sessionId = clientId || code;
1319
+ const tokenKey = `oauth_token:${sessionId}`;
1320
+ logger.info({ grantType, clientId, code, sessionId, tokenKey }, "Token exchange request");
1321
+ const tokenJson = await env.OAUTH_TOKENS.get(tokenKey);
1322
+ logger.info({ tokenKey, hasToken: !!tokenJson }, "Token lookup result");
1323
+ if (tokenJson) {
1324
+ const tokenData = JSON.parse(tokenJson);
1325
+ // Return tokens in OAuth 2.0 format
1326
+ return new Response(JSON.stringify({
1327
+ access_token: tokenData.accessToken,
1328
+ token_type: "Bearer",
1329
+ expires_in: Math.max(0, Math.floor((tokenData.expiresAt - Date.now()) / 1000)),
1330
+ refresh_token: tokenData.refreshToken,
1331
+ scope: "file_content:read file_versions:read file_variables:read file_comments:read file_comments:write library_content:read"
1332
+ }), {
1333
+ headers: {
1334
+ "Content-Type": "application/json",
1335
+ "Cache-Control": "no-store"
1336
+ }
1337
+ });
1338
+ }
1339
+ logger.error({ tokenKey, sessionId, clientId, code }, "Token not found for exchange");
1340
+ return new Response(JSON.stringify({
1341
+ error: "invalid_grant",
1342
+ error_description: "Authorization code not found or expired. Please re-authenticate."
1343
+ }), {
1344
+ status: 400,
1345
+ headers: { "Content-Type": "application/json" }
1346
+ });
1347
+ }
1348
+ // For refresh_token grant
1349
+ if (grantType === "refresh_token" && refreshToken) {
1350
+ if (!env.FIGMA_OAUTH_CLIENT_ID || !env.FIGMA_OAUTH_CLIENT_SECRET) {
1351
+ return new Response(JSON.stringify({
1352
+ error: "server_error",
1353
+ error_description: "OAuth not configured"
1354
+ }), {
1355
+ status: 500,
1356
+ headers: { "Content-Type": "application/json" }
1357
+ });
1358
+ }
1359
+ const credentials = btoa(`${env.FIGMA_OAUTH_CLIENT_ID}:${env.FIGMA_OAUTH_CLIENT_SECRET}`);
1360
+ const tokenParams = new URLSearchParams({
1361
+ grant_type: "refresh_token",
1362
+ refresh_token: refreshToken
1363
+ });
1364
+ const tokenResponse = await fetch("https://api.figma.com/v1/oauth/token", {
1365
+ method: "POST",
1366
+ headers: {
1367
+ "Content-Type": "application/x-www-form-urlencoded",
1368
+ "Authorization": `Basic ${credentials}`
1369
+ },
1370
+ body: tokenParams.toString()
1371
+ });
1372
+ if (!tokenResponse.ok) {
1373
+ return new Response(JSON.stringify({
1374
+ error: "invalid_grant",
1375
+ error_description: "Failed to refresh token"
1376
+ }), {
1377
+ status: 400,
1378
+ headers: { "Content-Type": "application/json" }
1379
+ });
1380
+ }
1381
+ const tokenData = await tokenResponse.json();
1382
+ // Store the refreshed token
1383
+ if (clientId) {
1384
+ const tokenKey = `oauth_token:${clientId}`;
1385
+ const expiresAt = Date.now() + (tokenData.expires_in * 1000);
1386
+ const storedToken = {
1387
+ accessToken: tokenData.access_token,
1388
+ refreshToken: tokenData.refresh_token || refreshToken,
1389
+ expiresAt
1390
+ };
1391
+ await env.OAUTH_TOKENS.put(tokenKey, JSON.stringify(storedToken), {
1392
+ expirationTtl: tokenData.expires_in
1393
+ });
1394
+ // Store reverse lookup for Bearer token validation on SSE endpoint
1395
+ const bearerKey = `bearer_token:${tokenData.access_token}`;
1396
+ await env.OAUTH_TOKENS.put(bearerKey, JSON.stringify({
1397
+ sessionId: clientId,
1398
+ expiresAt
1399
+ }), {
1400
+ expirationTtl: tokenData.expires_in
1401
+ });
1402
+ }
1403
+ return new Response(JSON.stringify({
1404
+ access_token: tokenData.access_token,
1405
+ token_type: "Bearer",
1406
+ expires_in: tokenData.expires_in,
1407
+ refresh_token: tokenData.refresh_token || refreshToken,
1408
+ scope: "file_content:read file_versions:read file_variables:read file_comments:read file_comments:write library_content:read"
1409
+ }), {
1410
+ headers: {
1411
+ "Content-Type": "application/json",
1412
+ "Cache-Control": "no-store"
1413
+ }
1414
+ });
1415
+ }
1416
+ return new Response(JSON.stringify({
1417
+ error: "unsupported_grant_type",
1418
+ error_description: "Only authorization_code and refresh_token grants are supported"
1419
+ }), {
1420
+ status: 400,
1421
+ headers: { "Content-Type": "application/json" }
1422
+ });
1423
+ }
1424
+ // Dynamic Client Registration (RFC7591)
1425
+ // Required by MCP spec for clients to register
1426
+ if (url.pathname === "/oauth/register" && request.method === "POST") {
1427
+ const body = await request.json();
1428
+ // Generate a client ID for this registration
1429
+ const clientId = `mcp_${FigmaConsoleMCPv3.generateStateToken().substring(0, 16)}`;
1430
+ // Store client registration (30 day expiration)
1431
+ await env.OAUTH_STATE.put(`client:${clientId}`, JSON.stringify({
1432
+ client_name: body.client_name || "MCP Client",
1433
+ redirect_uris: body.redirect_uris || [],
1434
+ created_at: Date.now()
1435
+ }), {
1436
+ expirationTtl: 30 * 24 * 60 * 60
1437
+ });
1438
+ return new Response(JSON.stringify({
1439
+ client_id: clientId,
1440
+ client_name: body.client_name || "MCP Client",
1441
+ redirect_uris: body.redirect_uris || [],
1442
+ token_endpoint_auth_method: "none",
1443
+ grant_types: ["authorization_code", "refresh_token"],
1444
+ response_types: ["code"]
1445
+ }), {
1446
+ status: 201,
1447
+ headers: { "Content-Type": "application/json" }
1448
+ });
1449
+ }
1450
+ // ============================================================
1451
+ // Original Figma OAuth Endpoints (kept for backwards compatibility)
1452
+ // ============================================================
1453
+ // OAuth authorization initiation
1454
+ if (url.pathname === "/oauth/authorize") {
1455
+ const sessionId = url.searchParams.get("session_id");
1456
+ if (!sessionId) {
1457
+ return new Response("Missing session_id parameter", { status: 400 });
1458
+ }
1459
+ // Check if OAuth credentials are configured
1460
+ if (!env.FIGMA_OAUTH_CLIENT_ID) {
1461
+ return new Response(JSON.stringify({
1462
+ error: "OAuth not configured",
1463
+ message: "Server administrator needs to configure FIGMA_OAUTH_CLIENT_ID",
1464
+ docs: "https://github.com/southleft/figma-console-mcp#oauth-setup"
1465
+ }), {
1466
+ status: 500,
1467
+ headers: { "Content-Type": "application/json" }
1468
+ });
1469
+ }
1470
+ // Generate cryptographically secure state token for CSRF protection
1471
+ const stateToken = FigmaConsoleMCPv3.generateStateToken();
1472
+ // Store state token with sessionId in KV (10 minute expiration)
1473
+ await env.OAUTH_STATE.put(stateToken, sessionId, {
1474
+ expirationTtl: 600 // 10 minutes
1475
+ });
1476
+ const redirectUri = `${oauthOrigin}/oauth/callback`;
1477
+ const figmaAuthUrl = new URL("https://www.figma.com/oauth");
1478
+ figmaAuthUrl.searchParams.set("client_id", env.FIGMA_OAUTH_CLIENT_ID);
1479
+ figmaAuthUrl.searchParams.set("redirect_uri", redirectUri);
1480
+ figmaAuthUrl.searchParams.set("scope", "file_content:read,file_versions:read,file_variables:read,file_comments:read,file_comments:write,library_content:read");
1481
+ figmaAuthUrl.searchParams.set("state", stateToken);
1482
+ figmaAuthUrl.searchParams.set("response_type", "code");
1483
+ return Response.redirect(figmaAuthUrl.toString(), 302);
1484
+ }
1485
+ // OAuth callback handler
1486
+ if (url.pathname === "/oauth/callback") {
1487
+ const code = url.searchParams.get("code");
1488
+ const stateToken = url.searchParams.get("state");
1489
+ const error = url.searchParams.get("error");
1490
+ // Handle OAuth errors
1491
+ if (error) {
1492
+ return new Response(`<html><body>
1493
+ <h1>Authentication Failed</h1>
1494
+ <p>Error: ${error}</p>
1495
+ <p>Description: ${url.searchParams.get("error_description") || "Unknown error"}</p>
1496
+ <p>You can close this window and try again.</p>
1497
+ </body></html>`, {
1498
+ status: 400,
1499
+ headers: { "Content-Type": "text/html" }
1500
+ });
1501
+ }
1502
+ if (!code || !stateToken) {
1503
+ return new Response("Missing code or state parameter", { status: 400 });
1504
+ }
1505
+ // Validate state token (CSRF protection)
1506
+ const sessionId = await env.OAUTH_STATE.get(stateToken);
1507
+ logger.info({ stateToken, sessionId, hasSessionId: !!sessionId }, "OAuth callback - state token lookup");
1508
+ if (!sessionId) {
1509
+ return new Response(`<html><body>
1510
+ <h1>Invalid or Expired Request</h1>
1511
+ <p>The authentication request has expired or is invalid.</p>
1512
+ <p>Please try authenticating again.</p>
1513
+ </body></html>`, {
1514
+ status: 400,
1515
+ headers: { "Content-Type": "text/html" }
1516
+ });
1517
+ }
1518
+ // Delete state token after validation (one-time use)
1519
+ await env.OAUTH_STATE.delete(stateToken);
1520
+ try {
1521
+ // Exchange authorization code for access token
1522
+ // Use Basic auth in Authorization header (Figma's recommended method)
1523
+ const credentials = btoa(`${env.FIGMA_OAUTH_CLIENT_ID}:${env.FIGMA_OAUTH_CLIENT_SECRET}`);
1524
+ const tokenParams = new URLSearchParams({
1525
+ redirect_uri: `${oauthOrigin}/oauth/callback`,
1526
+ code,
1527
+ grant_type: "authorization_code"
1528
+ });
1529
+ const tokenResponse = await fetch("https://api.figma.com/v1/oauth/token", {
1530
+ method: "POST",
1531
+ headers: {
1532
+ "Content-Type": "application/x-www-form-urlencoded",
1533
+ "Authorization": `Basic ${credentials}`
1534
+ },
1535
+ body: tokenParams.toString()
1536
+ });
1537
+ if (!tokenResponse.ok) {
1538
+ const errorText = await tokenResponse.text();
1539
+ let errorData;
1540
+ try {
1541
+ errorData = JSON.parse(errorText);
1542
+ }
1543
+ catch {
1544
+ errorData = { error: "Unknown error", raw: errorText, status: tokenResponse.status };
1545
+ }
1546
+ logger.error({ errorData, status: tokenResponse.status }, "Token exchange failed");
1547
+ throw new Error(`Token exchange failed: ${JSON.stringify(errorData)}`);
1548
+ }
1549
+ const tokenData = await tokenResponse.json();
1550
+ const accessToken = tokenData.access_token;
1551
+ const refreshToken = tokenData.refresh_token;
1552
+ const expiresIn = tokenData.expires_in;
1553
+ logger.info({
1554
+ sessionId,
1555
+ hasTokens: !!accessToken && !!refreshToken,
1556
+ expiresIn
1557
+ }, "Token exchange successful");
1558
+ // IMPORTANT: Use KV storage for tokens since Durable Object storage is instance-specific
1559
+ // Store token in Workers KV so it's accessible across all Durable Object instances
1560
+ const tokenKey = `oauth_token:${sessionId}`;
1561
+ const tokenExpiresAt = Date.now() + (expiresIn * 1000);
1562
+ const storedToken = {
1563
+ accessToken,
1564
+ refreshToken,
1565
+ expiresAt: tokenExpiresAt
1566
+ };
1567
+ // Store in KV with 90-day expiration (matching token lifetime)
1568
+ await env.OAUTH_TOKENS.put(tokenKey, JSON.stringify(storedToken), {
1569
+ expirationTtl: expiresIn
1570
+ });
1571
+ // CRITICAL: Also store under the fixed session ID that Durable Objects use
1572
+ // This ensures getFigmaAPI() can retrieve the token regardless of which
1573
+ // session ID was used during OAuth (e.g., mcp-remote's client_id)
1574
+ const fixedTokenKey = `oauth_token:figma-console-mcp-default-session`;
1575
+ if (tokenKey !== fixedTokenKey) {
1576
+ await env.OAUTH_TOKENS.put(fixedTokenKey, JSON.stringify(storedToken), {
1577
+ expirationTtl: expiresIn
1578
+ });
1579
+ logger.info({ fixedTokenKey }, "Token also stored under fixed session ID for Durable Object access");
1580
+ }
1581
+ // Store reverse lookup for Bearer token validation on SSE endpoint
1582
+ // This allows us to validate Authorization: Bearer <token> headers
1583
+ const bearerKey = `bearer_token:${accessToken}`;
1584
+ await env.OAUTH_TOKENS.put(bearerKey, JSON.stringify({
1585
+ sessionId,
1586
+ expiresAt: tokenExpiresAt
1587
+ }), {
1588
+ expirationTtl: expiresIn
1589
+ });
1590
+ // Verify the token was stored
1591
+ const verifyToken = await env.OAUTH_TOKENS.get(tokenKey);
1592
+ logger.info({ sessionId, tokenKey, storedSuccessfully: !!verifyToken }, "Token stored in KV");
1593
+ // Check if this flow came from an MCP client (like mcp-remote)
1594
+ // If so, we need to redirect back to the client with an authorization code
1595
+ const mcpStateKey = `mcp_auth:${sessionId}`;
1596
+ const mcpAuthJson = await env.OAUTH_STATE.get(mcpStateKey);
1597
+ if (mcpAuthJson) {
1598
+ // MCP client flow - redirect back with authorization code
1599
+ const mcpAuthData = JSON.parse(mcpAuthJson);
1600
+ // Clean up the MCP auth state
1601
+ await env.OAUTH_STATE.delete(mcpStateKey);
1602
+ // Generate an authorization code for the MCP client
1603
+ // We use the sessionId as the code since we've already stored the token
1604
+ const authCode = sessionId;
1605
+ // Build the redirect URL back to the MCP client
1606
+ const redirectUrl = new URL(mcpAuthData.redirectUri);
1607
+ redirectUrl.searchParams.set("code", authCode);
1608
+ redirectUrl.searchParams.set("state", mcpAuthData.state);
1609
+ logger.info({
1610
+ sessionId,
1611
+ redirectUri: mcpAuthData.redirectUri,
1612
+ state: mcpAuthData.state
1613
+ }, "Redirecting back to MCP client");
1614
+ return Response.redirect(redirectUrl.toString(), 302);
1615
+ }
1616
+ // Direct browser flow - show success page
1617
+ return new Response(`<!DOCTYPE html>
1618
+ <html>
1619
+ <head>
1620
+ <meta charset="UTF-8">
1621
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1622
+ <title>Authentication Successful</title>
1623
+ <link rel="icon" type="image/jpeg" href="https://p198.p4.n0.cdn.zight.com/items/Qwu1Dywx/b61b7b8f-05dc-4063-8a40-53fa4f8e3e97.jpg">
1624
+ <style>
1625
+ * {
1626
+ margin: 0;
1627
+ padding: 0;
1628
+ box-sizing: border-box;
1629
+ }
1630
+ body {
1631
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1632
+ background: #ffffff;
1633
+ color: #000000;
1634
+ display: flex;
1635
+ align-items: center;
1636
+ justify-content: center;
1637
+ min-height: 100vh;
1638
+ padding: 24px;
1639
+ }
1640
+ .container {
1641
+ max-width: 480px;
1642
+ text-align: center;
1643
+ }
1644
+ .icon {
1645
+ width: 64px;
1646
+ height: 64px;
1647
+ margin: 0 auto 24px;
1648
+ background: #18a0fb;
1649
+ border-radius: 50%;
1650
+ display: flex;
1651
+ align-items: center;
1652
+ justify-content: center;
1653
+ font-size: 32px;
1654
+ color: white;
1655
+ }
1656
+ h1 {
1657
+ font-size: 32px;
1658
+ font-weight: 700;
1659
+ margin-bottom: 16px;
1660
+ letter-spacing: -0.02em;
1661
+ }
1662
+ p {
1663
+ font-size: 16px;
1664
+ color: #666666;
1665
+ line-height: 1.6;
1666
+ margin-bottom: 32px;
1667
+ }
1668
+ .button {
1669
+ display: inline-block;
1670
+ padding: 12px 24px;
1671
+ background: #000000;
1672
+ color: #ffffff;
1673
+ text-decoration: none;
1674
+ border-radius: 8px;
1675
+ font-weight: 500;
1676
+ font-size: 16px;
1677
+ border: none;
1678
+ cursor: pointer;
1679
+ transition: background 0.2s;
1680
+ }
1681
+ .button:hover {
1682
+ background: #333333;
1683
+ }
1684
+ .footer {
1685
+ margin-top: 48px;
1686
+ font-size: 14px;
1687
+ color: #999999;
1688
+ }
1689
+ </style>
1690
+ </head>
1691
+ <body>
1692
+ <div class="container">
1693
+ <div class="icon">✓</div>
1694
+ <h1>Authentication successful</h1>
1695
+ <p>You've successfully connected Figma Console MCP to your Figma account. You can now close this window and return to Claude.</p>
1696
+ <button class="button" onclick="window.close()">Close this window</button>
1697
+ <div class="footer">This window will automatically close in 5 seconds</div>
1698
+ </div>
1699
+ <script>
1700
+ setTimeout(() => window.close(), 5000);
1701
+ </script>
1702
+ </body>
1703
+ </html>`, {
1704
+ headers: {
1705
+ "Content-Type": "text/html; charset=utf-8"
1706
+ }
1707
+ });
1708
+ }
1709
+ catch (error) {
1710
+ logger.error({ error, sessionId }, "OAuth callback failed");
1711
+ return new Response(`<html><body>
1712
+ <h1>Authentication Error</h1>
1713
+ <p>Failed to complete authentication: ${error instanceof Error ? error.message : String(error)}</p>
1714
+ <p>Please try again or contact support.</p>
1715
+ </body></html>`, {
1716
+ status: 500,
1717
+ headers: { "Content-Type": "text/html" }
1718
+ });
1719
+ }
1720
+ }
1721
+ // Health check endpoint
1722
+ if (url.pathname === "/health") {
1723
+ return new Response(JSON.stringify({
1724
+ status: "healthy",
1725
+ service: "Figma Console MCP",
1726
+ version: "1.35.0",
1727
+ endpoints: {
1728
+ mcp: ["/sse", "/mcp"],
1729
+ oauth_mcp_spec: ["/.well-known/oauth-authorization-server", "/authorize", "/token", "/oauth/register"],
1730
+ oauth_legacy: ["/oauth/authorize", "/oauth/callback"],
1731
+ utility: ["/health"]
1732
+ },
1733
+ oauth_configured: !!env.FIGMA_OAUTH_CLIENT_ID
1734
+ }), {
1735
+ headers: { "Content-Type": "application/json" },
1736
+ });
1737
+ }
1738
+ // Serve favicon
1739
+ if (url.pathname === "/favicon.ico") {
1740
+ // Redirect to custom Figma Console icon
1741
+ return Response.redirect("https://p198.p4.n0.cdn.zight.com/items/Qwu1Dywx/b61b7b8f-05dc-4063-8a40-53fa4f8e3e97.jpg", 302);
1742
+ }
1743
+ // Proxy /docs to Mintlify
1744
+ if (/^\/docs/.test(url.pathname)) {
1745
+ // Try mintlify.app domain (Mintlify's standard hosting)
1746
+ const DOCS_URL = "southleftllc.mintlify.app";
1747
+ const CUSTOM_URL = "figma-console-mcp.southleft.com";
1748
+ const proxyUrl = new URL(request.url);
1749
+ proxyUrl.hostname = DOCS_URL;
1750
+ const proxyRequest = new Request(proxyUrl, request);
1751
+ proxyRequest.headers.set("Host", DOCS_URL);
1752
+ proxyRequest.headers.set("X-Forwarded-Host", CUSTOM_URL);
1753
+ proxyRequest.headers.set("X-Forwarded-Proto", "https");
1754
+ return await fetch(proxyRequest);
1755
+ }
1756
+ // Root path - serve landing page with editorial layout and light/dark mode
1757
+ if (url.pathname === "/") {
1758
+ return new Response(`<!DOCTYPE html>
1759
+ <html lang="en">
1760
+ <head>
1761
+ <meta charset="UTF-8">
1762
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1763
+ <title>Figma Console MCP - The Most Comprehensive MCP Server for Figma</title>
1764
+ <link rel="icon" type="image/svg+xml" href="https://docs.figma-console-mcp.southleft.com/favicon.svg">
1765
+ <meta name="description" content="Turn your Figma design system into a living API. 112+ tools give AI assistants deep access to design tokens, component specs, variables, and programmatic design creation.">
1766
+
1767
+ <!-- Open Graph -->
1768
+ <meta property="og:type" content="website">
1769
+ <meta property="og:url" content="https://figma-console-mcp.southleft.com">
1770
+ <meta property="og:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
1771
+ <meta property="og:description" content="The most comprehensive MCP server for Figma. 112+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1772
+ <meta property="og:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
1773
+ <meta property="og:image:width" content="1200">
1774
+ <meta property="og:image:height" content="630">
1775
+
1776
+ <!-- Twitter -->
1777
+ <meta name="twitter:card" content="summary_large_image">
1778
+ <meta name="twitter:title" content="Figma Console MCP - Turn Your Design System Into a Living API">
1779
+ <meta name="twitter:description" content="The most comprehensive MCP server for Figma. 112+ tools give AI assistants deep access to design tokens, components, variables, and programmatic design creation.">
1780
+ <meta name="twitter:image" content="https://docs.figma-console-mcp.southleft.com/images/og-image.jpg">
1781
+
1782
+ <meta name="theme-color" content="#0D9488">
1783
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1784
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1785
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
1786
+ <style>
1787
+ :root {
1788
+ --color-primary: #0D9488;
1789
+ --color-primary-light: #14B8A6;
1790
+ --color-primary-dark: #0F766E;
1791
+ --color-bg: #0F0F0F;
1792
+ --color-bg-elevated: #161616;
1793
+ --color-bg-card: #1A1A1A;
1794
+ --color-border: #2A2A2A;
1795
+ --color-border-hover: #3A3A3A;
1796
+ --color-rule: #2A2A2A;
1797
+ --color-text: #FAFAFA;
1798
+ --color-text-secondary: #A1A1A1;
1799
+ --color-text-tertiary: #666666;
1800
+ --font-mono: "JetBrains Mono", "SF Mono", Monaco, monospace;
1801
+ --radius-sm: 6px;
1802
+ --radius-md: 10px;
1803
+ --radius-lg: 16px;
1804
+ --transition: 0.2s ease;
1805
+ }
1806
+
1807
+ [data-theme="light"] {
1808
+ --color-bg: #FAFAFA;
1809
+ --color-bg-elevated: #FFFFFF;
1810
+ --color-bg-card: #FFFFFF;
1811
+ --color-border: #E5E5E5;
1812
+ --color-border-hover: #D4D4D4;
1813
+ --color-rule: #E5E5E5;
1814
+ --color-text: #171717;
1815
+ --color-text-secondary: #525252;
1816
+ --color-text-tertiary: #A3A3A3;
1817
+ }
1818
+
1819
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1820
+
1821
+ html {
1822
+ scroll-behavior: smooth;
1823
+ }
1824
+
1825
+ body {
1826
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1827
+ background: var(--color-bg);
1828
+ color: var(--color-text);
1829
+ line-height: 1.6;
1830
+ min-height: 100vh;
1831
+ transition: background var(--transition), color var(--transition);
1832
+ }
1833
+
1834
+ a { color: inherit; text-decoration: none; }
1835
+
1836
+ /* Header */
1837
+ .header {
1838
+ position: sticky;
1839
+ top: 0;
1840
+ z-index: 100;
1841
+ padding: 16px 32px;
1842
+ display: flex;
1843
+ justify-content: space-between;
1844
+ align-items: center;
1845
+ background: var(--color-bg);
1846
+ border-bottom: 1px solid var(--color-rule);
1847
+ backdrop-filter: blur(12px);
1848
+ transition: background var(--transition), border-color var(--transition);
1849
+ }
1850
+
1851
+ .logo img {
1852
+ height: 35px;
1853
+ transition: opacity var(--transition);
1854
+ }
1855
+
1856
+ .logo img:hover { opacity: 0.8; }
1857
+
1858
+ .header-right {
1859
+ display: flex;
1860
+ align-items: center;
1861
+ gap: 24px;
1862
+ }
1863
+
1864
+ .nav {
1865
+ display: flex;
1866
+ gap: 24px;
1867
+ align-items: center;
1868
+ }
1869
+
1870
+ .nav a {
1871
+ color: var(--color-text-secondary);
1872
+ font-size: 14px;
1873
+ font-weight: 500;
1874
+ transition: color var(--transition);
1875
+ }
1876
+
1877
+ .nav a:hover { color: var(--color-text); }
1878
+
1879
+ .nav a.sponsor-link {
1880
+ color: #db61a2;
1881
+ display: flex;
1882
+ align-items: center;
1883
+ gap: 5px;
1884
+ }
1885
+ .nav a.sponsor-link:hover { color: #ea4aaa; }
1886
+ .nav a.sponsor-link svg { width: 14px; height: 14px; fill: currentColor; }
1887
+
1888
+ .theme-toggle {
1889
+ display: flex;
1890
+ align-items: center;
1891
+ justify-content: center;
1892
+ width: 36px;
1893
+ height: 36px;
1894
+ background: transparent;
1895
+ border: 1px solid var(--color-border);
1896
+ border-radius: var(--radius-sm);
1897
+ cursor: pointer;
1898
+ color: var(--color-text-secondary);
1899
+ transition: all var(--transition);
1900
+ }
1901
+
1902
+ .theme-toggle:hover {
1903
+ border-color: var(--color-border-hover);
1904
+ color: var(--color-text);
1905
+ }
1906
+
1907
+ .theme-toggle svg { width: 18px; height: 18px; }
1908
+ .theme-toggle .sun { display: none; }
1909
+ [data-theme="light"] .theme-toggle .moon { display: none; }
1910
+ [data-theme="light"] .theme-toggle .sun { display: block; }
1911
+
1912
+ /* Main layout */
1913
+ .main {
1914
+ max-width: 1280px;
1915
+ margin: 0 auto;
1916
+ padding: 64px 32px 80px;
1917
+ }
1918
+
1919
+ /* Section dividers */
1920
+ .section-rule {
1921
+ border: none;
1922
+ border-top: 1px solid var(--color-rule);
1923
+ margin: 72px 0;
1924
+ }
1925
+
1926
+ /* Grid layout */
1927
+ .grid {
1928
+ display: grid;
1929
+ grid-template-columns: repeat(12, 1fr);
1930
+ gap: 48px 48px;
1931
+ }
1932
+
1933
+ .grid-cell {
1934
+ transition: all var(--transition);
1935
+ }
1936
+
1937
+ /* Rule-based separators for grid cells */
1938
+ .grid-cell.rule-left {
1939
+ padding-left: 48px;
1940
+ border-left: 1px solid var(--color-rule);
1941
+ }
1942
+
1943
+ .grid-cell.rule-top {
1944
+ padding-top: 32px;
1945
+ border-top: 1px solid var(--color-rule);
1946
+ }
1947
+
1948
+ /* Hero section */
1949
+ .hero-cell {
1950
+ grid-column: span 7;
1951
+ padding-right: 48px;
1952
+ }
1953
+
1954
+ .badge {
1955
+ display: inline-block;
1956
+ width: fit-content;
1957
+ color: var(--color-text-secondary);
1958
+ font-size: 11px;
1959
+ font-weight: 600;
1960
+ text-transform: uppercase;
1961
+ letter-spacing: 0.5px;
1962
+ margin-bottom: 20px;
1963
+ }
1964
+
1965
+ h1 {
1966
+ font-size: 48px;
1967
+ font-weight: 700;
1968
+ letter-spacing: -0.03em;
1969
+ line-height: 1.1;
1970
+ margin-bottom: 20px;
1971
+ }
1972
+
1973
+ .highlight {
1974
+ color: var(--color-primary-light);
1975
+ }
1976
+
1977
+ .hero-subtitle {
1978
+ font-size: 18px;
1979
+ color: var(--color-text-secondary);
1980
+ line-height: 1.7;
1981
+ max-width: 560px;
1982
+ margin-bottom: 32px;
1983
+ }
1984
+
1985
+ .cta-row {
1986
+ display: flex;
1987
+ gap: 12px;
1988
+ flex-wrap: wrap;
1989
+ }
1990
+
1991
+ .btn {
1992
+ display: inline-flex;
1993
+ align-items: center;
1994
+ gap: 8px;
1995
+ padding: 12px 20px;
1996
+ border-radius: var(--radius-sm);
1997
+ font-weight: 500;
1998
+ font-size: 14px;
1999
+ transition: all var(--transition);
2000
+ border: none;
2001
+ cursor: pointer;
2002
+ }
2003
+
2004
+ .btn svg { width: 16px; height: 16px; }
2005
+
2006
+ .btn-primary {
2007
+ background: var(--color-primary);
2008
+ color: #FFFFFF;
2009
+ }
2010
+
2011
+ .btn-primary:hover { background: var(--color-primary-dark); }
2012
+
2013
+ .btn-secondary {
2014
+ background: transparent;
2015
+ color: var(--color-text);
2016
+ border: 1px solid var(--color-border);
2017
+ }
2018
+
2019
+ .btn-secondary:hover {
2020
+ border-color: var(--color-border-hover);
2021
+ background: var(--color-bg-elevated);
2022
+ }
2023
+
2024
+ /* Hero right - capabilities showcase */
2025
+ .showcase-cell {
2026
+ grid-column: span 5;
2027
+ }
2028
+
2029
+ .showcase-label {
2030
+ font-size: 11px;
2031
+ font-weight: 600;
2032
+ text-transform: uppercase;
2033
+ letter-spacing: 0.5px;
2034
+ color: var(--color-text-tertiary);
2035
+ margin-bottom: 16px;
2036
+ }
2037
+
2038
+ .showcase-stat {
2039
+ display: flex;
2040
+ align-items: baseline;
2041
+ gap: 12px;
2042
+ margin-bottom: 24px;
2043
+ }
2044
+
2045
+ .showcase-stat .number {
2046
+ font-size: 56px;
2047
+ font-weight: 700;
2048
+ color: var(--color-primary-light);
2049
+ line-height: 1;
2050
+ }
2051
+
2052
+ .showcase-stat .label {
2053
+ font-size: 16px;
2054
+ color: var(--color-text-secondary);
2055
+ }
2056
+
2057
+ .capability-list {
2058
+ display: grid;
2059
+ grid-template-columns: 1fr 1fr;
2060
+ gap: 10px;
2061
+ }
2062
+
2063
+ .capability-item {
2064
+ display: flex;
2065
+ align-items: center;
2066
+ gap: 10px;
2067
+ padding: 10px 14px;
2068
+ background: var(--color-bg-elevated);
2069
+ border: 1px solid var(--color-border);
2070
+ border-radius: var(--radius-md);
2071
+ font-size: 13px;
2072
+ color: var(--color-text-secondary);
2073
+ transition: all var(--transition);
2074
+ }
2075
+
2076
+ .capability-item:hover {
2077
+ border-color: var(--color-primary);
2078
+ color: var(--color-text);
2079
+ }
2080
+
2081
+ .capability-item svg {
2082
+ width: 18px;
2083
+ height: 18px;
2084
+ color: var(--color-primary-light);
2085
+ flex-shrink: 0;
2086
+ }
2087
+
2088
+ /* Value proposition section */
2089
+ .value-cell {
2090
+ grid-column: span 12;
2091
+ text-align: center;
2092
+ padding: 64px 48px;
2093
+ position: relative;
2094
+ background: linear-gradient(135deg, rgba(13, 148, 136, 0.06) 0%, rgba(13, 148, 136, 0.02) 50%, transparent 100%);
2095
+ border-radius: var(--radius-lg);
2096
+ border: 1px solid rgba(13, 148, 136, 0.1);
2097
+ }
2098
+
2099
+ .value-cell::before {
2100
+ content: '';
2101
+ position: absolute;
2102
+ top: -1px;
2103
+ left: 50%;
2104
+ transform: translateX(-50%);
2105
+ width: 120px;
2106
+ height: 3px;
2107
+ background: linear-gradient(90deg, transparent, var(--color-primary-light), transparent);
2108
+ border-radius: 2px;
2109
+ }
2110
+
2111
+ .value-cell h2 {
2112
+ font-size: 36px;
2113
+ font-weight: 700;
2114
+ margin-bottom: 16px;
2115
+ letter-spacing: -0.03em;
2116
+ background: linear-gradient(135deg, var(--color-text) 0%, var(--color-primary-light) 100%);
2117
+ -webkit-background-clip: text;
2118
+ -webkit-text-fill-color: transparent;
2119
+ background-clip: text;
2120
+ }
2121
+
2122
+ [data-theme="light"] .value-cell h2 {
2123
+ background: linear-gradient(135deg, var(--color-text) 0%, var(--color-primary-dark) 100%);
2124
+ -webkit-background-clip: text;
2125
+ -webkit-text-fill-color: transparent;
2126
+ background-clip: text;
2127
+ }
2128
+
2129
+ .value-cell p {
2130
+ font-size: 18px;
2131
+ color: var(--color-text-secondary);
2132
+ max-width: 680px;
2133
+ margin: 0 auto;
2134
+ line-height: 1.7;
2135
+ }
2136
+
2137
+ /* Capabilities grid */
2138
+ .capability-card {
2139
+ grid-column: span 3;
2140
+ }
2141
+
2142
+ .capability-icon {
2143
+ width: 44px;
2144
+ height: 44px;
2145
+ display: flex;
2146
+ align-items: center;
2147
+ justify-content: center;
2148
+ background: rgba(13, 148, 136, 0.12);
2149
+ border-radius: var(--radius-md);
2150
+ color: var(--color-primary-light);
2151
+ margin-bottom: 16px;
2152
+ }
2153
+
2154
+ .capability-icon svg { width: 22px; height: 22px; }
2155
+
2156
+ .capability-card h3 {
2157
+ font-size: 17px;
2158
+ font-weight: 600;
2159
+ margin-bottom: 10px;
2160
+ }
2161
+
2162
+ .capability-card p {
2163
+ font-size: 14px;
2164
+ color: var(--color-text-secondary);
2165
+ line-height: 1.7;
2166
+ }
2167
+
2168
+ /* Prompt showcase */
2169
+ .prompts-cell {
2170
+ grid-column: span 6;
2171
+ }
2172
+
2173
+ .section-header {
2174
+ display: flex;
2175
+ align-items: center;
2176
+ gap: 12px;
2177
+ margin-bottom: 24px;
2178
+ }
2179
+
2180
+ .section-header-icon {
2181
+ width: 40px;
2182
+ height: 40px;
2183
+ display: flex;
2184
+ align-items: center;
2185
+ justify-content: center;
2186
+ background: rgba(13, 148, 136, 0.12);
2187
+ border-radius: var(--radius-md);
2188
+ color: var(--color-primary-light);
2189
+ }
2190
+
2191
+ .section-header-icon svg {
2192
+ width: 20px;
2193
+ height: 20px;
2194
+ }
2195
+
2196
+ .section-header h3 {
2197
+ font-size: 18px;
2198
+ font-weight: 600;
2199
+ }
2200
+
2201
+ .prompt-list {
2202
+ display: flex;
2203
+ flex-direction: column;
2204
+ gap: 12px;
2205
+ }
2206
+
2207
+ .prompt-item {
2208
+ display: flex;
2209
+ align-items: center;
2210
+ gap: 12px;
2211
+ padding: 14px 16px;
2212
+ background: var(--color-bg-elevated);
2213
+ border: 1px solid var(--color-border);
2214
+ border-radius: var(--radius-md);
2215
+ font-family: var(--font-mono);
2216
+ font-size: 13px;
2217
+ color: var(--color-text-secondary);
2218
+ transition: all var(--transition);
2219
+ }
2220
+
2221
+ .prompt-item:hover {
2222
+ border-color: var(--color-primary);
2223
+ color: var(--color-text);
2224
+ }
2225
+
2226
+ .prompt-item svg {
2227
+ width: 16px;
2228
+ height: 16px;
2229
+ color: var(--color-primary-light);
2230
+ flex-shrink: 0;
2231
+ }
2232
+
2233
+ /* Audience cells */
2234
+ .audience-cell {
2235
+ grid-column: span 6;
2236
+ padding: 24px 0;
2237
+ }
2238
+
2239
+ .audience-header {
2240
+ display: flex;
2241
+ align-items: center;
2242
+ gap: 12px;
2243
+ margin-bottom: 28px;
2244
+ }
2245
+
2246
+ .audience-icon {
2247
+ width: 40px;
2248
+ height: 40px;
2249
+ display: flex;
2250
+ align-items: center;
2251
+ justify-content: center;
2252
+ background: rgba(13, 148, 136, 0.12);
2253
+ border-radius: var(--radius-md);
2254
+ color: var(--color-primary-light);
2255
+ }
2256
+
2257
+ .audience-icon svg { width: 20px; height: 20px; }
2258
+
2259
+ .audience-header h3 {
2260
+ font-size: 18px;
2261
+ font-weight: 600;
2262
+ }
2263
+
2264
+ .audience-list {
2265
+ list-style: none;
2266
+ display: flex;
2267
+ flex-direction: column;
2268
+ gap: 24px;
2269
+ }
2270
+
2271
+ .audience-list li {
2272
+ display: flex;
2273
+ align-items: flex-start;
2274
+ gap: 12px;
2275
+ font-size: 14px;
2276
+ color: var(--color-text-secondary);
2277
+ }
2278
+
2279
+ .audience-list li svg {
2280
+ width: 18px;
2281
+ height: 18px;
2282
+ color: var(--color-primary);
2283
+ flex-shrink: 0;
2284
+ margin-top: 1px;
2285
+ }
2286
+
2287
+ /* Getting started CTA */
2288
+ .getting-started-cell {
2289
+ grid-column: span 12;
2290
+ display: flex;
2291
+ align-items: center;
2292
+ justify-content: space-between;
2293
+ padding: 40px 48px;
2294
+ background: var(--color-bg-elevated);
2295
+ border: 1px solid var(--color-border);
2296
+ border-radius: var(--radius-lg);
2297
+ margin-top: 32px;
2298
+ }
2299
+
2300
+ .getting-started-content h3 {
2301
+ font-size: 20px;
2302
+ font-weight: 600;
2303
+ margin-bottom: 8px;
2304
+ }
2305
+
2306
+ .getting-started-content p {
2307
+ font-size: 14px;
2308
+ color: var(--color-text-secondary);
2309
+ max-width: 480px;
2310
+ }
2311
+
2312
+ .getting-started-actions {
2313
+ display: flex;
2314
+ gap: 12px;
2315
+ }
2316
+
2317
+ /* Blog CTA */
2318
+ .blog-cell {
2319
+ grid-column: span 12;
2320
+ display: flex;
2321
+ align-items: center;
2322
+ justify-content: space-between;
2323
+ padding: 32px 0;
2324
+ border-top: 1px solid var(--color-rule);
2325
+ margin-top: 40px;
2326
+ }
2327
+
2328
+ .blog-content {
2329
+ display: flex;
2330
+ align-items: center;
2331
+ gap: 16px;
2332
+ }
2333
+
2334
+ .blog-icon {
2335
+ width: 44px;
2336
+ height: 44px;
2337
+ display: flex;
2338
+ align-items: center;
2339
+ justify-content: center;
2340
+ background: rgba(13, 148, 136, 0.12);
2341
+ border-radius: var(--radius-md);
2342
+ color: var(--color-primary-light);
2343
+ }
2344
+
2345
+ .blog-icon svg { width: 22px; height: 22px; }
2346
+
2347
+ .blog-text h4 {
2348
+ font-size: 15px;
2349
+ font-weight: 600;
2350
+ margin-bottom: 2px;
2351
+ }
2352
+
2353
+ .blog-text p {
2354
+ font-size: 13px;
2355
+ color: var(--color-text-secondary);
2356
+ }
2357
+
2358
+ .blog-link {
2359
+ display: flex;
2360
+ align-items: center;
2361
+ gap: 8px;
2362
+ color: var(--color-primary-light);
2363
+ font-size: 14px;
2364
+ font-weight: 500;
2365
+ transition: gap var(--transition);
2366
+ }
2367
+
2368
+ .blog-link:hover { gap: 12px; }
2369
+ .blog-link svg { width: 16px; height: 16px; }
2370
+
2371
+ /* Footer */
2372
+ .footer {
2373
+ max-width: 1280px;
2374
+ margin: 0 auto;
2375
+ padding: 24px 32px;
2376
+ display: flex;
2377
+ justify-content: space-between;
2378
+ align-items: center;
2379
+ border-top: 1px solid var(--color-rule);
2380
+ color: var(--color-text-tertiary);
2381
+ font-size: 13px;
2382
+ }
2383
+
2384
+ .footer a {
2385
+ color: var(--color-text-secondary);
2386
+ transition: color var(--transition);
2387
+ }
2388
+
2389
+ .footer a:hover { color: var(--color-text); }
2390
+
2391
+ .footer-links {
2392
+ display: flex;
2393
+ gap: 24px;
2394
+ }
2395
+
2396
+ /* Mobile nav */
2397
+ .mobile-menu-btn {
2398
+ display: none;
2399
+ align-items: center;
2400
+ justify-content: center;
2401
+ width: 36px;
2402
+ height: 36px;
2403
+ background: transparent;
2404
+ border: none;
2405
+ cursor: pointer;
2406
+ color: var(--color-text);
2407
+ }
2408
+
2409
+ .mobile-menu-btn svg { width: 24px; height: 24px; }
2410
+
2411
+ /* Mobile menu overlay */
2412
+ .mobile-menu {
2413
+ display: none;
2414
+ position: fixed;
2415
+ top: 0;
2416
+ left: 0;
2417
+ right: 0;
2418
+ bottom: 0;
2419
+ background: var(--color-bg);
2420
+ z-index: 1000;
2421
+ padding: 20px;
2422
+ flex-direction: column;
2423
+ }
2424
+
2425
+ .mobile-menu.active {
2426
+ display: flex;
2427
+ }
2428
+
2429
+ .mobile-menu-header {
2430
+ display: flex;
2431
+ justify-content: space-between;
2432
+ align-items: center;
2433
+ margin-bottom: 48px;
2434
+ }
2435
+
2436
+ .mobile-menu-close {
2437
+ display: flex;
2438
+ align-items: center;
2439
+ justify-content: center;
2440
+ width: 36px;
2441
+ height: 36px;
2442
+ background: transparent;
2443
+ border: none;
2444
+ cursor: pointer;
2445
+ color: var(--color-text);
2446
+ }
2447
+
2448
+ .mobile-menu-close svg { width: 24px; height: 24px; }
2449
+
2450
+ .mobile-menu-nav {
2451
+ display: flex;
2452
+ flex-direction: column;
2453
+ gap: 8px;
2454
+ }
2455
+
2456
+ .mobile-menu-nav a {
2457
+ display: block;
2458
+ padding: 16px 0;
2459
+ font-size: 18px;
2460
+ font-weight: 500;
2461
+ color: var(--color-text);
2462
+ border-bottom: 1px solid var(--color-border);
2463
+ transition: color var(--transition);
2464
+ }
2465
+
2466
+ .mobile-menu-nav a:hover {
2467
+ color: var(--color-primary-light);
2468
+ }
2469
+
2470
+ body.menu-open {
2471
+ overflow: hidden;
2472
+ }
2473
+
2474
+ /* Responsive */
2475
+ @media (max-width: 1024px) {
2476
+ .hero-cell {
2477
+ grid-column: span 12;
2478
+ padding-right: 0;
2479
+ padding-bottom: 40px;
2480
+ border-bottom: 1px solid var(--color-rule);
2481
+ }
2482
+ .showcase-cell {
2483
+ grid-column: span 12;
2484
+ padding-left: 0;
2485
+ padding-top: 40px;
2486
+ border-left: none;
2487
+ }
2488
+ .capability-card {
2489
+ grid-column: span 6;
2490
+ }
2491
+ .capability-card.rule-left {
2492
+ padding-left: 0;
2493
+ border-left: none;
2494
+ }
2495
+ .capability-card:nth-child(odd) {
2496
+ padding-left: 0;
2497
+ border-left: none;
2498
+ }
2499
+ .capability-card:nth-child(even) {
2500
+ padding-left: 32px;
2501
+ border-left: 1px solid var(--color-rule);
2502
+ }
2503
+ .prompts-cell { grid-column: span 12; }
2504
+ .audience-cell {
2505
+ grid-column: span 6;
2506
+ padding-left: 0;
2507
+ }
2508
+ .audience-cell.rule-left {
2509
+ padding-left: 32px;
2510
+ border-left: 1px solid var(--color-rule);
2511
+ }
2512
+ h1 { font-size: 40px; }
2513
+ .value-cell { padding: 48px 32px; }
2514
+ .value-cell h2 { font-size: 30px; }
2515
+ .section-rule { margin: 56px 0; }
2516
+ .getting-started-cell {
2517
+ flex-direction: column;
2518
+ gap: 24px;
2519
+ text-align: center;
2520
+ }
2521
+ .getting-started-content p {
2522
+ max-width: 100%;
2523
+ }
2524
+ }
2525
+
2526
+ @media (max-width: 768px) {
2527
+ .header { padding: 12px 20px; }
2528
+ .nav { display: none; }
2529
+ .mobile-menu-btn { display: flex; }
2530
+ .main { padding: 32px 20px; }
2531
+ .grid { gap: 32px; }
2532
+
2533
+ /* Remove all vertical rules and left padding on mobile */
2534
+ .grid-cell.rule-left {
2535
+ padding-left: 0;
2536
+ border-left: none;
2537
+ }
2538
+
2539
+ .hero-cell {
2540
+ padding-right: 0;
2541
+ padding-bottom: 32px;
2542
+ }
2543
+ .showcase-cell {
2544
+ padding-top: 32px;
2545
+ }
2546
+ .capability-list {
2547
+ grid-template-columns: 1fr;
2548
+ }
2549
+ .capability-card {
2550
+ grid-column: span 12;
2551
+ padding: 0 !important;
2552
+ border: none !important;
2553
+ }
2554
+ .audience-cell {
2555
+ grid-column: span 12;
2556
+ padding: 0 !important;
2557
+ }
2558
+ .audience-cell.rule-left {
2559
+ padding-top: 32px !important;
2560
+ margin-top: 8px;
2561
+ border-top: 1px solid var(--color-rule);
2562
+ }
2563
+ h1 { font-size: 34px; }
2564
+ .hero-subtitle { font-size: 16px; }
2565
+ .showcase-stat .number { font-size: 48px; }
2566
+ .value-cell { padding: 40px 24px; }
2567
+ .value-cell h2 { font-size: 26px; }
2568
+ .value-cell p { font-size: 16px; }
2569
+ .section-rule { margin: 48px 0; }
2570
+ .blog-cell {
2571
+ flex-direction: column;
2572
+ gap: 16px;
2573
+ text-align: center;
2574
+ }
2575
+ .blog-content { flex-direction: column; }
2576
+ .footer {
2577
+ flex-direction: column;
2578
+ gap: 16px;
2579
+ text-align: center;
2580
+ }
2581
+ .getting-started-cell {
2582
+ padding: 24px;
2583
+ }
2584
+ .getting-started-actions {
2585
+ flex-direction: column;
2586
+ width: 100%;
2587
+ }
2588
+ .getting-started-actions .btn {
2589
+ justify-content: center;
2590
+ }
2591
+ }
2592
+
2593
+ @media (max-width: 480px) {
2594
+ .cta-row { flex-direction: column; }
2595
+ .btn { justify-content: center; }
2596
+ }
2597
+ </style>
2598
+ </head>
2599
+ <body>
2600
+ <header class="header">
2601
+ <a href="/" class="logo">
2602
+ <img src="https://docs.figma-console-mcp.southleft.com/logo/light.svg" alt="Figma Console MCP" class="logo-dark">
2603
+ <img src="https://docs.figma-console-mcp.southleft.com/logo/dark.svg" alt="Figma Console MCP" class="logo-light" style="display: none;">
2604
+ </a>
2605
+ <div class="header-right">
2606
+ <nav class="nav">
2607
+ <a href="https://docs.figma-console-mcp.southleft.com">Documentation</a>
2608
+ <a href="https://github.com/southleft/figma-console-mcp">GitHub</a>
2609
+ <a href="https://www.npmjs.com/package/figma-console-mcp">npm</a>
2610
+ <a href="https://southleft.com/insights/ai/figma-console-mcp-ai-powered-design-system-management/">Blog</a>
2611
+ <a href="https://github.com/sponsors/southleft" class="sponsor-link"><svg viewBox="0 0 16 16"><path d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.6 20.6 0 0 0 8 13.393a20.6 20.6 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5"/></svg>Sponsor</a>
2612
+ </nav>
2613
+ <button class="theme-toggle" aria-label="Toggle theme">
2614
+ <svg class="moon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
2615
+ <svg class="sun" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
2616
+ </button>
2617
+ <button class="mobile-menu-btn" aria-label="Menu">
2618
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
2619
+ </button>
2620
+ </div>
2621
+ </header>
2622
+
2623
+ <!-- Mobile Menu -->
2624
+ <div class="mobile-menu" id="mobileMenu">
2625
+ <div class="mobile-menu-header">
2626
+ <a href="/" class="logo">
2627
+ <img src="https://docs.figma-console-mcp.southleft.com/logo/light.svg" alt="Figma Console MCP" class="logo-dark">
2628
+ <img src="https://docs.figma-console-mcp.southleft.com/logo/dark.svg" alt="Figma Console MCP" class="logo-light" style="display: none;">
2629
+ </a>
2630
+ <button class="mobile-menu-close" aria-label="Close menu">
2631
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>
2632
+ </button>
2633
+ </div>
2634
+ <nav class="mobile-menu-nav">
2635
+ <a href="https://docs.figma-console-mcp.southleft.com">Documentation</a>
2636
+ <a href="https://github.com/southleft/figma-console-mcp">GitHub</a>
2637
+ <a href="https://www.npmjs.com/package/figma-console-mcp">npm</a>
2638
+ <a href="https://southleft.com/insights/ai/figma-console-mcp-ai-powered-design-system-management/">Blog</a>
2639
+ <a href="https://github.com/sponsors/southleft" style="color: #db61a2;">♥ Sponsor</a>
2640
+ </nav>
2641
+ </div>
2642
+
2643
+ <main class="main">
2644
+ <div class="grid">
2645
+ <!-- Hero -->
2646
+ <div class="grid-cell hero-cell">
2647
+ <div class="badge">Model Context Protocol</div>
2648
+ <h1>Your design system, now a <span class="highlight">living API</span></h1>
2649
+ <p class="hero-subtitle">The most comprehensive MCP server for Figma. Give AI assistants deep access to your design tokens, components, and variables. Read, query, and even create designs programmatically through natural language.</p>
2650
+ <div class="cta-row">
2651
+ <a href="https://docs.figma-console-mcp.southleft.com" class="btn btn-primary">
2652
+ Read the Docs
2653
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2654
+ </a>
2655
+ <a href="https://github.com/southleft/figma-console-mcp" class="btn btn-secondary">
2656
+ <svg fill="currentColor" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
2657
+ View on GitHub
2658
+ </a>
2659
+ </div>
2660
+ </div>
2661
+
2662
+ <!-- Capabilities showcase -->
2663
+ <div class="grid-cell showcase-cell rule-left">
2664
+ <div class="showcase-label">What AI Can Access</div>
2665
+ <div class="showcase-stat">
2666
+ <span class="number">112+</span>
2667
+ <span class="label">MCP tools for Figma</span>
2668
+ </div>
2669
+ <div class="capability-list">
2670
+ <div class="capability-item">
2671
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/></svg>
2672
+ <span>Design tokens and variables</span>
2673
+ </div>
2674
+ <div class="capability-item">
2675
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
2676
+ <span>Component specs and properties</span>
2677
+ </div>
2678
+ <div class="capability-item">
2679
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
2680
+ <span>Programmatic design creation</span>
2681
+ </div>
2682
+ <div class="capability-item">
2683
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
2684
+ <span>WCAG accessibility linting</span>
2685
+ </div>
2686
+ <div class="capability-item">
2687
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"/></svg>
2688
+ <span>Cloud relay for web AI clients</span>
2689
+ </div>
2690
+ <div class="capability-item">
2691
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
2692
+ <span>Visual debugging and screenshots</span>
2693
+ </div>
2694
+ <div class="capability-item">
2695
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/></svg>
2696
+ <span>Design annotations and dev specs</span>
2697
+ </div>
2698
+ <div class="capability-item">
2699
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="4" rx="1"/><rect x="2" y="9" width="9" height="4" rx="1"/><rect x="13" y="9" width="9" height="4" rx="1"/><rect x="2" y="15" width="20" height="4" rx="1"/></svg>
2700
+ <span>FigJam boards and Slides presentations</span>
2701
+ </div>
2702
+ </div>
2703
+ </div>
2704
+ </div>
2705
+
2706
+ <hr class="section-rule">
2707
+
2708
+ <!-- Value proposition -->
2709
+ <div class="grid">
2710
+ <div class="grid-cell value-cell">
2711
+ <h2>Design system intelligence for AI assistants</h2>
2712
+ <p>Whether you're maintaining a component library, implementing designs in code, or building Figma plugins, this MCP server gives AI the deep context it needs.</p>
2713
+ </div>
2714
+ </div>
2715
+
2716
+ <hr class="section-rule">
2717
+
2718
+ <!-- Capabilities detail -->
2719
+ <div class="grid">
2720
+ <div class="grid-cell capability-card">
2721
+ <div class="capability-icon">
2722
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/></svg>
2723
+ </div>
2724
+ <h3>Design Tokens</h3>
2725
+ <p>Extract colors, typography, spacing, and effects. Export as CSS custom properties, Tailwind config, or Sass variables.</p>
2726
+ </div>
2727
+
2728
+ <div class="grid-cell capability-card rule-left">
2729
+ <div class="capability-icon">
2730
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
2731
+ </div>
2732
+ <h3>Component Specs</h3>
2733
+ <p>Get detailed layout, spacing, variants, and property data for any component in your design system.</p>
2734
+ </div>
2735
+
2736
+ <div class="grid-cell capability-card rule-left">
2737
+ <div class="capability-icon">
2738
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
2739
+ </div>
2740
+ <h3>Programmatic Design</h3>
2741
+ <p>Create and modify variables, build component variants, and generate design elements through natural language.</p>
2742
+ </div>
2743
+
2744
+ <div class="grid-cell capability-card rule-left">
2745
+ <div class="capability-icon">
2746
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
2747
+ </div>
2748
+ <h3>Visual Debugging</h3>
2749
+ <p>Capture screenshots, inspect node properties, and track selection changes. Let AI analyze your designs and suggest improvements.</p>
2750
+ </div>
2751
+ </div>
2752
+
2753
+ <hr class="section-rule">
2754
+
2755
+ <div class="grid">
2756
+ <!-- Example Prompts -->
2757
+ <div class="grid-cell prompts-cell">
2758
+ <div class="section-header">
2759
+ <div class="section-header-icon">
2760
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
2761
+ </div>
2762
+ <h3>What you can ask</h3>
2763
+ </div>
2764
+ <div class="prompt-list">
2765
+ <div class="prompt-item">
2766
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2767
+ <span>"Extract all color variables as Tailwind config"</span>
2768
+ </div>
2769
+ <div class="prompt-item">
2770
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2771
+ <span>"Get the Button component specs from my design system"</span>
2772
+ </div>
2773
+ <div class="prompt-item">
2774
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2775
+ <span>"Create a dark mode version of my color variables"</span>
2776
+ </div>
2777
+ <div class="prompt-item">
2778
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2779
+ <span>"Check my design for accessibility issues"</span>
2780
+ </div>
2781
+ <div class="prompt-item">
2782
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2783
+ <span>"Connect to my Figma plugin and create a card component"</span>
2784
+ </div>
2785
+ <div class="prompt-item">
2786
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2787
+ <span>"Create a retrospective board with colored stickies on FigJam"</span>
2788
+ </div>
2789
+ <div class="prompt-item">
2790
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2791
+ <span>"List my slides and set a dissolve transition on each one"</span>
2792
+ </div>
2793
+ </div>
2794
+ </div>
2795
+
2796
+ <!-- For Designers -->
2797
+ <div class="grid-cell audience-cell rule-left">
2798
+ <div class="audience-header">
2799
+ <div class="audience-icon">
2800
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
2801
+ </div>
2802
+ <h3>For Designers</h3>
2803
+ </div>
2804
+ <ul class="audience-list">
2805
+ <li>
2806
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2807
+ <span>Generate design token documentation automatically</span>
2808
+ </li>
2809
+ <li>
2810
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2811
+ <span>Create component variants with AI assistance</span>
2812
+ </li>
2813
+ <li>
2814
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2815
+ <span>Lint designs for accessibility and quality issues</span>
2816
+ </li>
2817
+ </ul>
2818
+ </div>
2819
+ </div>
2820
+
2821
+ <div class="grid" style="margin-top: 48px;">
2822
+ <!-- For Engineers -->
2823
+ <div class="grid-cell audience-cell">
2824
+ <div class="audience-header">
2825
+ <div class="audience-icon">
2826
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
2827
+ </div>
2828
+ <h3>For Engineers</h3>
2829
+ </div>
2830
+ <ul class="audience-list">
2831
+ <li>
2832
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2833
+ <span>Extract tokens as CSS, Tailwind, or Sass variables</span>
2834
+ </li>
2835
+ <li>
2836
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2837
+ <span>Get accurate component specs for implementation</span>
2838
+ </li>
2839
+ <li>
2840
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2841
+ <span>Query your design system via MCP-enabled AI tools</span>
2842
+ </li>
2843
+ </ul>
2844
+ </div>
2845
+
2846
+ <!-- For Teams -->
2847
+ <div class="grid-cell audience-cell rule-left">
2848
+ <div class="audience-header">
2849
+ <div class="audience-icon">
2850
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
2851
+ </div>
2852
+ <h3>For Teams of All Sizes</h3>
2853
+ </div>
2854
+ <ul class="audience-list">
2855
+ <li>
2856
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2857
+ <span>Indie developers to enterprise design systems</span>
2858
+ </li>
2859
+ <li>
2860
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2861
+ <span>Local mode for development, remote for production</span>
2862
+ </li>
2863
+ <li>
2864
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
2865
+ <span>Works with Claude, Cursor, and any MCP client</span>
2866
+ </li>
2867
+ </ul>
2868
+ </div>
2869
+ </div>
2870
+
2871
+ <!-- Getting Started CTA -->
2872
+ <div class="grid">
2873
+ <div class="grid-cell getting-started-cell">
2874
+ <div class="getting-started-content">
2875
+ <h3>Ready to get started?</h3>
2876
+ <p>Three ways to connect: local mode for full capabilities, cloud mode for web AI clients like Claude.ai and v0, or remote mode for quick read-only access. Our docs will guide you through the right path.</p>
2877
+ </div>
2878
+ <div class="getting-started-actions">
2879
+ <a href="https://docs.figma-console-mcp.southleft.com/setup" class="btn btn-primary">
2880
+ View Setup Guide
2881
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2882
+ </a>
2883
+ <a href="https://docs.figma-console-mcp.southleft.com/tools" class="btn btn-secondary">
2884
+ Explore Tools
2885
+ </a>
2886
+ </div>
2887
+ </div>
2888
+ </div>
2889
+
2890
+ <div class="grid">
2891
+ <!-- Blog CTA -->
2892
+ <a href="https://southleft.com/insights/ai/figma-console-mcp-ai-powered-design-system-management/" class="grid-cell blog-cell">
2893
+ <div class="blog-content">
2894
+ <div class="blog-icon">
2895
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
2896
+ </div>
2897
+ <div class="blog-text">
2898
+ <h4>Read the announcement</h4>
2899
+ <p>AI-Powered Design System Management with Figma Console MCP</p>
2900
+ </div>
2901
+ </div>
2902
+ <span class="blog-link">
2903
+ Read article
2904
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
2905
+ </span>
2906
+ </a>
2907
+ </div>
2908
+ </main>
2909
+
2910
+ <footer class="footer">
2911
+ <p>MIT License. Built by <a href="https://southleft.com">Southleft</a></p>
2912
+ <div class="footer-links">
2913
+ <a href="https://docs.figma-console-mcp.southleft.com">Docs</a>
2914
+ <a href="https://github.com/southleft/figma-console-mcp">GitHub</a>
2915
+ <a href="https://www.npmjs.com/package/figma-console-mcp">npm</a>
2916
+ <a href="https://github.com/sponsors/southleft" style="color: #db61a2;">♥ Sponsor</a>
2917
+ </div>
2918
+ </footer>
2919
+
2920
+ <script>
2921
+ // Theme toggle with system preference detection
2922
+ (function() {
2923
+ const html = document.documentElement;
2924
+ const toggle = document.querySelector('.theme-toggle');
2925
+ const logosDark = document.querySelectorAll('.logo-dark');
2926
+ const logosLight = document.querySelectorAll('.logo-light');
2927
+
2928
+ function getSystemTheme() {
2929
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
2930
+ }
2931
+
2932
+ function getStoredTheme() {
2933
+ return localStorage.getItem('theme');
2934
+ }
2935
+
2936
+ function setTheme(theme) {
2937
+ html.setAttribute('data-theme', theme);
2938
+ localStorage.setItem('theme', theme);
2939
+ updateLogos(theme);
2940
+ }
2941
+
2942
+ function updateLogos(theme) {
2943
+ logosDark.forEach(logo => {
2944
+ logo.style.display = theme === 'light' ? 'none' : 'block';
2945
+ });
2946
+ logosLight.forEach(logo => {
2947
+ logo.style.display = theme === 'light' ? 'block' : 'none';
2948
+ });
2949
+ }
2950
+
2951
+ // Initialize theme
2952
+ const storedTheme = getStoredTheme();
2953
+ const initialTheme = storedTheme || getSystemTheme();
2954
+ setTheme(initialTheme);
2955
+
2956
+ // Listen for system theme changes
2957
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
2958
+ if (!getStoredTheme()) {
2959
+ setTheme(e.matches ? 'light' : 'dark');
2960
+ }
2961
+ });
2962
+
2963
+ // Toggle button
2964
+ toggle.addEventListener('click', () => {
2965
+ const currentTheme = html.getAttribute('data-theme');
2966
+ setTheme(currentTheme === 'light' ? 'dark' : 'light');
2967
+ });
2968
+ })();
2969
+
2970
+ // Mobile menu toggle
2971
+ (function() {
2972
+ const menuBtn = document.querySelector('.mobile-menu-btn');
2973
+ const menu = document.getElementById('mobileMenu');
2974
+ const closeBtn = document.querySelector('.mobile-menu-close');
2975
+ const menuLinks = document.querySelectorAll('.mobile-menu-nav a');
2976
+
2977
+ function openMenu() {
2978
+ menu.classList.add('active');
2979
+ document.body.classList.add('menu-open');
2980
+ }
2981
+
2982
+ function closeMenu() {
2983
+ menu.classList.remove('active');
2984
+ document.body.classList.remove('menu-open');
2985
+ }
2986
+
2987
+ menuBtn.addEventListener('click', openMenu);
2988
+ closeBtn.addEventListener('click', closeMenu);
2989
+
2990
+ // Close menu when clicking a link
2991
+ menuLinks.forEach(link => {
2992
+ link.addEventListener('click', closeMenu);
2993
+ });
2994
+
2995
+ // Close menu on escape key
2996
+ document.addEventListener('keydown', (e) => {
2997
+ if (e.key === 'Escape' && menu.classList.contains('active')) {
2998
+ closeMenu();
2999
+ }
3000
+ });
3001
+ })();
3002
+ </script>
3003
+ </body>
3004
+ </html>`, {
3005
+ headers: { "Content-Type": "text/html; charset=utf-8" }
3006
+ });
3007
+ }
3008
+ return new Response("Not found", { status: 404 });
3009
+ },
3010
+ };