figma-console-mcp 1.33.2 → 1.34.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 (67) hide show
  1. package/README.md +22 -16
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
  3. package/dist/cloudflare/core/tokens/alias-resolver.js +15 -2
  4. package/dist/cloudflare/core/tokens/config.js +10 -0
  5. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  6. package/dist/cloudflare/core/tokens/figma-converter.js +37 -3
  7. package/dist/cloudflare/core/tokens/formatters/dtcg.js +35 -14
  8. package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
  9. package/dist/cloudflare/core/tokens/index.js +2 -1
  10. package/dist/cloudflare/core/tokens/schemas.js +4 -0
  11. package/dist/cloudflare/core/tokens-tools.js +942 -81
  12. package/dist/cloudflare/core/websocket-connector.js +42 -0
  13. package/dist/cloudflare/core/write-tools.js +308 -49
  14. package/dist/cloudflare/index.js +7 -7
  15. package/dist/core/figma-connector.d.ts +12 -0
  16. package/dist/core/figma-connector.d.ts.map +1 -1
  17. package/dist/core/tokens/alias-resolver.d.ts +16 -0
  18. package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
  19. package/dist/core/tokens/alias-resolver.js +15 -2
  20. package/dist/core/tokens/alias-resolver.js.map +1 -1
  21. package/dist/core/tokens/config.d.ts +28 -0
  22. package/dist/core/tokens/config.d.ts.map +1 -1
  23. package/dist/core/tokens/config.js +10 -0
  24. package/dist/core/tokens/config.js.map +1 -1
  25. package/dist/core/tokens/dialect.d.ts +107 -0
  26. package/dist/core/tokens/dialect.d.ts.map +1 -0
  27. package/dist/core/tokens/dialect.js +233 -0
  28. package/dist/core/tokens/dialect.js.map +1 -0
  29. package/dist/core/tokens/figma-converter.d.ts +2 -0
  30. package/dist/core/tokens/figma-converter.d.ts.map +1 -1
  31. package/dist/core/tokens/figma-converter.js +37 -3
  32. package/dist/core/tokens/figma-converter.js.map +1 -1
  33. package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
  34. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
  35. package/dist/core/tokens/formatters/dtcg.js +35 -14
  36. package/dist/core/tokens/formatters/dtcg.js.map +1 -1
  37. package/dist/core/tokens/formatters/json.d.ts.map +1 -1
  38. package/dist/core/tokens/formatters/json.js +28 -10
  39. package/dist/core/tokens/formatters/json.js.map +1 -1
  40. package/dist/core/tokens/index.d.ts +2 -1
  41. package/dist/core/tokens/index.d.ts.map +1 -1
  42. package/dist/core/tokens/index.js +2 -1
  43. package/dist/core/tokens/index.js.map +1 -1
  44. package/dist/core/tokens/schemas.d.ts +3 -0
  45. package/dist/core/tokens/schemas.d.ts.map +1 -1
  46. package/dist/core/tokens/schemas.js +4 -0
  47. package/dist/core/tokens/schemas.js.map +1 -1
  48. package/dist/core/tokens/types.d.ts +29 -0
  49. package/dist/core/tokens/types.d.ts.map +1 -1
  50. package/dist/core/tokens/types.js.map +1 -1
  51. package/dist/core/tokens-tools.d.ts +211 -12
  52. package/dist/core/tokens-tools.d.ts.map +1 -1
  53. package/dist/core/tokens-tools.js +942 -81
  54. package/dist/core/tokens-tools.js.map +1 -1
  55. package/dist/core/websocket-connector.d.ts +38 -0
  56. package/dist/core/websocket-connector.d.ts.map +1 -1
  57. package/dist/core/websocket-connector.js +42 -0
  58. package/dist/core/websocket-connector.js.map +1 -1
  59. package/dist/core/write-tools.d.ts.map +1 -1
  60. package/dist/core/write-tools.js +308 -49
  61. package/dist/core/write-tools.js.map +1 -1
  62. package/dist/local.d.ts.map +1 -1
  63. package/dist/local.js +5 -4
  64. package/dist/local.js.map +1 -1
  65. package/figma-desktop-bridge/code.js +288 -1
  66. package/figma-desktop-bridge/ui.html +27 -0
  67. package/package.json +1 -1
@@ -19,11 +19,12 @@
19
19
  * structured $value form. Aliases emit `"$value": "{path.to.target}"`.
20
20
  *
21
21
  * This formatter is the canonical output — the format every other
22
- * formatter (CSS variables today, Tailwind/SCSS/etc. in future minor
23
- * versions) ultimately derives from.
22
+ * formatter (CSS variables, Tailwind v4/v3, SCSS, TS module, JSON
23
+ * flat/nested, Style Dictionary v3, Tokens Studio) ultimately derives from.
24
24
  */
25
25
  import { FIGMA_MCP_EXTENSION_KEY } from "../types.js";
26
26
  import { formatDtcgReference } from "../alias-resolver.js";
27
+ import { colorValueTo2025, dimensionLiteralTo2025, } from "../dialect.js";
27
28
  export function formatDtcg(doc, opts) {
28
29
  const warnings = [];
29
30
  const files = [];
@@ -35,6 +36,9 @@ export function formatDtcg(doc, opts) {
35
36
  // 4. neither → one file with everything
36
37
  const splitByMode = opts.target.splitByMode ?? false;
37
38
  const splitByCollection = opts.target.splitByCollection ?? false;
39
+ // Value-encoding dialect. 'legacy' (default) is byte-identical to the
40
+ // historical output; '2025' opts into DTCG 2025.10 object colors/dimensions.
41
+ const dialect = opts.target.dtcgDialect ?? "legacy";
38
42
  if (splitByMode && splitByCollection) {
39
43
  for (const set of doc.sets) {
40
44
  for (const mode of set.modes) {
@@ -43,7 +47,7 @@ export function formatDtcg(doc, opts) {
43
47
  .filter((t) => t !== null);
44
48
  files.push({
45
49
  path: filenameFor(opts, set, mode),
46
- content: serializeAsDtcg({ sets: [{ ...set, modes: [mode], tokens: fileTokens }], meta: doc.meta }, warnings, mode),
50
+ content: serializeAsDtcg({ sets: [{ ...set, modes: [mode], tokens: fileTokens }], meta: doc.meta }, warnings, dialect, mode),
47
51
  });
48
52
  }
49
53
  }
@@ -65,7 +69,7 @@ export function formatDtcg(doc, opts) {
65
69
  }));
66
70
  files.push({
67
71
  path: filenameFor(opts, undefined, mode),
68
- content: serializeAsDtcg({ sets: fileSets, meta: doc.meta }, warnings, mode),
72
+ content: serializeAsDtcg({ sets: fileSets, meta: doc.meta }, warnings, dialect, mode),
69
73
  });
70
74
  }
71
75
  }
@@ -73,14 +77,14 @@ export function formatDtcg(doc, opts) {
73
77
  for (const set of doc.sets) {
74
78
  files.push({
75
79
  path: filenameFor(opts, set),
76
- content: serializeAsDtcg({ sets: [set], meta: doc.meta }, warnings),
80
+ content: serializeAsDtcg({ sets: [set], meta: doc.meta }, warnings, dialect),
77
81
  });
78
82
  }
79
83
  }
80
84
  else {
81
85
  files.push({
82
86
  path: filenameFor(opts),
83
- content: serializeAsDtcg(doc, warnings),
87
+ content: serializeAsDtcg(doc, warnings, dialect),
84
88
  });
85
89
  }
86
90
  return { files, warnings };
@@ -131,7 +135,7 @@ function slugify(s) {
131
135
  * file represents — otherwise the parser sees only `$value` literals and
132
136
  * labels them "Default", which breaks round-trip diffs on multi-mode files.
133
137
  */
134
- function serializeAsDtcg(doc, warnings, fileMode) {
138
+ function serializeAsDtcg(doc, warnings, dialect, fileMode) {
135
139
  // Build the nested DTCG group tree by walking every token's path and
136
140
  // building groups along the way.
137
141
  const tree = {};
@@ -176,7 +180,7 @@ function serializeAsDtcg(doc, warnings, fileMode) {
176
180
  tree[setKey] = setGroup;
177
181
  }
178
182
  for (const token of set.tokens) {
179
- writeTokenIntoTree(setGroup, token, set.modes, warnings, fileMode);
183
+ writeTokenIntoTree(setGroup, token, set.modes, warnings, dialect, fileMode);
180
184
  }
181
185
  }
182
186
  return JSON.stringify(sortKeys(tree), null, 2) + "\n";
@@ -200,7 +204,7 @@ function setKeyFor(set) {
200
204
  * figma-console-mcp $extensions so the parser can restore the original
201
205
  * name on round-trip. A warning names both sides of the conflict.
202
206
  */
203
- function writeTokenIntoTree(root, token, setModes, warnings, fileMode) {
207
+ function writeTokenIntoTree(root, token, setModes, warnings, dialect, fileMode) {
204
208
  let cursor = root;
205
209
  for (let i = 0; i < token.path.length - 1; i++) {
206
210
  const segment = token.path[i];
@@ -222,7 +226,7 @@ function writeTokenIntoTree(root, token, setModes, warnings, fileMode) {
222
226
  cursor = next;
223
227
  }
224
228
  const leafKey = token.path[token.path.length - 1];
225
- const rendered = renderToken(token, setModes, warnings, fileMode);
229
+ const rendered = renderToken(token, setModes, warnings, dialect, fileMode);
226
230
  const existing = cursor[leafKey];
227
231
  if (existing &&
228
232
  typeof existing === "object" &&
@@ -261,7 +265,7 @@ function isToken(node) {
261
265
  * splitByMode. Callers who want one-file-per-mode should set splitByMode at
262
266
  * the formatter level.
263
267
  */
264
- function renderToken(token, setModes, warnings, fileMode) {
268
+ function renderToken(token, setModes, warnings, dialect, fileMode) {
265
269
  const result = {
266
270
  $value: "",
267
271
  $type: token.type,
@@ -273,13 +277,13 @@ function renderToken(token, setModes, warnings, fileMode) {
273
277
  // Pick the primary mode: the set's first mode when the token has a value
274
278
  // for it, otherwise the token's first mode.
275
279
  const primaryMode = setModes[0] in token.values ? setModes[0] : modeKeys[0];
276
- result.$value = encodeValue(token.values[primaryMode], token, warnings);
280
+ result.$value = encodeValue(token.values[primaryMode], token, warnings, dialect);
277
281
  const otherModes = {};
278
282
  if (!isSingleMode) {
279
283
  for (const m of modeKeys) {
280
284
  if (m === primaryMode)
281
285
  continue;
282
- otherModes[m] = encodeValue(token.values[m], token, warnings);
286
+ otherModes[m] = encodeValue(token.values[m], token, warnings, dialect);
283
287
  }
284
288
  }
285
289
  // Preserve any pre-existing extensions (e.g. studio.tokens, our own metadata).
@@ -311,7 +315,7 @@ function renderToken(token, setModes, warnings, fileMode) {
311
315
  }
312
316
  return result;
313
317
  }
314
- function encodeValue(value, token, warnings) {
318
+ function encodeValue(value, token, warnings, dialect) {
315
319
  if (!value) {
316
320
  warnings.push(`Token ${token.path.join(".")} has no mode values — emitting empty string.`);
317
321
  return "";
@@ -323,6 +327,23 @@ function encodeValue(value, token, warnings) {
323
327
  warnings.push(`Token ${token.path.join(".")} has neither literal nor reference — emitting empty string.`);
324
328
  return "";
325
329
  }
330
+ // DTCG 2025.10 dialect: colors emit the object form (components from the
331
+ // converter's full-precision rawColor floats, hex kept as the interop
332
+ // courtesy field); dimension-typed bare numbers emit { value, unit: "px" }.
333
+ // duration already emits { value, unit: "ms" } in both dialects. Anything
334
+ // the encoders don't recognize keeps the legacy rendering.
335
+ if (dialect === "2025") {
336
+ if (token.type === "color") {
337
+ const encoded = colorValueTo2025(value);
338
+ if (encoded)
339
+ return encoded;
340
+ }
341
+ else if (token.type === "dimension") {
342
+ const encoded = dimensionLiteralTo2025(value.literal);
343
+ if (encoded)
344
+ return encoded;
345
+ }
346
+ }
326
347
  return value.literal;
327
348
  }
328
349
  function mergeExtension(token, key, payload) {
@@ -31,11 +31,13 @@
31
31
  * aliases get a `null` (caller can decide how to fill those in).
32
32
  */
33
33
  import { buildTokenIndex, resolveAliasChain } from "../alias-resolver.js";
34
+ import { colorValueTo2025, dimensionLiteralTo2025, } from "../dialect.js";
34
35
  export function formatJsonFlat(doc, opts) {
35
36
  const warnings = [];
36
37
  const files = [];
37
38
  const splitByCollection = opts.target.splitByCollection ?? false;
38
39
  const prefix = opts.target.prefix ?? "";
40
+ const dialect = opts.target.dtcgDialect ?? "legacy";
39
41
  // Plain JSON has no native alias mechanism — resolve aliases to their
40
42
  // literal target so consumers get usable values, not opaque `{ref}` strings.
41
43
  const tokenIndex = buildTokenIndex(doc);
@@ -43,14 +45,14 @@ export function formatJsonFlat(doc, opts) {
43
45
  for (const set of doc.sets) {
44
46
  files.push({
45
47
  path: filenameFor(opts, set, "flat"),
46
- content: renderFlat([set], prefix, tokenIndex, warnings),
48
+ content: renderFlat([set], prefix, tokenIndex, warnings, dialect),
47
49
  });
48
50
  }
49
51
  }
50
52
  else {
51
53
  files.push({
52
54
  path: filenameFor(opts, undefined, "flat"),
53
- content: renderFlat(doc.sets, prefix, tokenIndex, warnings),
55
+ content: renderFlat(doc.sets, prefix, tokenIndex, warnings, dialect),
54
56
  });
55
57
  }
56
58
  return { files, warnings };
@@ -59,19 +61,20 @@ export function formatJsonNested(doc, opts) {
59
61
  const warnings = [];
60
62
  const files = [];
61
63
  const splitByCollection = opts.target.splitByCollection ?? false;
64
+ const dialect = opts.target.dtcgDialect ?? "legacy";
62
65
  const tokenIndex = buildTokenIndex(doc);
63
66
  if (splitByCollection) {
64
67
  for (const set of doc.sets) {
65
68
  files.push({
66
69
  path: filenameFor(opts, set, "nested"),
67
- content: renderNested([set], tokenIndex, warnings),
70
+ content: renderNested([set], tokenIndex, warnings, dialect),
68
71
  });
69
72
  }
70
73
  }
71
74
  else {
72
75
  files.push({
73
76
  path: filenameFor(opts, undefined, "nested"),
74
- content: renderNested(doc.sets, tokenIndex, warnings),
77
+ content: renderNested(doc.sets, tokenIndex, warnings, dialect),
75
78
  });
76
79
  }
77
80
  return { files, warnings };
@@ -92,7 +95,7 @@ function slugify(s) {
92
95
  .replace(/[^a-z0-9]+/g, "-")
93
96
  .replace(/^-+|-+$/g, "");
94
97
  }
95
- function renderFlat(sets, prefix, tokenIndex, warnings) {
98
+ function renderFlat(sets, prefix, tokenIndex, warnings, dialect) {
96
99
  const out = {};
97
100
  for (const set of sets) {
98
101
  const primaryMode = pickPrimaryMode(set.modes);
@@ -102,7 +105,7 @@ function renderFlat(sets, prefix, tokenIndex, warnings) {
102
105
  const key = modeName === primaryMode
103
106
  ? baseName
104
107
  : `${baseName}--${slugify(modeName)}`;
105
- const resolved = resolveValue(value, token, modeName, tokenIndex, warnings);
108
+ const resolved = resolveValue(value, token, modeName, tokenIndex, warnings, dialect);
106
109
  if (resolved !== undefined)
107
110
  out[key] = resolved;
108
111
  }
@@ -112,7 +115,7 @@ function renderFlat(sets, prefix, tokenIndex, warnings) {
112
115
  const sorted = Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
113
116
  return JSON.stringify(sorted, null, 2) + "\n";
114
117
  }
115
- function renderNested(sets, tokenIndex, warnings) {
118
+ function renderNested(sets, tokenIndex, warnings, dialect) {
116
119
  const out = {};
117
120
  for (const set of sets) {
118
121
  const isMultiMode = set.modes.length > 1;
@@ -131,7 +134,7 @@ function renderNested(sets, tokenIndex, warnings) {
131
134
  if (isMultiMode) {
132
135
  const modeValues = {};
133
136
  for (const [modeName, value] of Object.entries(token.values)) {
134
- const resolved = resolveValue(value, token, modeName, tokenIndex, warnings);
137
+ const resolved = resolveValue(value, token, modeName, tokenIndex, warnings, dialect);
135
138
  if (resolved !== undefined)
136
139
  modeValues[modeName] = resolved;
137
140
  }
@@ -140,7 +143,7 @@ function renderNested(sets, tokenIndex, warnings) {
140
143
  else {
141
144
  const [onlyModeName, onlyValue] = Object.entries(token.values)[0] ?? [];
142
145
  if (onlyValue) {
143
- const resolved = resolveValue(onlyValue, token, onlyModeName, tokenIndex, warnings);
146
+ const resolved = resolveValue(onlyValue, token, onlyModeName, tokenIndex, warnings, dialect);
144
147
  if (resolved !== undefined)
145
148
  cursor[leafKey] = resolved;
146
149
  }
@@ -152,7 +155,7 @@ function renderNested(sets, tokenIndex, warnings) {
152
155
  function pickPrimaryMode(modes) {
153
156
  return modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
154
157
  }
155
- function resolveValue(value, token, mode, tokenIndex, warnings) {
158
+ function resolveValue(value, token, mode, tokenIndex, warnings, dialect) {
156
159
  let effective = value;
157
160
  if (value.reference) {
158
161
  effective = resolveAliasChain(value, mode, tokenIndex);
@@ -168,6 +171,21 @@ function resolveValue(value, token, mode, tokenIndex, warnings) {
168
171
  if (!effective || effective.literal === undefined || effective.literal === null) {
169
172
  return undefined;
170
173
  }
174
+ // DTCG 2025.10 dialect (opt-in): colors emit the object form, dimensions
175
+ // emit { value, unit: "px" }. Legacy (default) keeps hex strings and
176
+ // "16px" strings — byte-identical to historical output.
177
+ if (dialect === "2025") {
178
+ if (token.type === "color") {
179
+ const encoded = colorValueTo2025(effective);
180
+ if (encoded)
181
+ return encoded;
182
+ }
183
+ else if (token.type === "dimension") {
184
+ const encoded = dimensionLiteralTo2025(effective.literal);
185
+ if (encoded)
186
+ return encoded;
187
+ }
188
+ }
171
189
  if (typeof effective.literal === "number") {
172
190
  if (token.type === "dimension")
173
191
  return `${effective.literal}px`;
@@ -11,5 +11,6 @@ export { TokensConfigSchema, loadTokensConfig, findTokensConfig, DEFAULT_TOKENS_
11
11
  export { ExportTokensInputSchema, ImportTokensInputSchema, ExportFormatSchema, ImportFormatSchema, SyncStrategySchema, ConflictResolutionSchema, } from "./schemas.js";
12
12
  export { parse, detectFormat, } from "./parsers/index.js";
13
13
  export { format, } from "./formatters/index.js";
14
- export { buildTokenIndex, resolveReference, resolveAliasChain, validateAliases, formatDtcgReference, parseDtcgReference, referenceTargetPath, slugifySetName, } from "./alias-resolver.js";
14
+ export { buildTokenIndex, buildTokenLookup, resolveReference, resolveAliasChain, validateAliases, formatDtcgReference, parseDtcgReference, referenceTargetPath, slugifySetName, } from "./alias-resolver.js";
15
15
  export { convertFigmaVariablesToDocument, } from "./figma-converter.js";
16
+ export { canonicalizeTokenValueForComparison, colorLiteralToCanonicalHex, colorValueTo2025, dimensionLiteralTo2025, hexToRawRgba, stripRawColorFromValues, clamp01, } from "./dialect.js";
@@ -88,6 +88,10 @@ export const ExportTokensInputSchema = z.object({
88
88
  .boolean()
89
89
  .optional()
90
90
  .describe("Emit one file per Figma collection. Default false. Useful when collections map to different runtime themes."),
91
+ dtcgDialect: z
92
+ .enum(["legacy", "2025"])
93
+ .optional()
94
+ .describe("DTCG dialect for dtcg/json-flat/json-nested outputs. legacy (default): hex-string colors, maximum compatibility (Style Dictionary v4, Tokens Studio). 2025: DTCG 2025.10 object colors/dimensions (Style Dictionary v5+). Other formats (css-vars, scss, tailwind, ts-module) render final code and ignore this option."),
91
95
  colorFormat: ColorFormatSchema.optional().describe("Color value format in the output. Default: 'hex'. Use 'oklch' for modern Tailwind v4 charts."),
92
96
  sizeUnit: SizeUnitSchema.optional().describe("Unit for dimension tokens. Default: 'rem' for web outputs, 'pt' for iOS, 'dp' for Android."),
93
97
  remBase: z