figma-console-mcp 1.40.1 → 1.40.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.
@@ -1 +1 @@
1
- {"version":3,"file":"tokens-tools.d.ts","sourceRoot":"","sources":["../../src/core/tokens-tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokens-tools.d.ts","sourceRoot":"","sources":["../../src/core/tokens-tools.ts"],"names":[],"mappings":"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"}
@@ -35,7 +35,7 @@ const logger = createChildLogger({ component: "tokens-tools" });
35
35
  * on every exported token document. Kept in sync with package.json by
36
36
  * scripts/release.sh — see step 3 of the release flow.
37
37
  */
38
- const MCP_VERSION = "1.40.1";
38
+ const MCP_VERSION = "1.40.2";
39
39
  const EXPORT_TOOL_DESCRIPTION = `Export Figma variables to design token files in your codebase. Bidirectional with figma_import_tokens — together they replace Style Dictionary and Tokens Studio's export pipeline for the popular styling methods.
40
40
 
41
41
  FULLY-IMPLEMENTED OUTPUT FORMATS:
@@ -52,7 +52,7 @@ FULLY-IMPLEMENTED OUTPUT FORMATS:
52
52
 
53
53
  ZERO-ARG USAGE: With a tokens.config.json at your project root, just call the tool with no args — it picks up source dir, output formats, modes, prefix, etc. from config. See the response's \`suggestedScaffold\` payload when no config is detected — present it to the user, write the scaffold via your file tools, then call again.
54
54
 
55
- MERGE STRATEGY: Default \`strategy: "merge"\` only writes tokens that actually changed in Figma since the last sync. Use \`dry-run\` to preview what would change. Use \`replace\` to wipe and rewrite (rare; for resetting drift).
55
+ WRITE SAFETY: Export REPLACES the contents of each target file with the current Figma state — it does not merge into it. Under the default \`strategy: "merge"\` the tool refuses to write (and changes nothing) when overwriting an existing DTCG file would delete tokens it does not manage: tokens added by hand, or tokens from collections outside this export. It also refuses when a requested collectionId is not in the file it read, or when the export is empty — both usually mean a different file is active in Figma than you intended. Variables deleted in Figma are removed from the file as normal. Use \`dry-run\` to preview, \`replace\` to overwrite unconditionally. Generated files record the Figma file they came from (a \`Source: Figma file <key>\` header line; \`figmaFileKey\` in DTCG), and the default strategy also refuses to overwrite a file that was generated from a DIFFERENT Figma file. Plain-JSON formats (json-flat, json-nested, style-dictionary-v3, tokens-studio) cannot carry that record — pin \`figmaFile\` in tokens.config.json for a hard guarantee. Every response includes \`source\` (the Figma file name/key the data was read from) — check it.
56
56
 
57
57
  DTCG DIALECT (\`dtcgDialect\`, applies to 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+). figma_import_tokens accepts BOTH dialects regardless of this setting.
58
58
 
@@ -82,7 +82,7 @@ export function registerExportTokensTool(server, getDesktopConnector, opts = {})
82
82
  text: JSON.stringify({
83
83
  error: err instanceof Error ? err.message : String(err),
84
84
  // Output-location errors already say what to do; the format hint would only mislead.
85
- ...(/outputPath|Cannot write/.test(err instanceof Error ? err.message : String(err))
85
+ ...(/outputPath|Cannot write|[Nn]othing was written/.test(err instanceof Error ? err.message : String(err))
86
86
  ? {}
87
87
  : {
88
88
  hint: "If this is a TokenFormatNotImplementedError for a non-DTCG/non-CSS format, export to 'dtcg' or 'css-vars' instead — those are the fully-implemented formats. The canonical DTCG JSON can be consumed by Style Dictionary v4 or any other DTCG-aware tooling.",
@@ -171,9 +171,27 @@ async function handleExport(args, getDesktopConnector, opts) {
171
171
  }
172
172
  // 3. Normalize to the converter's expected shape.
173
173
  const payload = normalizeFigmaPayload(variableData);
174
+ const sourceFileKey = (typeof variableData.fileKey === "string" && variableData.fileKey) ||
175
+ fileKey ||
176
+ null;
177
+ // 3b. Identify the Figma file the data ACTUALLY came from. Without a
178
+ // tokens.config.json `figmaFile`, the bridge reads whichever file is
179
+ // active in Figma Desktop — which may not be the one the caller means.
180
+ // (With `figmaFile` pinned, sendCommand routes to that file's plugin
181
+ // connection and rejects if it isn't connected, so no check is needed.)
182
+ const source = {
183
+ fileKey: sourceFileKey,
184
+ fileName: (sourceFileKey && opts.resolveFileName?.(sourceFileKey)) || null,
185
+ };
186
+ // 3c. Every requested collection must exist in that file. A partial or
187
+ // empty match would otherwise be written over the target as if it were
188
+ // the full export.
189
+ assertRequestedCollectionsFound(args.collectionIds, payload.collections, source);
174
190
  // 4. Convert to canonical TokenDocument.
175
191
  const { document, warnings } = convertFigmaVariablesToDocument(payload, {
176
- figmaFileKey: fileKey,
192
+ // The file the data was ACTUALLY read from — stamped into DTCG output so a
193
+ // later export from a different Figma file can be recognized and refused.
194
+ figmaFileKey: sourceFileKey ?? undefined,
177
195
  collectionIds: args.collectionIds,
178
196
  modes: args.modes,
179
197
  stripPrefix: args.prefix,
@@ -207,7 +225,12 @@ async function handleExport(args, getDesktopConnector, opts) {
207
225
  projectRoot: loaded?.projectRoot,
208
226
  });
209
227
  for (const file of result.files) {
210
- allFiles.push({ format: target.format, ...file });
228
+ allFiles.push({
229
+ format: target.format,
230
+ ...file,
231
+ content: stampSourceFile(file.content, sourceFileKey),
232
+ splitByCollection: !!(args.splitByCollection ?? target.splitByCollection),
233
+ });
211
234
  }
212
235
  allWarnings.push(...result.warnings);
213
236
  }
@@ -228,6 +251,76 @@ async function handleExport(args, getDesktopConnector, opts) {
228
251
  const writtenPaths = [];
229
252
  const plan = planExportWrites(allFiles, args.outputPath, loaded);
230
253
  allWarnings.push(...plan.warnings);
254
+ // Validate EVERY planned write before performing ANY of them — a refused
255
+ // export must leave the disk exactly as it found it.
256
+ const totalTokens = document.sets.reduce((n, s) => n + s.tokens.length, 0);
257
+ if (totalTokens === 0) {
258
+ if (plan.writes.length > 0) {
259
+ throw new Error(`[figma-console-mcp] The export contains 0 tokens (read from ${describeSource(source)}), so nothing was written — writing it would have emptied ${plan.writes.length === 1 ? `"${plan.writes[0].fullPath}"` : `${plan.writes.length} files`}. Check that the right file is active in Figma and that the collectionIds/modes filters match it.`);
260
+ }
261
+ allWarnings.push(`The export contains 0 tokens (read from ${describeSource(source)}). Check that the right file is active in Figma and that the collectionIds/modes filters match it.`);
262
+ }
263
+ if (args.strategy !== "replace") {
264
+ const allExportedCollectionIds = document.sets
265
+ .map((s) => s.meta?.figmaCollectionId)
266
+ .filter((id) => typeof id === "string");
267
+ // Exported collections that ended up with no tokens appear in no file's
268
+ // content — yet a token stamped with one that vanishes was still
269
+ // legitimately deleted in Figma.
270
+ const emptyExportedCollectionIds = document.sets
271
+ .filter((s) => s.tokens.length === 0)
272
+ .map((s) => s.meta?.figmaCollectionId)
273
+ .filter((id) => typeof id === "string");
274
+ for (const write of plan.writes) {
275
+ if (!existsSync(write.fullPath))
276
+ continue;
277
+ let existing;
278
+ try {
279
+ existing = readFileSync(write.fullPath, "utf-8");
280
+ }
281
+ catch (err) {
282
+ // Unreadable target: we cannot prove the overwrite is safe, so don't.
283
+ throw new Error(`[figma-console-mcp] Nothing was written. "${write.fullPath}" exists but could not be read to check what overwriting it would destroy (${err instanceof Error ? err.message : String(err)}). Fix the file's permissions, or pass strategy: "replace" to overwrite it unconditionally.`);
284
+ }
285
+ // (a) Any format: was this file generated from a DIFFERENT Figma file?
286
+ const stampedKey = readSourceFileStamp(existing);
287
+ if (stampedKey && !sourceFileKey) {
288
+ // The target records its source, but this run can't say where ITS data
289
+ // came from — we cannot prove they match, so don't overwrite.
290
+ throw new Error(`[figma-console-mcp] Nothing was written. "${write.fullPath}" was generated from Figma file ${stampedKey}, but the Desktop Bridge plugin did not report which file this export read from, so it can't be confirmed to be the same one. Re-import the plugin manifest in Figma (the bundled manifest enables file-key reporting) and retry, or pass strategy: "replace" to overwrite anyway.`);
291
+ }
292
+ if (stampedKey && sourceFileKey && stampedKey !== sourceFileKey) {
293
+ throw new Error(`[figma-console-mcp] Nothing was written. "${write.fullPath}" was generated from Figma file ${stampedKey}, but this export read from ${describeSource(source)} — most likely a different file/tab is active in Figma than you intended. Switch to the right file (or use figma_navigate) and retry. If you really mean to re-source this file, pass strategy: "replace".`);
294
+ }
295
+ // (b) DTCG: would the overwrite destroy tokens this export doesn't manage?
296
+ // A DTCG target we cannot parse (merge-conflict markers, a truncated
297
+ // write) can't be checked — refuse rather than bulldoze it.
298
+ if (write.format === "dtcg" &&
299
+ stripBom(existing).trim() !== "" &&
300
+ !isParsableJson(existing)) {
301
+ throw new Error(`[figma-console-mcp] Nothing was written. "${write.fullPath}" exists but is not valid JSON (unresolved merge conflict? partial write?), so what overwriting it would destroy can't be checked. Fix the file, or pass strategy: "replace" to overwrite it unconditionally.`);
302
+ }
303
+ // A DTCG token file must never be replaced by a DIFFERENT kind of
304
+ // output (css-vars aimed at typography.tokens.json): the new side
305
+ // isn't JSON, so the token comparison below would see nothing lost.
306
+ if (dtcgTokenCount(existing) > 0 && !isParsableJson(write.content)) {
307
+ throw new Error(`[figma-console-mcp] Nothing was written. "${write.fullPath}" is a DTCG token file (${dtcgTokenCount(existing)} tokens), and this export would replace it with ${write.format} output — every token in it would be lost. Point outputPath at a ${write.format} file instead, or pass strategy: "replace" if you really mean to convert this file.`);
308
+ }
309
+ // Which collections does THIS file speak for? With splitByCollection
310
+ // a file holds one collection, so only that one (a token from another
311
+ // collection pasted in is NOT ours to drop). Otherwise every exported
312
+ // collection feeds the file — including one that has just lost the
313
+ // mode a splitByMode file is for, whose tokens rightly disappear.
314
+ const loss = findUnmanagedTokenLoss(existing, write.content, new Set(write.splitByCollection
315
+ ? [
316
+ ...collectionIdsInDtcg(write.content),
317
+ ...emptyExportedCollectionIds,
318
+ ]
319
+ : allExportedCollectionIds));
320
+ if (loss)
321
+ throw unmanagedLossError(loss, write.fullPath, source);
322
+ }
323
+ }
231
324
  if (!dryRun) {
232
325
  for (const write of plan.writes) {
233
326
  try {
@@ -250,6 +343,10 @@ async function handleExport(args, getDesktopConnector, opts) {
250
343
  mode: dryRun ? "dry-run" : outputBase ? "written" : "inline",
251
344
  configFound: !!loaded,
252
345
  configPath: loaded?.configPath ?? null,
346
+ // Which Figma file the variables were read from. Verify this is
347
+ // the file you meant — the bridge reads the ACTIVE file unless
348
+ // tokens.config.json pins `figmaFile`.
349
+ source,
253
350
  collections: document.sets.map((s) => ({
254
351
  name: s.name,
255
352
  modes: s.modes,
@@ -575,6 +672,244 @@ function resolveOutputBaseForFormat(outputPath, loaded, format) {
575
672
  }
576
673
  return null;
577
674
  }
675
+ function describeSource(source) {
676
+ if (source.fileName && source.fileKey)
677
+ return `Figma file "${source.fileName}" (${source.fileKey})`;
678
+ if (source.fileKey)
679
+ return `Figma file ${source.fileKey}`;
680
+ return "the file currently active in Figma";
681
+ }
682
+ /**
683
+ * Throw unless every requested collection id exists in the pulled payload.
684
+ * All-or-nothing on purpose: exporting 1 of 2 requested collections over an
685
+ * existing file is still data loss, just a quieter one.
686
+ */
687
+ export function assertRequestedCollectionsFound(requested, available, source) {
688
+ if (!Array.isArray(requested) || requested.length === 0)
689
+ return;
690
+ const have = new Set(available.map((c) => c.id));
691
+ const missing = requested.filter((id) => !have.has(id));
692
+ if (missing.length === 0)
693
+ return;
694
+ const listed = available
695
+ .slice(0, 12)
696
+ .map((c) => `${c.name ?? "(unnamed)"} [${c.id}]`)
697
+ .join(", ");
698
+ throw new Error(`[figma-console-mcp] ${missing.length} of ${requested.length} requested collection(s) not found in ${describeSource(source)}: ${missing.join(", ")}. Nothing was written. ` +
699
+ "The Desktop Bridge reads from the file that is active in Figma Desktop, so this usually means a different file/tab is focused than the one that owns the collection — switch to it (or use figma_navigate) and retry. " +
700
+ (available.length > 0
701
+ ? `Collections in that file: ${listed}${available.length > 12 ? ", …" : ""}.`
702
+ : "That file has no variable collections."));
703
+ }
704
+ const GENERATED_HEADER = "Generated by figma-console-mcp";
705
+ /** Editors on Windows love a UTF-8 BOM; JSON.parse does not. */
706
+ function stripBom(content) {
707
+ return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
708
+ }
709
+ function isParsableJson(content) {
710
+ try {
711
+ JSON.parse(stripBom(content));
712
+ return true;
713
+ }
714
+ catch {
715
+ return false;
716
+ }
717
+ }
718
+ /**
719
+ * Record which Figma file a generated TEXT output came from, as a
720
+ * `Source: Figma file <key>` comment line directly under the formatter's
721
+ * existing "Generated by figma-console-mcp" header — in that header's own
722
+ * comment style, so this works for every current and future text format
723
+ * without touching the formatters. JSON outputs are left alone here: DTCG
724
+ * carries the key in its root `$extensions`, and the plain-JSON formats have
725
+ * nowhere to put a comment.
726
+ */
727
+ export function stampSourceFile(content, sourceFileKey) {
728
+ if (!sourceFileKey)
729
+ return content;
730
+ const lines = content.split("\n");
731
+ const idx = lines
732
+ .slice(0, 6)
733
+ .findIndex((line) => line.includes(GENERATED_HEADER));
734
+ if (idx === -1)
735
+ return content;
736
+ const header = lines[idx];
737
+ const label = `Source: Figma file ${sourceFileKey}`;
738
+ const indent = header.match(/^\s*/)?.[0] ?? "";
739
+ const trimmed = header.trimStart();
740
+ let stamp;
741
+ if (trimmed.startsWith("/*") && trimmed.trimEnd().endsWith("*/")) {
742
+ stamp = `${indent}/* ${label} */`;
743
+ }
744
+ else if (trimmed.startsWith("//")) {
745
+ stamp = `${indent}// ${label}`;
746
+ }
747
+ else if (trimmed.startsWith("*")) {
748
+ stamp = `${indent}* ${label}`;
749
+ }
750
+ else {
751
+ return content; // unknown comment style — never guess at syntax
752
+ }
753
+ lines.splice(idx + 1, 0, stamp);
754
+ return lines.join("\n");
755
+ }
756
+ /**
757
+ * Read back the Figma file key a previously exported file was generated from:
758
+ * DTCG root `$extensions["figma-console-mcp"].figmaFileKey`, or the
759
+ * `Source: Figma file <key>` header line of a text output. Null when the file
760
+ * carries no stamp (exported before stamping existed, or a plain-JSON format).
761
+ */
762
+ export function readSourceFileStamp(rawContent) {
763
+ const content = stripBom(rawContent);
764
+ const head = content.slice(0, 800);
765
+ if (head.includes(GENERATED_HEADER)) {
766
+ const m = head.match(/Source: Figma file ([A-Za-z0-9_-]+)/);
767
+ if (m)
768
+ return m[1];
769
+ }
770
+ if (content.trimStart().startsWith("{")) {
771
+ try {
772
+ const key = JSON.parse(content)?.$extensions?.["figma-console-mcp"]
773
+ ?.figmaFileKey;
774
+ if (typeof key === "string" && key)
775
+ return key;
776
+ }
777
+ catch {
778
+ /* not JSON — no stamp */
779
+ }
780
+ }
781
+ return null;
782
+ }
783
+ /** Number of `$value` leaves in a DTCG document; 0 when it isn't one. */
784
+ function dtcgTokenCount(content) {
785
+ let root;
786
+ try {
787
+ root = JSON.parse(stripBom(content));
788
+ }
789
+ catch {
790
+ return 0;
791
+ }
792
+ let count = 0;
793
+ const walk = (node) => {
794
+ if (!node || typeof node !== "object" || Array.isArray(node))
795
+ return;
796
+ const obj = node;
797
+ if ("$value" in obj) {
798
+ count++;
799
+ return;
800
+ }
801
+ for (const [key, child] of Object.entries(obj)) {
802
+ if (!key.startsWith("$"))
803
+ walk(child);
804
+ }
805
+ };
806
+ walk(root);
807
+ return count;
808
+ }
809
+ /** Collection ids present in a DTCG document (set-level and token-level stamps). */
810
+ function collectionIdsInDtcg(content) {
811
+ let root;
812
+ try {
813
+ root = JSON.parse(stripBom(content));
814
+ }
815
+ catch {
816
+ return [];
817
+ }
818
+ const ids = new Set();
819
+ const walk = (node) => {
820
+ if (!node || typeof node !== "object" || Array.isArray(node))
821
+ return;
822
+ const obj = node;
823
+ const ext = obj.$extensions?.["figma-console-mcp"];
824
+ if (typeof ext?.figmaCollectionId === "string")
825
+ ids.add(ext.figmaCollectionId);
826
+ if (typeof ext?.collectionId === "string")
827
+ ids.add(ext.collectionId);
828
+ if ("$value" in obj)
829
+ return;
830
+ for (const [key, child] of Object.entries(obj)) {
831
+ if (!key.startsWith("$"))
832
+ walk(child);
833
+ }
834
+ };
835
+ walk(root);
836
+ return [...ids];
837
+ }
838
+ /**
839
+ * Compare an existing DTCG token file with the content about to replace it.
840
+ *
841
+ * A token that disappears is only a legitimate drop when the export MANAGES
842
+ * it: stamped with a variableId AND belonging to a collection included in this
843
+ * export — i.e. the variable was deleted in Figma. Anything else that would
844
+ * vanish is loss the caller never asked for:
845
+ * • code-only tokens (no stamp) someone added by hand, and
846
+ * • tokens from collections outside this export — a scoped export pointed at
847
+ * a shared file, or an export read from the WRONG Figma file.
848
+ *
849
+ * Returns null when nothing unmanaged is lost, or when the existing file is
850
+ * not a DTCG JSON document (CSS/SCSS/TS outputs are generated artifacts).
851
+ */
852
+ export function findUnmanagedTokenLoss(existingContent, newContent, exportedCollectionIds) {
853
+ let existing;
854
+ let next;
855
+ try {
856
+ existing = JSON.parse(stripBom(existingContent));
857
+ next = JSON.parse(stripBom(newContent));
858
+ }
859
+ catch {
860
+ return null;
861
+ }
862
+ const collect = (node, path, out) => {
863
+ if (!node || typeof node !== "object" || Array.isArray(node))
864
+ return;
865
+ const obj = node;
866
+ if ("$value" in obj) {
867
+ const ext = obj.$extensions?.["figma-console-mcp"];
868
+ out.set(path.join("."), ext && typeof ext === "object"
869
+ ? ext
870
+ : undefined);
871
+ return;
872
+ }
873
+ for (const [key, child] of Object.entries(obj)) {
874
+ if (key.startsWith("$"))
875
+ continue;
876
+ collect(child, [...path, key], out);
877
+ }
878
+ };
879
+ const before = new Map();
880
+ const after = new Map();
881
+ collect(existing, [], before);
882
+ collect(next, [], after);
883
+ if (before.size === 0)
884
+ return null;
885
+ const loss = { codeOnly: [], otherCollections: [] };
886
+ for (const [path, stamp] of before) {
887
+ if (after.has(path))
888
+ continue;
889
+ if (!stamp || typeof stamp.variableId !== "string") {
890
+ loss.codeOnly.push(path);
891
+ }
892
+ else if (typeof stamp.collectionId !== "string" ||
893
+ !exportedCollectionIds.has(stamp.collectionId)) {
894
+ loss.otherCollections.push(path);
895
+ }
896
+ // else: stamped + collection exported + gone = deleted in Figma. Fine.
897
+ }
898
+ return loss.codeOnly.length + loss.otherCollections.length > 0 ? loss : null;
899
+ }
900
+ function unmanagedLossError(loss, fullPath, source) {
901
+ const sample = (paths) => `${paths.slice(0, 5).join(", ")}${paths.length > 5 ? `, +${paths.length - 5} more` : ""}`;
902
+ const parts = [];
903
+ if (loss.otherCollections.length > 0) {
904
+ parts.push(`${loss.otherCollections.length} token(s) from collections that are not part of this export (${sample(loss.otherCollections)})`);
905
+ }
906
+ if (loss.codeOnly.length > 0) {
907
+ parts.push(`${loss.codeOnly.length} token(s) that were not exported from Figma, e.g. added by hand (${sample(loss.codeOnly)})`);
908
+ }
909
+ return new Error(`[figma-console-mcp] Nothing was written. Overwriting "${fullPath}" with this export (read from ${describeSource(source)}) would delete ${parts.join(" and ")}. ` +
910
+ "Export replaces the whole file — it does not merge into it. If the source file above is not the one you intended, switch files in Figma and retry. " +
911
+ 'If the loss is intended, pass strategy: "replace" to overwrite anyway; otherwise export to a different path and reconcile by hand.');
912
+ }
578
913
  /**
579
914
  * Extensions the formatters emit. An `outputPath` ending in one of these is a
580
915
  * FILE target, not a directory — nobody wants a directory named
@@ -636,6 +971,7 @@ export function planExportWrites(files, outputPath, loaded) {
636
971
  sourcePath: file.path,
637
972
  fullPath: target,
638
973
  content: file.content,
974
+ splitByCollection: file.splitByCollection,
639
975
  },
640
976
  ],
641
977
  warnings,
@@ -652,6 +988,7 @@ export function planExportWrites(files, outputPath, loaded) {
652
988
  sourcePath: file.path,
653
989
  fullPath: isAbsolute(file.path) ? file.path : join(base, file.path),
654
990
  content: file.content,
991
+ splitByCollection: file.splitByCollection,
655
992
  });
656
993
  }
657
994
  return { writes, warnings };