@mp3wizard/figma-console-mcp 1.25.1 → 1.28.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 (207) hide show
  1. package/README.md +53 -35
  2. package/dist/apps/design-system-dashboard/mcp-app.html +78 -78
  3. package/dist/apps/token-browser/mcp-app.html +60 -59
  4. package/dist/cloudflare/core/config.js +0 -8
  5. package/dist/cloudflare/core/console-monitor.js +3 -3
  6. package/dist/cloudflare/core/diagnose-tool.js +96 -0
  7. package/dist/cloudflare/core/figma-tools.js +69 -229
  8. package/dist/cloudflare/core/identity.js +96 -0
  9. package/dist/cloudflare/core/tokens/alias-resolver.js +135 -0
  10. package/dist/cloudflare/core/tokens/config.js +284 -0
  11. package/dist/cloudflare/core/tokens/figma-converter.js +195 -0
  12. package/dist/cloudflare/core/tokens/formatters/css-vars.js +329 -0
  13. package/dist/cloudflare/core/tokens/formatters/dtcg.js +300 -0
  14. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  15. package/dist/cloudflare/core/tokens/formatters/json.js +187 -0
  16. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  17. package/dist/cloudflare/core/tokens/formatters/scss.js +252 -0
  18. package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
  19. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +207 -0
  20. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
  21. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +330 -0
  22. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +250 -0
  23. package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
  24. package/dist/cloudflare/core/tokens/index.js +15 -0
  25. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  26. package/dist/cloudflare/core/tokens/parsers/dtcg.js +253 -0
  27. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  28. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  29. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  30. package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
  31. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  32. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  33. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  34. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  35. package/dist/cloudflare/core/tokens/schemas.js +148 -0
  36. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  37. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  38. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  39. package/dist/cloudflare/core/tokens/types.js +18 -0
  40. package/dist/cloudflare/core/tokens-tools.js +859 -0
  41. package/dist/cloudflare/core/websocket-server.js +5 -55
  42. package/dist/cloudflare/index.js +37 -26
  43. package/dist/core/config.d.ts.map +1 -1
  44. package/dist/core/config.js +0 -8
  45. package/dist/core/config.js.map +1 -1
  46. package/dist/core/console-monitor.d.ts +2 -2
  47. package/dist/core/console-monitor.d.ts.map +1 -1
  48. package/dist/core/console-monitor.js +3 -3
  49. package/dist/core/console-monitor.js.map +1 -1
  50. package/dist/core/diagnose-tool.d.ts +33 -0
  51. package/dist/core/diagnose-tool.d.ts.map +1 -0
  52. package/dist/core/diagnose-tool.js +97 -0
  53. package/dist/core/diagnose-tool.js.map +1 -0
  54. package/dist/core/figma-connector.d.ts +1 -1
  55. package/dist/core/figma-connector.d.ts.map +1 -1
  56. package/dist/core/figma-tools.d.ts +1 -2
  57. package/dist/core/figma-tools.d.ts.map +1 -1
  58. package/dist/core/figma-tools.js +69 -229
  59. package/dist/core/figma-tools.js.map +1 -1
  60. package/dist/core/identity.d.ts +41 -0
  61. package/dist/core/identity.d.ts.map +1 -0
  62. package/dist/core/identity.js +97 -0
  63. package/dist/core/identity.js.map +1 -0
  64. package/dist/core/tokens/alias-resolver.d.ts +55 -0
  65. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  66. package/dist/core/tokens/alias-resolver.js +136 -0
  67. package/dist/core/tokens/alias-resolver.js.map +1 -0
  68. package/dist/core/tokens/config.d.ts +352 -0
  69. package/dist/core/tokens/config.d.ts.map +1 -0
  70. package/dist/core/tokens/config.js +285 -0
  71. package/dist/core/tokens/config.js.map +1 -0
  72. package/dist/core/tokens/figma-converter.d.ts +81 -0
  73. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  74. package/dist/core/tokens/figma-converter.js +196 -0
  75. package/dist/core/tokens/figma-converter.js.map +1 -0
  76. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  77. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  78. package/dist/core/tokens/formatters/css-vars.js +330 -0
  79. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  80. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  81. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  82. package/dist/core/tokens/formatters/dtcg.js +301 -0
  83. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  84. package/dist/core/tokens/formatters/index.d.ts +30 -0
  85. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  86. package/dist/core/tokens/formatters/index.js +46 -0
  87. package/dist/core/tokens/formatters/index.js.map +1 -0
  88. package/dist/core/tokens/formatters/json.d.ts +37 -0
  89. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  90. package/dist/core/tokens/formatters/json.js +188 -0
  91. package/dist/core/tokens/formatters/json.js.map +1 -0
  92. package/dist/core/tokens/formatters/less.d.ts +4 -0
  93. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  94. package/dist/core/tokens/formatters/less.js +5 -0
  95. package/dist/core/tokens/formatters/less.js.map +1 -0
  96. package/dist/core/tokens/formatters/scss.d.ts +26 -0
  97. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  98. package/dist/core/tokens/formatters/scss.js +253 -0
  99. package/dist/core/tokens/formatters/scss.js.map +1 -0
  100. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  101. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  102. package/dist/core/tokens/formatters/stubs.js +14 -0
  103. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  104. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
  105. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  106. package/dist/core/tokens/formatters/style-dictionary-v3.js +208 -0
  107. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  108. package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
  109. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  110. package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
  111. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  112. package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
  113. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  114. package/dist/core/tokens/formatters/tailwind-v4.js +331 -0
  115. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  116. package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
  117. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  118. package/dist/core/tokens/formatters/tokens-studio.js +251 -0
  119. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  120. package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
  121. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  122. package/dist/core/tokens/formatters/ts-module.js +199 -0
  123. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  124. package/dist/core/tokens/index.d.ts +17 -0
  125. package/dist/core/tokens/index.d.ts.map +1 -0
  126. package/dist/core/tokens/index.js +16 -0
  127. package/dist/core/tokens/index.js.map +1 -0
  128. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  129. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  130. package/dist/core/tokens/parsers/css-vars.js +5 -0
  131. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  132. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  133. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  134. package/dist/core/tokens/parsers/dtcg.js +254 -0
  135. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  136. package/dist/core/tokens/parsers/index.d.ts +37 -0
  137. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  138. package/dist/core/tokens/parsers/index.js +139 -0
  139. package/dist/core/tokens/parsers/index.js.map +1 -0
  140. package/dist/core/tokens/parsers/json.d.ts +4 -0
  141. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  142. package/dist/core/tokens/parsers/json.js +8 -0
  143. package/dist/core/tokens/parsers/json.js.map +1 -0
  144. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  145. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  146. package/dist/core/tokens/parsers/scss.js +5 -0
  147. package/dist/core/tokens/parsers/scss.js.map +1 -0
  148. package/dist/core/tokens/parsers/stubs.d.ts +15 -0
  149. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  150. package/dist/core/tokens/parsers/stubs.js +21 -0
  151. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  152. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  153. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  154. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  155. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  156. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  157. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  158. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  159. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  160. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  161. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  162. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  163. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  164. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  165. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  166. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  167. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  168. package/dist/core/tokens/schemas.d.ts +152 -0
  169. package/dist/core/tokens/schemas.d.ts.map +1 -0
  170. package/dist/core/tokens/schemas.js +149 -0
  171. package/dist/core/tokens/schemas.js.map +1 -0
  172. package/dist/core/tokens/transforms/color.d.ts +9 -0
  173. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  174. package/dist/core/tokens/transforms/color.js +13 -0
  175. package/dist/core/tokens/transforms/color.js.map +1 -0
  176. package/dist/core/tokens/transforms/index.d.ts +36 -0
  177. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  178. package/dist/core/tokens/transforms/index.js +30 -0
  179. package/dist/core/tokens/transforms/index.js.map +1 -0
  180. package/dist/core/tokens/transforms/size.d.ts +7 -0
  181. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  182. package/dist/core/tokens/transforms/size.js +8 -0
  183. package/dist/core/tokens/transforms/size.js.map +1 -0
  184. package/dist/core/tokens/types.d.ts +228 -0
  185. package/dist/core/tokens/types.d.ts.map +1 -0
  186. package/dist/core/tokens/types.js +19 -0
  187. package/dist/core/tokens/types.js.map +1 -0
  188. package/dist/core/tokens-tools.d.ts +42 -0
  189. package/dist/core/tokens-tools.d.ts.map +1 -0
  190. package/dist/core/tokens-tools.js +860 -0
  191. package/dist/core/tokens-tools.js.map +1 -0
  192. package/dist/core/types/index.d.ts +0 -8
  193. package/dist/core/types/index.d.ts.map +1 -1
  194. package/dist/core/websocket-connector.d.ts +1 -1
  195. package/dist/core/websocket-connector.d.ts.map +1 -1
  196. package/dist/core/websocket-server.d.ts +4 -3
  197. package/dist/core/websocket-server.d.ts.map +1 -1
  198. package/dist/core/websocket-server.js +5 -55
  199. package/dist/core/websocket-server.js.map +1 -1
  200. package/dist/local.d.ts +0 -12
  201. package/dist/local.d.ts.map +1 -1
  202. package/dist/local.js +959 -3406
  203. package/dist/local.js.map +1 -1
  204. package/figma-desktop-bridge/code.js +11 -63
  205. package/figma-desktop-bridge/ui.html +72 -11
  206. package/package.json +27 -12
  207. package/figma-desktop-bridge/ui-full.html +0 -1353
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Server identity helpers.
3
+ *
4
+ * When a user has multiple Figma-related MCP servers configured at once
5
+ * (e.g. figma-console-mcp alongside Figma's native codegen MCP), an LLM can
6
+ * conflate errors from one server with the troubleshooting copy of another —
7
+ * producing remediation advice that points at the wrong tool. Tagging our
8
+ * responses with an explicit `[figma-console-mcp]` prefix and an `_mcp`
9
+ * field makes attribution unambiguous.
10
+ */
11
+ export const MCP_NAME = "figma-console-mcp";
12
+ export const ERROR_PREFIX = `[${MCP_NAME}]`;
13
+ /**
14
+ * Prefix a thrown-error message with our MCP identity so cross-tool errors
15
+ * can't be mistakenly attributed to this server.
16
+ */
17
+ export function identifiedError(message) {
18
+ return new Error(`${ERROR_PREFIX} ${message}`);
19
+ }
20
+ /**
21
+ * Tag a response payload with our MCP identity at the top level.
22
+ * The `_mcp` field is read by LLMs alongside the rest of the response and
23
+ * gives them a reliable signal for "which server produced this output".
24
+ */
25
+ export function withIdentity(data) {
26
+ return { _mcp: MCP_NAME, ...data };
27
+ }
28
+ /**
29
+ * Tag every text/JSON content block in a tool response with our MCP identity.
30
+ * Idempotent — already-tagged content (from adaptiveResponse or explicit
31
+ * withIdentity calls) is left alone. Non-JSON text content is left alone.
32
+ */
33
+ function tagToolResponse(result) {
34
+ if (!result ||
35
+ typeof result !== "object" ||
36
+ !("content" in result) ||
37
+ !Array.isArray(result.content)) {
38
+ return result;
39
+ }
40
+ const r = result;
41
+ const newContent = r.content.map((item) => {
42
+ if (item.type !== "text" || typeof item.text !== "string")
43
+ return item;
44
+ try {
45
+ const parsed = JSON.parse(item.text);
46
+ if (parsed &&
47
+ typeof parsed === "object" &&
48
+ !Array.isArray(parsed) &&
49
+ !("_mcp" in parsed)) {
50
+ return { ...item, text: JSON.stringify({ _mcp: MCP_NAME, ...parsed }) };
51
+ }
52
+ }
53
+ catch {
54
+ // Not JSON — leave the text untouched (e.g. AI instruction blocks
55
+ // emitted by adaptiveResponse, or plain-text error messages).
56
+ }
57
+ return item;
58
+ });
59
+ return { ...r, content: newContent };
60
+ }
61
+ /**
62
+ * Monkey-patch an MCP server instance so every tool registered on it gets
63
+ * identity tagging applied to its responses and an identity prefix on any
64
+ * Error it throws — without modifying the ~97 individual tool handlers.
65
+ *
66
+ * Call this once, immediately after constructing the McpServer, BEFORE any
67
+ * tool registration calls run. The wrap is idempotent at the response level
68
+ * (tools that already tag themselves via withIdentity or adaptiveResponse
69
+ * won't get double-tagged).
70
+ *
71
+ * Adds attribution coverage to every response path uniformly — see
72
+ * project_lauren_cross_mcp_confusion for why this matters.
73
+ */
74
+ export function wrapServerForIdentity(server) {
75
+ const target = server;
76
+ const originalTool = target.tool.bind(target);
77
+ target.tool = function (...args) {
78
+ if (args.length === 0 || typeof args[args.length - 1] !== "function") {
79
+ return originalTool(...args);
80
+ }
81
+ const handler = args[args.length - 1];
82
+ const wrappedHandler = async (...handlerArgs) => {
83
+ try {
84
+ const result = await handler(...handlerArgs);
85
+ return tagToolResponse(result);
86
+ }
87
+ catch (err) {
88
+ if (err instanceof Error && !err.message.startsWith(ERROR_PREFIX)) {
89
+ err.message = `${ERROR_PREFIX} ${err.message}`;
90
+ }
91
+ throw err;
92
+ }
93
+ };
94
+ return originalTool(...args.slice(0, -1), wrappedHandler);
95
+ };
96
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Alias reference resolution and validation.
3
+ *
4
+ * DTCG alias references look like `{color.primary}` or `{tier-1.color.blue.500}`.
5
+ * They can chain (an alias can reference another alias), but cycles are an
6
+ * error.
7
+ *
8
+ * This module:
9
+ * - Resolves an alias to its eventual literal value (for formatters that
10
+ * can't natively express references — CSS, SCSS, Tailwind).
11
+ * - Validates that every alias points to an existing token.
12
+ * - Detects cycles.
13
+ */
14
+ /**
15
+ * Build a lookup map from dot-path strings (e.g. "color.primary") to Token
16
+ * objects. Used as the index for resolveAliases().
17
+ */
18
+ export function buildTokenIndex(doc) {
19
+ const index = new Map();
20
+ for (const set of doc.sets) {
21
+ for (const token of set.tokens) {
22
+ const key = token.path.join(".");
23
+ index.set(key, token);
24
+ }
25
+ }
26
+ return index;
27
+ }
28
+ /**
29
+ * Resolve a single alias reference. Returns the eventual literal value, or
30
+ * throws if the reference is unresolvable or cyclic.
31
+ */
32
+ export function resolveReference(reference, mode, index, seen = new Set()) {
33
+ // Strip the curly braces if present: "{color.primary}" → "color.primary"
34
+ const path = reference.replace(/^\{|\}$/g, "");
35
+ if (seen.has(path)) {
36
+ throw new Error(`[figma-console-mcp] Alias cycle detected: ${[...seen, path].join(" → ")}`);
37
+ }
38
+ seen.add(path);
39
+ const target = index.get(path);
40
+ if (!target) {
41
+ throw new Error(`[figma-console-mcp] Unresolvable alias reference: {${path}}`);
42
+ }
43
+ // Find the value for the requested mode; fall back to the only mode if
44
+ // there's just one (alias's target may be single-mode while the source
45
+ // is multi-mode, or vice versa).
46
+ const value = target.values[mode] ??
47
+ (Object.keys(target.values).length === 1
48
+ ? Object.values(target.values)[0]
49
+ : undefined);
50
+ if (!value) {
51
+ throw new Error(`[figma-console-mcp] Alias {${path}} has no value for mode "${mode}"`);
52
+ }
53
+ // Chain: if the target's value is itself an alias, recurse.
54
+ if (value.reference) {
55
+ return resolveReference(value.reference, mode, index, seen);
56
+ }
57
+ return value;
58
+ }
59
+ /**
60
+ * Resolve an alias chain to its final literal value, walking through
61
+ * intermediate alias hops. Returns the final TokenValue (with `literal` set
62
+ * if resolution succeeded) or `null` if the chain ends at a cross-library
63
+ * reference / unresolvable target / cycle.
64
+ *
65
+ * Used by formatters that can't natively express alias references in their
66
+ * output (Tailwind v3, TypeScript modules, plain JSON) — those need literal
67
+ * values at export time.
68
+ *
69
+ * Safer counterpart of `resolveReference` because it swallows errors
70
+ * (unresolvable / cycle) into `null` rather than throwing; formatters can
71
+ * then emit a comment or skip the token instead of failing the whole export.
72
+ */
73
+ export function resolveAliasChain(value, mode, index) {
74
+ if (!value.reference)
75
+ return value;
76
+ // Cross-library aliases are not resolvable — formatters should skip with a comment.
77
+ const bare = value.reference.replace(/^\{|\}$/g, "");
78
+ if (bare.startsWith("__library:") || bare === "unknown")
79
+ return null;
80
+ try {
81
+ const resolved = resolveReference(value.reference, mode, index);
82
+ // resolveReference throws on cycles / unresolvable, so a returned value
83
+ // is either a literal or another reference. If still a reference,
84
+ // recurse (defensive — resolveReference already chases chains, but the
85
+ // top-level call may return a value with `reference` if mode-fallback
86
+ // routes through an aliased entry).
87
+ if (resolved.reference) {
88
+ return resolveAliasChain(resolved, mode, index);
89
+ }
90
+ return resolved;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * Validate every alias in the document. Returns a list of error messages —
98
+ * empty array means all aliases resolve cleanly.
99
+ */
100
+ export function validateAliases(doc) {
101
+ const index = buildTokenIndex(doc);
102
+ const errors = [];
103
+ for (const set of doc.sets) {
104
+ for (const token of set.tokens) {
105
+ for (const [mode, value] of Object.entries(token.values)) {
106
+ if (value.reference) {
107
+ try {
108
+ resolveReference(value.reference, mode, index);
109
+ }
110
+ catch (err) {
111
+ errors.push(`${token.path.join(".")} (mode "${mode}"): ${err instanceof Error ? err.message : String(err)}`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return errors;
118
+ }
119
+ /**
120
+ * Format an alias reference for DTCG output. DTCG uses `{path.to.token}`
121
+ * syntax with curly braces.
122
+ */
123
+ export function formatDtcgReference(referencePath) {
124
+ return `{${referencePath.join(".")}}`;
125
+ }
126
+ /**
127
+ * Parse a DTCG alias string back into a path array. Returns null if the
128
+ * string isn't an alias reference.
129
+ */
130
+ export function parseDtcgReference(s) {
131
+ const match = s.match(/^\{([^}]+)\}$/);
132
+ if (!match)
133
+ return null;
134
+ return match[1].split(".");
135
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * tokens.config.json schema, loader, and autodiscovery for the figma-console-mcp
3
+ * token sync engine.
4
+ *
5
+ * Both figma_export_tokens and figma_import_tokens read this single config so
6
+ * follow-up calls in a project are zero-arg. Autodiscovery walks up from the
7
+ * current working directory looking for `tokens.config.json` at each level
8
+ * — same convention as `tsconfig.json`, `package.json`, `.eslintrc`, etc.
9
+ */
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { z } from "zod";
13
+ /**
14
+ * Schema for a single output target in `tokens.config.json`. Each entry
15
+ * produces one or more files when figma_export_tokens runs.
16
+ */
17
+ const OutputTargetSchema = z.object({
18
+ format: z.enum([
19
+ "dtcg",
20
+ "tokens-studio",
21
+ "css-vars",
22
+ "tailwind-v4",
23
+ "tailwind-v3",
24
+ "scss",
25
+ "less",
26
+ "ts-module",
27
+ "json-flat",
28
+ "json-nested",
29
+ "style-dictionary-v3",
30
+ ]),
31
+ /** Optional filename override. Default is derived from format + scope. */
32
+ filename: z.string().optional(),
33
+ /** Output prefix applied to every token name (e.g. "ds-", "al-"). */
34
+ prefix: z.string().optional(),
35
+ /** Emit one file per mode (e.g. tokens-light.css, tokens-dark.css). */
36
+ splitByMode: z.boolean().optional(),
37
+ /** Emit one file per token set / Figma collection. */
38
+ splitByCollection: z.boolean().optional(),
39
+ /**
40
+ * If true, alias references are resolved to literal values in the output.
41
+ * If false, aliases are preserved (default for JSON formats, forced true
42
+ * for CSS/SCSS/Tailwind/etc. since they can't natively express aliases).
43
+ */
44
+ resolveAliases: z.boolean().optional(),
45
+ /** Per-target transform options. Override the global defaults. */
46
+ transforms: z
47
+ .object({
48
+ colorFormat: z.enum(["hex", "hex8", "rgba", "oklch", "hsl"]).optional(),
49
+ sizeUnit: z.enum(["px", "rem", "pt", "dp"]).optional(),
50
+ remBase: z.number().positive().optional(),
51
+ })
52
+ .optional(),
53
+ });
54
+ /**
55
+ * Full schema for `tokens.config.json`. Every field is optional so the
56
+ * minimum-viable config is `{ "figmaFile": "..." }` — the rest gets sensible
57
+ * defaults.
58
+ */
59
+ export const TokensConfigSchema = z
60
+ .object({
61
+ /** Optional JSON Schema URL for editor autocompletion. */
62
+ $schema: z.string().optional(),
63
+ /**
64
+ * Figma file URL or fileKey. When omitted, tools fall back to the
65
+ * currently-connected Desktop Bridge plugin's file (Local Mode) or the
66
+ * file context bound by figma_pair_plugin (Cloud Mode).
67
+ */
68
+ figmaFile: z.string().optional(),
69
+ /** Where the canonical (committed) token sources live. */
70
+ source: z
71
+ .object({
72
+ /** Directory holding the canonical token files. */
73
+ dir: z.string(),
74
+ /** Glob pattern within dir. Default: "*.tokens.json" */
75
+ pattern: z.string().optional(),
76
+ /**
77
+ * Canonical format for source files. DTCG is the recommended default;
78
+ * Tokens Studio is supported for users who already have a `$themes.json`
79
+ * setup (e.g. Altitude).
80
+ */
81
+ canonical: z.enum(["dtcg", "tokens-studio"]).default("dtcg"),
82
+ })
83
+ .default({ dir: "src/styles/tokens", canonical: "dtcg" }),
84
+ /** Where build outputs (CSS, Tailwind, etc.) get written. */
85
+ generated: z
86
+ .object({
87
+ dir: z.string().default("src/styles/generated"),
88
+ formats: z.array(OutputTargetSchema).default([]),
89
+ })
90
+ .optional(),
91
+ /** Mode name mappings (Figma mode name → output mode name). */
92
+ modes: z
93
+ .object({
94
+ /** e.g. { "Light": "light", "Dark": "dark" } */
95
+ map: z.record(z.string()).optional(),
96
+ /** Default mode if a token has no explicit mode (e.g. "Light"). */
97
+ default: z.string().optional(),
98
+ })
99
+ .optional(),
100
+ /** Default conflict-resolution strategy when not specified per-call. */
101
+ conflictResolution: z
102
+ .enum(["ask", "figma-wins", "code-wins", "skip"])
103
+ .default("ask"),
104
+ /** Behavior for tokens that exist on one side but not the other. */
105
+ sync: z
106
+ .object({
107
+ onMissingInCode: z
108
+ .enum(["preserve", "delete", "warn"])
109
+ .default("preserve"),
110
+ onMissingInFigma: z
111
+ .enum(["preserve", "delete", "warn"])
112
+ .default("preserve"),
113
+ })
114
+ .optional(),
115
+ })
116
+ .strict();
117
+ /**
118
+ * Walk up from `startDir` looking for `tokens.config.json`. Returns the first
119
+ * match, or `null` if none found by the filesystem root.
120
+ */
121
+ export function findTokensConfig(startDir) {
122
+ let dir = resolve(startDir);
123
+ // Hard cap on directory traversal so a misconfigured startDir can't loop
124
+ // forever (defense against symlinks/weird filesystems).
125
+ const maxDepth = 32;
126
+ for (let i = 0; i < maxDepth; i++) {
127
+ const candidate = join(dir, "tokens.config.json");
128
+ if (existsSync(candidate)) {
129
+ return candidate;
130
+ }
131
+ const parent = dirname(dir);
132
+ if (parent === dir) {
133
+ // Hit filesystem root, no config found.
134
+ return null;
135
+ }
136
+ dir = parent;
137
+ }
138
+ return null;
139
+ }
140
+ /**
141
+ * Load and validate `tokens.config.json`. If `explicitPath` is provided, uses
142
+ * that; otherwise autodiscovers by walking up from `cwd` (default
143
+ * `process.cwd()`).
144
+ *
145
+ * Returns `null` if no config is found AND no explicit path was given. Throws
146
+ * if an explicit path doesn't exist, or if the discovered file fails schema
147
+ * validation.
148
+ */
149
+ export function loadTokensConfig(opts = {}) {
150
+ const cwd = opts.cwd ?? process.cwd();
151
+ const configPath = opts.explicitPath
152
+ ? resolve(opts.explicitPath)
153
+ : findTokensConfig(cwd);
154
+ if (!configPath)
155
+ return null;
156
+ if (!existsSync(configPath)) {
157
+ throw new Error(`[figma-console-mcp] tokens.config.json not found at ${configPath}`);
158
+ }
159
+ const raw = readFileSync(configPath, "utf-8");
160
+ let parsed;
161
+ try {
162
+ parsed = JSON.parse(raw);
163
+ }
164
+ catch (err) {
165
+ throw new Error(`[figma-console-mcp] tokens.config.json at ${configPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
166
+ }
167
+ const result = TokensConfigSchema.safeParse(parsed);
168
+ if (!result.success) {
169
+ const issues = result.error.issues
170
+ .map((i) => ` • ${i.path.join(".")}: ${i.message}`)
171
+ .join("\n");
172
+ throw new Error(`[figma-console-mcp] tokens.config.json at ${configPath} failed validation:\n${issues}`);
173
+ }
174
+ return {
175
+ config: result.data,
176
+ configPath,
177
+ projectRoot: dirname(configPath),
178
+ };
179
+ }
180
+ /**
181
+ * Default config used when none is found. Drives the "no-config detected"
182
+ * response shape from figma_export_tokens — the AI uses this to propose a
183
+ * scaffold to the user.
184
+ */
185
+ export const DEFAULT_TOKENS_CONFIG = {
186
+ source: { dir: "src/styles/tokens", canonical: "dtcg" },
187
+ generated: {
188
+ dir: "src/styles/generated",
189
+ formats: [
190
+ { format: "css-vars", splitByMode: true },
191
+ ],
192
+ },
193
+ conflictResolution: "ask",
194
+ };
195
+ /**
196
+ * Build a `suggestedScaffold` payload returned when a tool is called and no
197
+ * `tokens.config.json` exists. The AI presents this scaffold to the user,
198
+ * writes the files via its native edit/write tools, then calls the original
199
+ * tool again.
200
+ */
201
+ export function buildSuggestedScaffold(opts) {
202
+ const config = {
203
+ $schema: "https://figma-console-mcp.southleft.com/schemas/tokens.config.v1.json",
204
+ source: { dir: "src/styles/tokens", canonical: "dtcg" },
205
+ generated: {
206
+ dir: "src/styles/generated",
207
+ formats: pickStartingFormats(opts.detectedFramework),
208
+ },
209
+ conflictResolution: "ask",
210
+ };
211
+ const stylesheetImport = pickStylesheetImport(opts.detectedFramework);
212
+ return {
213
+ configContent: JSON.stringify(config, null, 2),
214
+ directories: [config.source.dir, config.generated?.dir ?? "src/styles/generated"],
215
+ stylesheetImport,
216
+ nextSteps: [
217
+ "1. Write `tokens.config.json` at the project root using `configContent`.",
218
+ `2. Create the directories: ${config.source.dir} and ${config.generated?.dir}.`,
219
+ `3. Add this line to your main stylesheet:\n ${stylesheetImport}`,
220
+ "4. Run `figma_export_tokens` again — it'll pick up the new config and populate the source dir.",
221
+ ].join("\n"),
222
+ };
223
+ }
224
+ function pickStartingFormats(framework) {
225
+ // Always emit DTCG as the canonical committed source; layer the
226
+ // framework-appropriate runtime format on top.
227
+ const base = [];
228
+ switch (framework) {
229
+ case "tailwind-v4":
230
+ base.push({ format: "tailwind-v4", splitByMode: true });
231
+ break;
232
+ case "tailwind-v3":
233
+ base.push({ format: "tailwind-v3" });
234
+ break;
235
+ case "scss":
236
+ base.push({ format: "scss", splitByMode: true });
237
+ break;
238
+ case "ts":
239
+ base.push({ format: "ts-module" });
240
+ base.push({ format: "css-vars", splitByMode: true });
241
+ break;
242
+ case "css":
243
+ default:
244
+ base.push({ format: "css-vars", splitByMode: true });
245
+ break;
246
+ }
247
+ return base;
248
+ }
249
+ function pickStylesheetImport(framework) {
250
+ switch (framework) {
251
+ case "tailwind-v4":
252
+ return "@import './styles/generated/tailwind.theme.css';";
253
+ case "scss":
254
+ return "@use './styles/generated/tokens.scss' as *;";
255
+ case "ts":
256
+ case "css":
257
+ default:
258
+ return "@import './styles/generated/tokens.css';";
259
+ }
260
+ }
261
+ /**
262
+ * Pick the export formats from a loaded config that map to a given runtime
263
+ * format. Used by figma_export_tokens to decide which generated files to
264
+ * write. Returns the list verbatim if the caller passed an explicit format.
265
+ */
266
+ export function resolveOutputTargets(config, explicitFormat) {
267
+ if (explicitFormat) {
268
+ // Caller specified a format directly; ignore config's generated list.
269
+ return [{ format: explicitFormat }];
270
+ }
271
+ if (!config?.generated?.formats?.length) {
272
+ // No formats configured. Default to DTCG only — produces the canonical
273
+ // source files but no derived runtime outputs.
274
+ return [{ format: "dtcg" }];
275
+ }
276
+ return config.generated.formats;
277
+ }
278
+ /**
279
+ * Resolve the conflict-resolution strategy. Per-call argument wins over config
280
+ * default, which wins over the global default ("ask").
281
+ */
282
+ export function resolveConflictStrategy(config, perCall) {
283
+ return perCall ?? config?.conflictResolution ?? "ask";
284
+ }