@mp3wizard/figma-console-mcp 1.23.1 → 1.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +49 -33
  2. package/dist/cloudflare/core/config.js +0 -8
  3. package/dist/cloudflare/core/console-monitor.js +3 -3
  4. package/dist/cloudflare/core/diagnose-tool.js +96 -0
  5. package/dist/cloudflare/core/figma-tools.js +69 -229
  6. package/dist/cloudflare/core/identity.js +96 -0
  7. package/dist/cloudflare/core/tokens/alias-resolver.js +98 -0
  8. package/dist/cloudflare/core/tokens/config.js +284 -0
  9. package/dist/cloudflare/core/tokens/figma-converter.js +195 -0
  10. package/dist/cloudflare/core/tokens/formatters/css-vars.js +329 -0
  11. package/dist/cloudflare/core/tokens/formatters/dtcg.js +300 -0
  12. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  13. package/dist/cloudflare/core/tokens/formatters/json.js +7 -0
  14. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  15. package/dist/cloudflare/core/tokens/formatters/scss.js +4 -0
  16. package/dist/cloudflare/core/tokens/formatters/stubs.js +11 -0
  17. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +4 -0
  18. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +4 -0
  19. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +4 -0
  20. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +4 -0
  21. package/dist/cloudflare/core/tokens/formatters/ts-module.js +4 -0
  22. package/dist/cloudflare/core/tokens/index.js +15 -0
  23. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  24. package/dist/cloudflare/core/tokens/parsers/dtcg.js +253 -0
  25. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  26. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  27. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  28. package/dist/cloudflare/core/tokens/parsers/stubs.js +13 -0
  29. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  30. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  31. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  32. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  33. package/dist/cloudflare/core/tokens/schemas.js +148 -0
  34. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  35. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  36. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  37. package/dist/cloudflare/core/tokens/types.js +18 -0
  38. package/dist/cloudflare/core/tokens-tools.js +849 -0
  39. package/dist/cloudflare/core/version-tools.js +151 -7
  40. package/dist/cloudflare/core/websocket-server.js +77 -55
  41. package/dist/cloudflare/index.js +37 -26
  42. package/dist/core/config.d.ts.map +1 -1
  43. package/dist/core/config.js +0 -8
  44. package/dist/core/config.js.map +1 -1
  45. package/dist/core/console-monitor.d.ts +2 -2
  46. package/dist/core/console-monitor.d.ts.map +1 -1
  47. package/dist/core/console-monitor.js +3 -3
  48. package/dist/core/console-monitor.js.map +1 -1
  49. package/dist/core/diagnose-tool.d.ts +33 -0
  50. package/dist/core/diagnose-tool.d.ts.map +1 -0
  51. package/dist/core/diagnose-tool.js +97 -0
  52. package/dist/core/diagnose-tool.js.map +1 -0
  53. package/dist/core/diff/diff-engine.d.ts +14 -0
  54. package/dist/core/diff/diff-engine.d.ts.map +1 -1
  55. package/dist/core/diff/diff-engine.js.map +1 -1
  56. package/dist/core/figma-connector.d.ts +1 -1
  57. package/dist/core/figma-connector.d.ts.map +1 -1
  58. package/dist/core/figma-tools.d.ts +1 -2
  59. package/dist/core/figma-tools.d.ts.map +1 -1
  60. package/dist/core/figma-tools.js +69 -229
  61. package/dist/core/figma-tools.js.map +1 -1
  62. package/dist/core/identity.d.ts +41 -0
  63. package/dist/core/identity.d.ts.map +1 -0
  64. package/dist/core/identity.js +97 -0
  65. package/dist/core/identity.js.map +1 -0
  66. package/dist/core/tokens/alias-resolver.d.ts +40 -0
  67. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  68. package/dist/core/tokens/alias-resolver.js +99 -0
  69. package/dist/core/tokens/alias-resolver.js.map +1 -0
  70. package/dist/core/tokens/config.d.ts +352 -0
  71. package/dist/core/tokens/config.d.ts.map +1 -0
  72. package/dist/core/tokens/config.js +285 -0
  73. package/dist/core/tokens/config.js.map +1 -0
  74. package/dist/core/tokens/figma-converter.d.ts +81 -0
  75. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  76. package/dist/core/tokens/figma-converter.js +196 -0
  77. package/dist/core/tokens/figma-converter.js.map +1 -0
  78. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  79. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  80. package/dist/core/tokens/formatters/css-vars.js +330 -0
  81. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  82. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  83. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  84. package/dist/core/tokens/formatters/dtcg.js +301 -0
  85. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  86. package/dist/core/tokens/formatters/index.d.ts +30 -0
  87. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  88. package/dist/core/tokens/formatters/index.js +46 -0
  89. package/dist/core/tokens/formatters/index.js.map +1 -0
  90. package/dist/core/tokens/formatters/json.d.ts +5 -0
  91. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  92. package/dist/core/tokens/formatters/json.js +8 -0
  93. package/dist/core/tokens/formatters/json.js.map +1 -0
  94. package/dist/core/tokens/formatters/less.d.ts +4 -0
  95. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  96. package/dist/core/tokens/formatters/less.js +5 -0
  97. package/dist/core/tokens/formatters/less.js.map +1 -0
  98. package/dist/core/tokens/formatters/scss.d.ts +4 -0
  99. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  100. package/dist/core/tokens/formatters/scss.js +5 -0
  101. package/dist/core/tokens/formatters/scss.js.map +1 -0
  102. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  103. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  104. package/dist/core/tokens/formatters/stubs.js +12 -0
  105. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  106. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +4 -0
  107. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  108. package/dist/core/tokens/formatters/style-dictionary-v3.js +5 -0
  109. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  110. package/dist/core/tokens/formatters/tailwind-v3.d.ts +4 -0
  111. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  112. package/dist/core/tokens/formatters/tailwind-v3.js +5 -0
  113. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  114. package/dist/core/tokens/formatters/tailwind-v4.d.ts +4 -0
  115. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  116. package/dist/core/tokens/formatters/tailwind-v4.js +5 -0
  117. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  118. package/dist/core/tokens/formatters/tokens-studio.d.ts +4 -0
  119. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  120. package/dist/core/tokens/formatters/tokens-studio.js +5 -0
  121. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  122. package/dist/core/tokens/formatters/ts-module.d.ts +4 -0
  123. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  124. package/dist/core/tokens/formatters/ts-module.js +5 -0
  125. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  126. package/dist/core/tokens/index.d.ts +17 -0
  127. package/dist/core/tokens/index.d.ts.map +1 -0
  128. package/dist/core/tokens/index.js +16 -0
  129. package/dist/core/tokens/index.js.map +1 -0
  130. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  131. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  132. package/dist/core/tokens/parsers/css-vars.js +5 -0
  133. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  134. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  135. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  136. package/dist/core/tokens/parsers/dtcg.js +254 -0
  137. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  138. package/dist/core/tokens/parsers/index.d.ts +37 -0
  139. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  140. package/dist/core/tokens/parsers/index.js +139 -0
  141. package/dist/core/tokens/parsers/index.js.map +1 -0
  142. package/dist/core/tokens/parsers/json.d.ts +4 -0
  143. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  144. package/dist/core/tokens/parsers/json.js +8 -0
  145. package/dist/core/tokens/parsers/json.js.map +1 -0
  146. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  147. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  148. package/dist/core/tokens/parsers/scss.js +5 -0
  149. package/dist/core/tokens/parsers/scss.js.map +1 -0
  150. package/dist/core/tokens/parsers/stubs.d.ts +11 -0
  151. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  152. package/dist/core/tokens/parsers/stubs.js +14 -0
  153. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  154. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  155. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  156. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  157. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  158. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  159. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  160. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  161. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  162. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  163. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  164. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  165. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  166. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  167. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  168. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  169. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  170. package/dist/core/tokens/schemas.d.ts +152 -0
  171. package/dist/core/tokens/schemas.d.ts.map +1 -0
  172. package/dist/core/tokens/schemas.js +149 -0
  173. package/dist/core/tokens/schemas.js.map +1 -0
  174. package/dist/core/tokens/transforms/color.d.ts +9 -0
  175. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  176. package/dist/core/tokens/transforms/color.js +13 -0
  177. package/dist/core/tokens/transforms/color.js.map +1 -0
  178. package/dist/core/tokens/transforms/index.d.ts +36 -0
  179. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  180. package/dist/core/tokens/transforms/index.js +30 -0
  181. package/dist/core/tokens/transforms/index.js.map +1 -0
  182. package/dist/core/tokens/transforms/size.d.ts +7 -0
  183. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  184. package/dist/core/tokens/transforms/size.js +8 -0
  185. package/dist/core/tokens/transforms/size.js.map +1 -0
  186. package/dist/core/tokens/types.d.ts +228 -0
  187. package/dist/core/tokens/types.d.ts.map +1 -0
  188. package/dist/core/tokens/types.js +19 -0
  189. package/dist/core/tokens/types.js.map +1 -0
  190. package/dist/core/tokens-tools.d.ts +42 -0
  191. package/dist/core/tokens-tools.d.ts.map +1 -0
  192. package/dist/core/tokens-tools.js +850 -0
  193. package/dist/core/tokens-tools.js.map +1 -0
  194. package/dist/core/types/index.d.ts +0 -8
  195. package/dist/core/types/index.d.ts.map +1 -1
  196. package/dist/core/version-tools.d.ts +30 -1
  197. package/dist/core/version-tools.d.ts.map +1 -1
  198. package/dist/core/version-tools.js +151 -7
  199. package/dist/core/version-tools.js.map +1 -1
  200. package/dist/core/websocket-connector.d.ts +1 -1
  201. package/dist/core/websocket-connector.d.ts.map +1 -1
  202. package/dist/core/websocket-server.d.ts +47 -3
  203. package/dist/core/websocket-server.d.ts.map +1 -1
  204. package/dist/core/websocket-server.js +77 -55
  205. package/dist/core/websocket-server.js.map +1 -1
  206. package/dist/local.d.ts +0 -12
  207. package/dist/local.d.ts.map +1 -1
  208. package/dist/local.js +967 -3406
  209. package/dist/local.js.map +1 -1
  210. package/figma-desktop-bridge/code.js +59 -63
  211. package/figma-desktop-bridge/ui.html +85 -11
  212. package/package.json +12 -30
  213. package/figma-desktop-bridge/ui-full.html +0 -1353
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared NotImplementedError used by formatter stubs. DTCG JSON and CSS
3
+ * custom properties are the fully-implemented formatters; everything else
4
+ * is scaffolded and throws this error.
5
+ */
6
+ export class FormatterNotImplementedError extends Error {
7
+ constructor(formatName) {
8
+ super(`[figma-console-mcp] The ${formatName} formatter is scaffolded but not yet implemented. Fully implemented formats: 'dtcg' (canonical W3C JSON) and 'css-vars' (CSS custom properties with mode-aware selectors). For other targets, export to 'dtcg' and either consume the canonical JSON directly or convert it via a downstream tool — or open an issue with your styling stack to prioritize the format.`);
9
+ this.name = "FormatterNotImplementedError";
10
+ }
11
+ }
@@ -0,0 +1,4 @@
1
+ import { FormatterNotImplementedError } from "./stubs.js";
2
+ export function formatStyleDictionaryV3(_doc, _opts) {
3
+ throw new FormatterNotImplementedError("Style Dictionary v3");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { FormatterNotImplementedError } from "./stubs.js";
2
+ export function formatTailwindV3(_doc, _opts) {
3
+ throw new FormatterNotImplementedError("Tailwind v3 config");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { FormatterNotImplementedError } from "./stubs.js";
2
+ export function formatTailwindV4(_doc, _opts) {
3
+ throw new FormatterNotImplementedError("Tailwind v4 @theme");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { FormatterNotImplementedError } from "./stubs.js";
2
+ export function formatTokensStudio(_doc, _opts) {
3
+ throw new FormatterNotImplementedError("Tokens Studio");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { FormatterNotImplementedError } from "./stubs.js";
2
+ export function formatTsModule(_doc, _opts) {
3
+ throw new FormatterNotImplementedError("TypeScript module");
4
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public surface of the token sync engine.
3
+ *
4
+ * Re-exports the canonical types, the parser/formatter dispatchers, the
5
+ * config loader, the alias resolver, and the Figma converter. External
6
+ * callers (tools, tests) should import from here rather than reaching into
7
+ * subdirectories.
8
+ */
9
+ export { FIGMA_MCP_EXTENSION_KEY } from "./types.js";
10
+ export { TokensConfigSchema, loadTokensConfig, findTokensConfig, DEFAULT_TOKENS_CONFIG, buildSuggestedScaffold, resolveOutputTargets, resolveConflictStrategy, } from "./config.js";
11
+ export { ExportTokensInputSchema, ImportTokensInputSchema, ExportFormatSchema, ImportFormatSchema, SyncStrategySchema, ConflictResolutionSchema, } from "./schemas.js";
12
+ export { parse, detectFormat, } from "./parsers/index.js";
13
+ export { format, } from "./formatters/index.js";
14
+ export { buildTokenIndex, resolveReference, validateAliases, formatDtcgReference, parseDtcgReference, } from "./alias-resolver.js";
15
+ export { convertFigmaVariablesToDocument, } from "./figma-converter.js";
@@ -0,0 +1,4 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseCssVars(_input) {
3
+ throw new TokenFormatNotImplementedError("CSS custom properties", "parser");
4
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * DTCG (Design Tokens Community Group) JSON parser.
3
+ *
4
+ * Reads DTCG-spec JSON (https://tr.designtokens.org/format/) and produces our
5
+ * canonical internal TokenDocument. Designed for non-destructive round-trip:
6
+ * a document serialized by `formatDtcg` then parsed back through this module
7
+ * is equal to the original (modulo key ordering, which the formatter sorts
8
+ * for stable diffs).
9
+ *
10
+ * Handles:
11
+ * - Group nesting with arbitrary depth
12
+ * - $value / $type / $description / $extensions on leaf tokens
13
+ * - Alias references: `"$value": "{path.to.token}"`
14
+ * - Group-level $type inheritance per the DTCG spec (tokens without their
15
+ * own $type inherit from their nearest ancestor group that has one)
16
+ * - Our $extensions["figma-console-mcp"] metadata for round-trip ID preservation
17
+ * - Multi-mode tokens stashed in $extensions.modes by our formatter
18
+ */
19
+ import { FIGMA_MCP_EXTENSION_KEY } from "../types.js";
20
+ import { parseDtcgReference } from "../alias-resolver.js";
21
+ const DTCG_TYPES = new Set([
22
+ "color",
23
+ "dimension",
24
+ "fontFamily",
25
+ "fontWeight",
26
+ "duration",
27
+ "cubicBezier",
28
+ "number",
29
+ "string",
30
+ "boolean",
31
+ "shadow",
32
+ "typography",
33
+ "gradient",
34
+ "border",
35
+ "transition",
36
+ "strokeStyle",
37
+ ]);
38
+ export function parseDtcg(input) {
39
+ const warnings = [];
40
+ const root = parseJson(input);
41
+ // Document-level $extensions: pull out our MCP metadata if present.
42
+ // `fileMode` is the critical piece for splitByMode round-trip — when set,
43
+ // every token in the file represents that mode's value, so we label them
44
+ // accordingly instead of falling back to "Default".
45
+ const meta = {};
46
+ let fileMode;
47
+ const rootExt = root.$extensions;
48
+ if (rootExt && typeof rootExt === "object") {
49
+ const mcpMeta = rootExt[FIGMA_MCP_EXTENSION_KEY];
50
+ if (mcpMeta && typeof mcpMeta === "object") {
51
+ const m = mcpMeta;
52
+ if (typeof m.figmaFileKey === "string")
53
+ meta.figmaFileKey = m.figmaFileKey;
54
+ if (typeof m.exportedAt === "string")
55
+ meta.exportedAt = m.exportedAt;
56
+ if (typeof m.mcpVersion === "string")
57
+ meta.mcpVersion = m.mcpVersion;
58
+ if (typeof m.fileMode === "string")
59
+ fileMode = m.fileMode;
60
+ }
61
+ }
62
+ // Each top-level group is a TokenSet. Walk it to extract its tokens.
63
+ const sets = [];
64
+ for (const [setKey, setNode] of Object.entries(root)) {
65
+ if (setKey.startsWith("$"))
66
+ continue; // $extensions etc.
67
+ if (!setNode || typeof setNode !== "object") {
68
+ warnings.push(`Top-level entry "${setKey}" is not a group; skipping. Expected object.`);
69
+ continue;
70
+ }
71
+ sets.push(extractSet(setKey, setNode, warnings, fileMode));
72
+ }
73
+ return {
74
+ document: {
75
+ $schema: "https://figma-console-mcp.southleft.com/schemas/dtcg-extended-v1.json",
76
+ sets,
77
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
78
+ },
79
+ detectedFormat: "dtcg",
80
+ warnings,
81
+ };
82
+ }
83
+ function parseJson(input) {
84
+ try {
85
+ return JSON.parse(input.payload);
86
+ }
87
+ catch (err) {
88
+ throw new Error(`[figma-console-mcp] Failed to parse DTCG JSON${input.sourcePath ? ` at ${input.sourcePath}` : ""}: ${err instanceof Error ? err.message : String(err)}`);
89
+ }
90
+ }
91
+ function extractSet(setKey, setNode, warnings, fileMode) {
92
+ const tokens = [];
93
+ const modes = new Set();
94
+ // Set-level metadata pulled from $extensions["figma-console-mcp"]. We
95
+ // recover the original (un-slugified) set name from `originalName` so
96
+ // round-trip matching works even after slugification.
97
+ const setExt = setNode.$extensions;
98
+ let figmaCollectionId;
99
+ let originalName;
100
+ if (setExt && typeof setExt === "object") {
101
+ const mcp = setExt[FIGMA_MCP_EXTENSION_KEY];
102
+ if (mcp && typeof mcp === "object") {
103
+ const m = mcp;
104
+ if (typeof m.figmaCollectionId === "string") {
105
+ figmaCollectionId = m.figmaCollectionId;
106
+ }
107
+ if (typeof m.originalName === "string") {
108
+ originalName = m.originalName;
109
+ }
110
+ }
111
+ }
112
+ // Walk the set's tree, collecting tokens.
113
+ walkGroup(setNode, [], undefined, tokens, modes, warnings, fileMode);
114
+ return {
115
+ name: originalName ?? setKey,
116
+ description: typeof setNode.$description === "string" ? setNode.$description : undefined,
117
+ modes: modes.size > 0 ? [...modes] : ["Default"],
118
+ tokens,
119
+ meta: figmaCollectionId ? { figmaCollectionId } : undefined,
120
+ };
121
+ }
122
+ function walkGroup(node, path, inheritedType, tokens, modes, warnings, fileMode) {
123
+ // Group-level $type provides inheritance for descendant tokens that lack
124
+ // their own $type, per the DTCG spec.
125
+ const groupType = typeof node.$type === "string" && DTCG_TYPES.has(node.$type)
126
+ ? node.$type
127
+ : inheritedType;
128
+ for (const [key, value] of Object.entries(node)) {
129
+ if (key.startsWith("$"))
130
+ continue;
131
+ if (!value || typeof value !== "object") {
132
+ warnings.push(`Non-group, non-token entry at ${[...path, key].join(".")}; skipping.`);
133
+ continue;
134
+ }
135
+ const childPath = [...path, key];
136
+ if (isLeafToken(value)) {
137
+ const token = extractToken(childPath, value, groupType, warnings, fileMode);
138
+ tokens.push(token);
139
+ for (const mode of Object.keys(token.values))
140
+ modes.add(mode);
141
+ }
142
+ else {
143
+ walkGroup(value, childPath, groupType, tokens, modes, warnings, fileMode);
144
+ }
145
+ }
146
+ }
147
+ function isLeafToken(node) {
148
+ return "$value" in node;
149
+ }
150
+ function extractToken(path, node, inheritedType, warnings, fileMode) {
151
+ const rawType = node.$type;
152
+ let type;
153
+ if (typeof rawType === "string" && DTCG_TYPES.has(rawType)) {
154
+ type = rawType;
155
+ }
156
+ else if (inheritedType) {
157
+ type = inheritedType;
158
+ }
159
+ else {
160
+ type = inferType(node.$value);
161
+ warnings.push(`Token ${path.join(".")} has no $type and no group $type inherited; inferred "${type}".`);
162
+ }
163
+ const description = typeof node.$description === "string" ? node.$description : undefined;
164
+ // Detect multi-mode stashed in $extensions.{FIGMA_MCP_EXTENSION_KEY}.modes
165
+ // (placed there by our own formatter for one-file-multi-mode output).
166
+ const values = {};
167
+ const ext = node.$extensions;
168
+ const mcpExt = ext && typeof ext === "object"
169
+ ? ext[FIGMA_MCP_EXTENSION_KEY]
170
+ : undefined;
171
+ const stashedModes = mcpExt?.modes;
172
+ // Decide which mode name to assign to the primary $value.
173
+ // 1. If the file declares a fileMode (splitByMode output), use that.
174
+ // 2. Otherwise fall back to "Default" — the parser can't know the
175
+ // mode without that hint.
176
+ // Then absorb any stashedModes (one-file-multi-mode output) verbatim.
177
+ const primaryMode = fileMode ?? "Default";
178
+ values[primaryMode] = decodeValue(node.$value);
179
+ if (stashedModes) {
180
+ for (const [modeName, modeValue] of Object.entries(stashedModes)) {
181
+ // Don't overwrite the primary if a stashed entry collides with it.
182
+ if (modeName !== primaryMode) {
183
+ values[modeName] = decodeValue(modeValue);
184
+ }
185
+ }
186
+ }
187
+ // Preserve all other vendor extensions verbatim.
188
+ let extensions;
189
+ if (ext && typeof ext === "object") {
190
+ extensions = {};
191
+ for (const [vendor, payload] of Object.entries(ext)) {
192
+ if (vendor === FIGMA_MCP_EXTENSION_KEY &&
193
+ payload &&
194
+ typeof payload === "object") {
195
+ // Strip the "modes" we already absorbed into values.
196
+ const cleaned = { ...payload };
197
+ delete cleaned.modes;
198
+ if (Object.keys(cleaned).length > 0) {
199
+ extensions[vendor] = cleaned;
200
+ }
201
+ }
202
+ else {
203
+ extensions[vendor] = payload;
204
+ }
205
+ }
206
+ if (Object.keys(extensions).length === 0)
207
+ extensions = undefined;
208
+ }
209
+ return {
210
+ path,
211
+ type,
212
+ description,
213
+ values,
214
+ extensions,
215
+ };
216
+ }
217
+ /**
218
+ * Convert a DTCG $value to our internal TokenValue. Detects alias references
219
+ * (strings of the form `{path.to.token}`) and unwraps them into TokenValue.reference.
220
+ */
221
+ function decodeValue(rawValue) {
222
+ if (typeof rawValue === "string") {
223
+ const refPath = parseDtcgReference(rawValue);
224
+ if (refPath) {
225
+ return { reference: rawValue };
226
+ }
227
+ return { literal: rawValue };
228
+ }
229
+ if (rawValue === null ||
230
+ typeof rawValue === "number" ||
231
+ typeof rawValue === "boolean") {
232
+ return { literal: rawValue ?? "" };
233
+ }
234
+ // Composite values (typography, shadow, etc.) — preserve verbatim.
235
+ return { literal: rawValue };
236
+ }
237
+ function inferType(rawValue) {
238
+ if (typeof rawValue === "string") {
239
+ // Heuristics: color-ish strings → color; px/rem/em → dimension; else string.
240
+ if (/^#[0-9a-f]{3,8}$/i.test(rawValue))
241
+ return "color";
242
+ if (/^(rgb|hsl|oklch)/i.test(rawValue))
243
+ return "color";
244
+ if (/^[\d.]+(px|rem|em|pt|dp)$/i.test(rawValue))
245
+ return "dimension";
246
+ return "string";
247
+ }
248
+ if (typeof rawValue === "number")
249
+ return "number";
250
+ if (typeof rawValue === "boolean")
251
+ return "boolean";
252
+ return "string";
253
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Parser dispatcher. Each parser converts an input format into our canonical
3
+ * internal TokenDocument model.
4
+ */
5
+ import { parseDtcg } from "./dtcg.js";
6
+ import { parseTokensStudio } from "./tokens-studio.js";
7
+ import { parseCssVars } from "./css-vars.js";
8
+ import { parseTailwindV4 } from "./tailwind-v4.js";
9
+ import { parseTailwindV3Config } from "./tailwind-v3.js";
10
+ import { parseScss } from "./scss.js";
11
+ import { parseStyleDictionaryV3 } from "./style-dictionary-v3.js";
12
+ import { parseJsonFlat, parseJsonNested } from "./json.js";
13
+ /**
14
+ * Parse a payload using the given format. When format is 'auto', sniffs the
15
+ * payload to pick the right parser.
16
+ */
17
+ export function parse(format, input) {
18
+ const resolved = format === "auto" ? detectFormat(input) : format;
19
+ switch (resolved) {
20
+ case "dtcg":
21
+ return parseDtcg(input);
22
+ case "tokens-studio":
23
+ return parseTokensStudio(input);
24
+ case "css-vars":
25
+ return parseCssVars(input);
26
+ case "tailwind-v4":
27
+ return parseTailwindV4(input);
28
+ case "tailwind-v3-config":
29
+ return parseTailwindV3Config(input);
30
+ case "scss":
31
+ return parseScss(input);
32
+ case "style-dictionary-v3":
33
+ return parseStyleDictionaryV3(input);
34
+ case "json-flat":
35
+ return parseJsonFlat(input);
36
+ case "json-nested":
37
+ return parseJsonNested(input);
38
+ default: {
39
+ const _exhaustive = resolved;
40
+ throw new Error(`[figma-console-mcp] Unknown import format: ${_exhaustive}`);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Sniff the payload to determine its format. Order matters — earlier checks
46
+ * are higher-priority signals.
47
+ *
48
+ * 1. JSON content with DTCG markers ($value/$type at any depth)
49
+ * 2. JSON content with Tokens Studio markers ($themes.json or $metadata)
50
+ * 3. JSON content with Style Dictionary v3 markers (bare value/type)
51
+ * 4. Tailwind v4 `@theme` block
52
+ * 5. CSS custom properties (`:root { --foo: bar; }`)
53
+ * 6. SCSS variables (`$foo: bar;`)
54
+ * 7. File extension as a last-resort hint
55
+ */
56
+ export function detectFormat(input) {
57
+ const trimmed = input.payload.trim();
58
+ // JSON-shape sniffing
59
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
60
+ try {
61
+ const parsed = JSON.parse(trimmed);
62
+ if (hasDtcgMarkers(parsed))
63
+ return "dtcg";
64
+ if (hasTokensStudioMarkers(parsed))
65
+ return "tokens-studio";
66
+ if (hasStyleDictionaryV3Markers(parsed))
67
+ return "style-dictionary-v3";
68
+ if (looksFlat(parsed))
69
+ return "json-flat";
70
+ return "json-nested";
71
+ }
72
+ catch {
73
+ // Not actually JSON — fall through to text sniffing.
74
+ }
75
+ }
76
+ // Tailwind v4: presence of `@theme` directive
77
+ if (trimmed.includes("@theme"))
78
+ return "tailwind-v4";
79
+ // CSS custom properties
80
+ if (/--[a-z][a-z0-9_-]*\s*:/i.test(trimmed))
81
+ return "css-vars";
82
+ // SCSS variables
83
+ if (/^\s*\$[a-z][a-z0-9_-]*\s*:/im.test(trimmed))
84
+ return "scss";
85
+ // File extension fallback
86
+ if (input.sourcePath) {
87
+ if (input.sourcePath.endsWith(".css"))
88
+ return "css-vars";
89
+ if (input.sourcePath.endsWith(".scss"))
90
+ return "scss";
91
+ if (input.sourcePath.endsWith(".json"))
92
+ return "json-nested";
93
+ }
94
+ throw new Error(`[figma-console-mcp] Unable to auto-detect format for ${input.sourcePath ?? "payload"}. Pass an explicit format to figma_import_tokens.`);
95
+ }
96
+ function hasDtcgMarkers(obj) {
97
+ if (typeof obj !== "object" || obj === null)
98
+ return false;
99
+ for (const key of Object.keys(obj)) {
100
+ const val = obj[key];
101
+ if (key === "$value" || key === "$type")
102
+ return true;
103
+ if (val && typeof val === "object" && hasDtcgMarkers(val))
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+ function hasTokensStudioMarkers(obj) {
109
+ if (typeof obj !== "object" || obj === null)
110
+ return false;
111
+ const top = obj;
112
+ // Tokens Studio's signature: presence of $themes or $metadata at the root,
113
+ // or selectedTokenSets nested inside the document.
114
+ return ("$themes" in top ||
115
+ "$metadata" in top ||
116
+ JSON.stringify(top).includes("selectedTokenSets"));
117
+ }
118
+ function hasStyleDictionaryV3Markers(obj) {
119
+ if (typeof obj !== "object" || obj === null)
120
+ return false;
121
+ // SD v3 uses bare "value" and "type" fields (no $ prefix) on leaf nodes.
122
+ for (const val of Object.values(obj)) {
123
+ if (val && typeof val === "object") {
124
+ const inner = val;
125
+ if ("value" in inner && ("type" in inner || typeof inner.value !== "object")) {
126
+ return true;
127
+ }
128
+ if (hasStyleDictionaryV3Markers(val))
129
+ return true;
130
+ }
131
+ }
132
+ return false;
133
+ }
134
+ function looksFlat(obj) {
135
+ if (typeof obj !== "object" || obj === null)
136
+ return false;
137
+ return Object.values(obj).every((v) => typeof v !== "object" || v === null);
138
+ }
@@ -0,0 +1,7 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseJsonFlat(_input) {
3
+ throw new TokenFormatNotImplementedError("flat JSON", "parser");
4
+ }
5
+ export function parseJsonNested(_input) {
6
+ throw new TokenFormatNotImplementedError("nested JSON", "parser");
7
+ }
@@ -0,0 +1,4 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseScss(_input) {
3
+ throw new TokenFormatNotImplementedError("SCSS variables", "parser");
4
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared NotImplementedError for parsers/formatters that are scaffolded but
3
+ * not yet implemented. DTCG round-trip is the canonical fully-implemented
4
+ * path; CSS variables is also fully implemented as a formatter output.
5
+ * Everything else returns a helpful error pointing the user at the canonical
6
+ * format.
7
+ */
8
+ export class TokenFormatNotImplementedError extends Error {
9
+ constructor(formatName, kind) {
10
+ super(`[figma-console-mcp] The ${formatName} ${kind} is scaffolded but not yet implemented. Fully implemented: DTCG JSON (parser + formatter) and CSS custom properties (formatter only). For now, export to 'dtcg' as the canonical format and either consume the JSON directly or convert downstream — or open an issue with your use case to prioritize this format.`);
11
+ this.name = "TokenFormatNotImplementedError";
12
+ }
13
+ }
@@ -0,0 +1,4 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseStyleDictionaryV3(_input) {
3
+ throw new TokenFormatNotImplementedError("Style Dictionary v3", "parser");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseTailwindV3Config(_input) {
3
+ throw new TokenFormatNotImplementedError("Tailwind v3 config", "parser");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseTailwindV4(_input) {
3
+ throw new TokenFormatNotImplementedError("Tailwind v4 @theme", "parser");
4
+ }
@@ -0,0 +1,4 @@
1
+ import { TokenFormatNotImplementedError } from "./stubs.js";
2
+ export function parseTokensStudio(_input) {
3
+ throw new TokenFormatNotImplementedError("Tokens Studio", "parser");
4
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Zod schemas for the figma_export_tokens and figma_import_tokens MCP tools.
3
+ *
4
+ * Kept in a dedicated file because they're the AI-facing surface of the token
5
+ * sync engine — the descriptions matter for prompt comprehension, and they
6
+ * need to stay in sync with `src/core/tokens/types.ts` (the internal model).
7
+ */
8
+ import { z } from "zod";
9
+ /**
10
+ * Output format enum mirrors `ExportFormat` from `./types.ts`. Listed in the
11
+ * same priority order — DTCG and Tokens Studio first as the canonical JSON
12
+ * outputs, then CSS-family formats, then code modules, then back-compat.
13
+ */
14
+ export const ExportFormatSchema = z.enum([
15
+ "dtcg",
16
+ "tokens-studio",
17
+ "css-vars",
18
+ "tailwind-v4",
19
+ "tailwind-v3",
20
+ "scss",
21
+ "less",
22
+ "ts-module",
23
+ "json-flat",
24
+ "json-nested",
25
+ "style-dictionary-v3",
26
+ ]);
27
+ export const ImportFormatSchema = z.enum([
28
+ "auto",
29
+ "dtcg",
30
+ "tokens-studio",
31
+ "css-vars",
32
+ "tailwind-v4",
33
+ "tailwind-v3-config",
34
+ "scss",
35
+ "style-dictionary-v3",
36
+ "json-flat",
37
+ "json-nested",
38
+ ]);
39
+ export const SyncStrategySchema = z.enum(["merge", "replace", "dry-run"]);
40
+ export const ConflictResolutionSchema = z.enum([
41
+ "ask",
42
+ "figma-wins",
43
+ "code-wins",
44
+ "skip",
45
+ ]);
46
+ const ColorFormatSchema = z.enum(["hex", "hex8", "rgba", "oklch", "hsl"]);
47
+ const SizeUnitSchema = z.enum(["px", "rem", "pt", "dp"]);
48
+ /**
49
+ * Schema for figma_export_tokens. Most fields are optional — the typical call
50
+ * is zero-arg, picking everything up from `tokens.config.json` autodiscovery.
51
+ */
52
+ export const ExportTokensInputSchema = z.object({
53
+ scope: z
54
+ .enum(["file", "collection"])
55
+ .optional()
56
+ .describe("Whether to export the entire file's variables ('file', default) or just specific collections via collectionIds."),
57
+ collectionIds: z
58
+ .array(z.string())
59
+ .optional()
60
+ .describe("Specific Figma collection IDs to export. Required when scope is 'collection'. Use figma_get_variables to enumerate available collections."),
61
+ modes: z
62
+ .union([z.array(z.string()), z.literal("all")])
63
+ .optional()
64
+ .describe("Modes to include in the output. 'all' (default) exports every mode in every collection. Pass an array like ['Light', 'Dark'] to filter."),
65
+ format: ExportFormatSchema.optional().describe("Specific output format to emit. When omitted, formats come from tokens.config.json's generated.formats list. Common starting choices: 'dtcg' for the canonical JSON, 'css-vars' for runtime CSS custom properties, 'tailwind-v4' for Tailwind v4 @theme blocks."),
66
+ outputPath: z
67
+ .string()
68
+ .optional()
69
+ .describe("Filesystem path to write the output file(s) to. Relative paths resolve against the project root (the directory containing tokens.config.json) or cwd if no config. When omitted, the output is returned inline in the response (suitable for the AI to inspect or write via its own file tools)."),
70
+ configPath: z
71
+ .string()
72
+ .optional()
73
+ .describe("Explicit path to a tokens.config.json file. When omitted, the tool walks up from cwd looking for one — typical case is zero-arg."),
74
+ strategy: SyncStrategySchema.optional().describe("How to handle existing output files. 'merge' (default) diffs against current contents and writes only changed tokens, preserving code-only additions. 'replace' wipes and rewrites. 'dry-run' computes the diff and reports what would change without writing."),
75
+ prefix: z
76
+ .string()
77
+ .optional()
78
+ .describe("Prefix prepended to every output token name (e.g. 'ds-', 'al-'). Only affects formatters that emit named tokens — DTCG and JSON outputs use unmodified paths."),
79
+ resolveAliases: z
80
+ .boolean()
81
+ .optional()
82
+ .describe("If true, alias references are resolved to literal values in the output. Default is false for JSON formats (preserves alias semantics) and true for CSS/SCSS/Tailwind/etc. (which can't natively express aliases)."),
83
+ splitByMode: z
84
+ .boolean()
85
+ .optional()
86
+ .describe("Emit one file per mode (e.g. tokens-light.css, tokens-dark.css). Default false (single file with all modes)."),
87
+ splitByCollection: z
88
+ .boolean()
89
+ .optional()
90
+ .describe("Emit one file per Figma collection. Default false. Useful when collections map to different runtime themes."),
91
+ colorFormat: ColorFormatSchema.optional().describe("Color value format in the output. Default: 'hex'. Use 'oklch' for modern Tailwind v4 charts."),
92
+ sizeUnit: SizeUnitSchema.optional().describe("Unit for dimension tokens. Default: 'rem' for web outputs, 'pt' for iOS, 'dp' for Android."),
93
+ remBase: z
94
+ .number()
95
+ .positive()
96
+ .optional()
97
+ .describe("Base font size in pixels for px→rem conversion. Default: 16."),
98
+ });
99
+ /**
100
+ * Schema for figma_import_tokens. Mirrors export's shape on the inverse
101
+ * direction: instead of producing files, this consumes payloads or files and
102
+ * pushes the diff to Figma.
103
+ */
104
+ export const ImportTokensInputSchema = z
105
+ .object({
106
+ format: ImportFormatSchema.optional().describe("Format of the input payload. 'auto' (default) detects from payload shape or file extension. Pass an explicit format if auto-detection misfires."),
107
+ payload: z
108
+ .string()
109
+ .optional()
110
+ .describe("Single-file content to import. Use this for one-shot imports without setting up tokens.config.json. Mutually exclusive with `files` and `configPath`."),
111
+ files: z
112
+ .array(z.object({
113
+ path: z.string().describe("Relative or absolute filesystem path."),
114
+ content: z.string().describe("File contents."),
115
+ }))
116
+ .optional()
117
+ .describe("Multi-file import (used for Tokens Studio's split-set format, or for projects with many DTCG source files). Mutually exclusive with `payload` and `configPath`."),
118
+ configPath: z
119
+ .string()
120
+ .optional()
121
+ .describe("Explicit path to tokens.config.json. When omitted, the tool autodiscovers and uses the config's source.dir to find files. Mutually exclusive with `payload` and `files`."),
122
+ strategy: SyncStrategySchema.optional().describe("How to apply changes. 'merge' (default) diffs against current Figma state and applies only deltas, preserving Figma-only variables. 'replace' wipes the target collections and rewrites. 'dry-run' computes the diff and reports without touching Figma."),
123
+ collectionMapping: z
124
+ .record(z.string())
125
+ .optional()
126
+ .describe("Map input token set names to Figma collection names. Example: {'primitives': 'Primitive Tokens'}. When omitted, set names map 1:1 to collection names."),
127
+ modeMapping: z
128
+ .record(z.string())
129
+ .optional()
130
+ .describe("Map input mode names to Figma mode names. Useful when source uses 'light'/'dark' and Figma uses 'Light'/'Dark'. Defaults to 1:1 mapping with case preservation."),
131
+ prefix: z
132
+ .string()
133
+ .optional()
134
+ .describe("Prefix to strip from input token names on import. E.g. with prefix 'ds-', a token named '--ds-color-primary' becomes 'color/primary'."),
135
+ onConflict: ConflictResolutionSchema.optional().describe("How to resolve true two-sided conflicts (both Figma and code changed the same token since last sync). 'ask' (default) surfaces the conflict and writes nothing. 'figma-wins' / 'code-wins' apply the corresponding side. 'skip' leaves conflicted tokens alone but proceeds with the rest."),
136
+ dryRun: z
137
+ .boolean()
138
+ .optional()
139
+ .describe("Shorthand for strategy: 'dry-run'. Computes the diff and returns a preview without applying any changes to Figma."),
140
+ })
141
+ .refine((data) => {
142
+ // Exactly one of payload / files / configPath should be set (or none,
143
+ // which triggers tokens.config.json autodiscovery).
144
+ const sources = [data.payload, data.files, data.configPath].filter((x) => x !== undefined);
145
+ return sources.length <= 1;
146
+ }, {
147
+ message: "Pass at most one of: payload, files, configPath. (Or none, to autodiscover tokens.config.json.)",
148
+ });