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.
- package/README.md +3 -2
- package/dist/cloudflare/core/tokens/schemas.js +1 -1
- package/dist/cloudflare/core/tokens-tools.js +342 -5
- package/dist/cloudflare/index.js +3 -3
- package/dist/core/tokens/schemas.js +1 -1
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +62 -0
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +342 -5
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +7 -1
- package/dist/local.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> **Your design system as an API.** Model Context Protocol server that bridges design and developmentβgiving AI assistants complete access to Figma for **extraction**, **creation**, **debugging**, and **bidirectional token sync**.
|
|
10
10
|
|
|
11
|
-
> **π Design System Extraction (v1.40.0, patched v1.40.
|
|
11
|
+
> **π Design System Extraction (v1.40.0, patched v1.40.2):** Turn a production codebase into a design system. Seven new `figma_ds_*` tools (Local Mode) scan one or more app codebases β framework, styling-method, and vendor-layer detection; a usage-ranked component inventory classified vendored / wrapped / pure-vendor / bespoke; variant inference from real call sites; duplicate detection; and an architecture pass that separates a UI kit from a design system (`FollowButton` is really a `Button` β and here are the missing generic primitives). Then they mine the app's de-facto styling into DTCG tokens with per-token provenance β multi-mode CSS custom properties (`.dark`, `[data-theme]`, and friends), SCSS variables, Tailwind config values, shadcn HSL triples, Tailwind utility-class frequency mining valued from the app's own theme, and frequency-promoted raw values β scaffold the design-system package with token/typography/iconography showcase pages, wire a fresh Storybook workshop to the app's real theme layers and fonts, deep-extract components for porting with CSF3 story scaffolds, gate the result behind deterministic fidelity evals, and persist porting progress across sessions. The extracted `tokens/tokens.json` imports straight into Figma variables via `figma_import_tokens` β a full code β design system β Figma round-trip. Also fixes the token formatters quoting CSS functional expressions like `cubic-bezier(...)` (a quoted easing silently kills transitions β affects `figma_export_tokens` output too). Server-only β no plugin re-import needed. [See what's new β](CHANGELOG.md#1400---2026-08-16)
|
|
12
12
|
|
|
13
13
|
## What is this?
|
|
14
14
|
|
|
@@ -840,9 +840,10 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
840
840
|
|
|
841
841
|
## π€οΈ Roadmap
|
|
842
842
|
|
|
843
|
-
**Current Status:** v1.40.
|
|
843
|
+
**Current Status:** v1.40.2 (Stable) - Production-ready. Latest: a data-loss fix for `figma_export_tokens` β with a different file active in Figma than intended, an export could replace a real token file with an empty one (or with another file's tokens) and report success; exports now refuse to write when a requested collection is missing, when the export is empty, when the target was generated from a different Figma file, or when overwriting would destroy tokens the export doesn't manage, and `strategy: "merge"` β documented as preserving code-only tokens but never implemented β is now a genuinely safe default (server-only, no plugin re-import). On top of v1.40.1's accuracy fixes for generated component docs across variants β a descendant's fill (typically an icon's) could be documented as a variant's Background, Color Token headings kept only a property literally named `Variant`, spacing was read from the first variant alone, and icons were only detected by layer name β plus `figma_export_tokens` accepting a file path as `outputPath` instead of failing with a raw `EEXIST` or silently creating a directory (server-only, no plugin re-import). On top of v1.40.0's design system extraction from production codebases β seven Local-Mode `figma_ds_*` tools that scan one or more apps (framework/styling/vendor detection, usage-ranked component inventory classified vendored/wrapped/pure-vendor/bespoke, variant inference from real call sites, duplicate detection, and an architecture pass separating UI-kit specializations from the missing generic primitives), mine the de-facto styling into DTCG tokens with per-token provenance (multi-mode CSS custom properties, SCSS, Tailwind config, shadcn HSL triples, utility-class frequency mining valued from the app's own theme), scaffold the design-system package with showcase docs pages, wire a fresh Storybook workshop to the app's real theme layers and fonts, deep-extract components for porting with CSF3 story scaffolds, verify with deterministic fidelity evals, and persist porting progress across sessions β the extracted tokens import straight into Figma variables via `figma_import_tokens`. On top of v1.39.1's fix for a plugin-update banner that could never be cleared β an older server instance nagging a newer plugin to re-import, which only ever installs the same or a newer plugin. On top of v1.39.0's multi-file execution β `figma_execute_across_files` runs one script against several Desktop Bridge-connected files concurrently (four files x 2s of work in ~3s, not ~8s), and `figma_execute` takes an optional `fileKey` to target a single file without moving the active file or releasing target lock; targeting must be explicit (`fileKeys` or `allFiles: true`), so nothing fans out to files you did not name. From community PR #107 by @Wolfr. Ships with a relay fix: `handleResult()` in the plugin's ui.html rebuilt each response field by field and silently dropped `resultAnalysis` and `fileContext`, so the `resultAnalysis.warning` check that `figma_execute`'s own description tells callers to perform has never been possible (re-import `manifest.json` for those two fields; everything else works without it). On top of v1.38.2's connection-stability fix β the server's own orphan reaper was terminating healthy MCP servers because its liveness probe used an IPv4 literal while the server binds the IPv6 loopback, which turned every kill-safety gate into a rubber stamp; this was the cause of recurring "Server disconnected" errors across all MCP clients. Alongside v1.38.1's fix for the shared-library variable tools, which had silently reported zero collections since v1.29.0 and reported failed imports as successes. On top of v1.38.0's ongoing component changelogs in generated docs β `figma_generate_component_doc` takes an opt-in `history` parameter that pulls per-component **design history** from Figma version history (each version diffed and scoped to that component, so renames, added properties, and token bindings show up as rows) alongside **code history** from `git log` on the component's source files, rendered as a `## History` section with design, code, and release-note tables. Prefers labeled versions, falls back to auto-saves when a file has none, and is off by default so existing calls are unchanged (server-only, no plugin re-import). On top of v1.37.x's design-system health audit for every client β `figma_audit_design_system_report` runs the dashboard's deterministic six-category scoring engine and returns the report as data with per-finding remediation, live-first fileKey-verified data (source disclosed), chunked per-category drill-down, and a 5-minute cache (server-only, no plugin re-import). On top of v1.36.0's target lock for multi-file parallel work β β `figma_navigate` takes a `lock: true` flag that pins the active file so an AI agent can work in one file while you work in another without commands routing to the wrong file. On top of v1.35.0's Figma Slots write support β create, inspect, populate, and reset Slots (GA at Config 2026) via 5 new tools (`figma_create_slot`, `figma_get_slots`, `figma_append_to_slot`, `figma_reset_slot`, `figma_add_slot_property`), live-validated against the GA Plugin API and based on community PR #77. On top of v1.34.0's Bidirectional Token Sync v2 + DTCG 2025.10 β `figma_import_tokens` applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5Γ is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog β fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figmaβcode token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **121 tools** (Local) / **101 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
|
|
844
844
|
|
|
845
845
|
**Recent Releases:**
|
|
846
|
+
- [x] **v1.40.2** - Token export data-loss fix. `figma_export_tokens` reads from whichever file is active in Figma Desktop; with the wrong one focused it could overwrite a real token file with an empty document β or, for CSS/SCSS/Tailwind/TS outputs, with another file's tokens β and report `success: true`. Exports now refuse (writing nothing) when a requested collection isn't in the file that was read, when the export is empty, when the target was generated from a different Figma file (generated files now record their source), when a DTCG target can't be parsed or would be replaced by a different kind of output, or when overwriting would destroy hand-added tokens or another collection's tokens. `strategy: "merge"` had been documented as preserving code-only tokens but was never implemented; it is now the safe default and `replace` is the explicit override. Every response reports the `source` Figma file. Server-only. Reported by Isabella Minzly; two further loss paths found in code review.
|
|
846
847
|
- [x] **v1.40.1** - Component-doc variant accuracy + token export file paths. `figma_generate_component_doc`: Background now comes only from the variant's own fills (a transparent outline/ghost variant no longer borrows its icon's color), Color Token headings name every variant property under any property names, Spacing Tokens are compared across all variants (`varies by **Size**: β¦`, with a Spacing Inconsistencies list for properties tokenized on some variants and hardcoded on others), and icons resolve from the main component and `INSTANCE_SWAP` slots rather than a layer name containing "icon". `figma_export_tokens`: an `outputPath` that is an existing file or ends in a token-file extension is written as that exact file; multi-file exports into a file path are rejected before anything is written. `figma_search_components` no longer reports a failed component load as a successful empty search. Server-only. Reported by Robin Di Capua and Isabella Minzly.
|
|
847
848
|
- [x] **v1.40.0** - Design System Extraction. Seven new Local-Mode-only tools that turn a production codebase into a design system, plus the supporting extraction engine. `figma_ds_analyze` scans one or more app codebases: framework/styling/vendor-layer detection (React/Next/Angular/Web Components; Tailwind v3/v4, CSS Modules, SCSS, Emotion, styled-components; shadcn/ui, Radix, MUI, Chakraβ¦), a usage-ranked component inventory classified vendored/wrapped/pure-vendor/bespoke with prop contracts, variant inference from real call sites, duplicate detection, iconography + typography capture, and an architecture classification separating a UI kit from a design system (atomic levels, specializations like FollowButton β Button, missing generic primitives). `figma_ds_extract_tokens` mines de-facto styling into DTCG tokens β multi-mode CSS custom properties across `.dark`/`[data-theme]` conventions, SCSS variables, Tailwind config values, shadcn HSL triples, Tailwind utility-class frequency mining valued from the app's own theme, frequency-promoted raw values β with per-token provenance in `$extensions`, importable straight into Figma variables via `figma_import_tokens`. `figma_ds_scaffold` generates the package (token files via the shared formatters, token/typography/iconography showcase pages); `figma_ds_setup_storybook` wires a fresh Storybook workshop to the app's real theme layers and fonts; `figma_ds_extract_component` produces per-component porting manifests with CSF3 story scaffolds; `figma_ds_verify` gates the result with deterministic fidelity evals plus Figma round-trip readiness; `figma_ds_status` persists porting progress across sessions. Also fixes the CSS/SCSS/Tailwind v4 formatters quoting CSS functional expressions (a quoted `cubic-bezier(...)` silently kills transitions β affects `figma_export_tokens` too). Server-only, no plugin re-import.
|
|
848
849
|
- [x] **v1.39.1** - Plugin update banner could never be cleared. `computePluginUpdateAvailable()` was a bare inequality with no direction check, so a server whose bundled plugin copy was OLDER than the connected plugin still told the user to re-import β which only ever installs the same or a newer plugin. Not an edge case: the 9223β9232 port range keeps several server instances alive, `BUNDLED_PLUGIN_VERSION` is parsed once at module load, and every server left running from before an upgrade nags a correctly-updated plugin. Reproduced live with four leftover v1.38.2 servers against a freshly re-imported 1.39.0 plugin. Now flags only when the bundled copy is genuinely newer. Server-only, no plugin re-import. 1452 tests.
|
|
@@ -71,7 +71,7 @@ export const ExportTokensInputSchema = z.object({
|
|
|
71
71
|
.string()
|
|
72
72
|
.optional()
|
|
73
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)
|
|
74
|
+
strategy: SyncStrategySchema.optional().describe("How to handle existing output files. Export always replaces a target file's contents with the current Figma state β it does not merge token-by-token. 'merge' (default) is the SAFE mode: it refuses to write, leaving the file untouched, if overwriting an existing DTCG file would delete tokens the export does not manage (hand-added tokens, or tokens from collections not included in this export). 'replace' overwrites unconditionally. 'dry-run' reports what would be written without writing."),
|
|
75
75
|
prefix: z
|
|
76
76
|
.string()
|
|
77
77
|
.optional()
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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 };
|
package/dist/cloudflare/index.js
CHANGED
|
@@ -75,7 +75,7 @@ export class FigmaConsoleMCPv3 extends McpAgent {
|
|
|
75
75
|
this.server = (() => {
|
|
76
76
|
const s = new McpServer({
|
|
77
77
|
name: "Figma Console MCP",
|
|
78
|
-
version: "1.40.
|
|
78
|
+
version: "1.40.2",
|
|
79
79
|
});
|
|
80
80
|
// Identity wrap β every tool's response and thrown error gets stamped
|
|
81
81
|
// with our MCP name so cross-MCP attribution is unambiguous.
|
|
@@ -1085,7 +1085,7 @@ export default {
|
|
|
1085
1085
|
});
|
|
1086
1086
|
const statelessServer = new McpServer({
|
|
1087
1087
|
name: "Figma Console MCP",
|
|
1088
|
-
version: "1.40.
|
|
1088
|
+
version: "1.40.2",
|
|
1089
1089
|
});
|
|
1090
1090
|
wrapServerForIdentity(statelessServer);
|
|
1091
1091
|
// ================================================================
|
|
@@ -1723,7 +1723,7 @@ export default {
|
|
|
1723
1723
|
return new Response(JSON.stringify({
|
|
1724
1724
|
status: "healthy",
|
|
1725
1725
|
service: "Figma Console MCP",
|
|
1726
|
-
version: "1.40.
|
|
1726
|
+
version: "1.40.2",
|
|
1727
1727
|
endpoints: {
|
|
1728
1728
|
mcp: ["/sse", "/mcp"],
|
|
1729
1729
|
oauth_mcp_spec: ["/.well-known/oauth-authorization-server", "/authorize", "/token", "/oauth/register"],
|
|
@@ -71,7 +71,7 @@ export const ExportTokensInputSchema = z.object({
|
|
|
71
71
|
.string()
|
|
72
72
|
.optional()
|
|
73
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)
|
|
74
|
+
strategy: SyncStrategySchema.optional().describe("How to handle existing output files. Export always replaces a target file's contents with the current Figma state β it does not merge token-by-token. 'merge' (default) is the SAFE mode: it refuses to write, leaving the file untouched, if overwriting an existing DTCG file would delete tokens the export does not manage (hand-added tokens, or tokens from collections not included in this export). 'replace' overwrites unconditionally. 'dry-run' reports what would be written without writing."),
|
|
75
75
|
prefix: z
|
|
76
76
|
.string()
|
|
77
77
|
.optional()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../../src/core/tokens/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC;IACvC,MAAM;IACN,eAAe;IACf,UAAU;IACV,aAAa;IACb,aAAa;IACb,MAAM;IACN,MAAM;IACN,WAAW;IACX,WAAW;IACX,aAAa;IACb,qBAAqB;CACtB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC;IACvC,MAAM;IACN,MAAM;IACN,eAAe;IACf,UAAU;IACV,aAAa;IACb,oBAAoB;IACpB,MAAM;IACN,qBAAqB;IACrB,WAAW;IACX,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;AAE1E,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,IAAI,CAAC;IAC7C,KAAK;IACL,YAAY;IACZ,WAAW;IACX,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAC1E,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,KAAK,EAAE,CAAC;SACL,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;SAC5B,QAAQ,EAAE;SACV,QAAQ,CACP,iHAAiH,CAClH;IACH,aAAa,EAAE,CAAC;SACb,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,EAAE;SACV,QAAQ,CACP,2IAA2I,CAC5I;IACH,KAAK,EAAE,CAAC;SACL,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;SAC9C,QAAQ,EAAE;SACV,QAAQ,CACP,yIAAyI,CAC1I;IACH,MAAM,EAAE,kBAAkB,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAC5C,iQAAiQ,CAClQ;IACD,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,u7BAAu7B,CACx7B;IACH,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,kIAAkI,CACnI;IACH,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAC9C,
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../../src/core/tokens/schemas.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,12 @@ export interface RegisterTokensToolsOptions {
|
|
|
38
38
|
* are all Local Mode only.
|
|
39
39
|
*/
|
|
40
40
|
isRemoteMode?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a Figma fileKey to its display name (Local Mode: from the Desktop
|
|
43
|
+
* Bridge's connected-file registry). Used to tell the caller WHICH file an
|
|
44
|
+
* export actually read from β the bridge reads whatever file is active.
|
|
45
|
+
*/
|
|
46
|
+
resolveFileName?: (fileKey: string) => string | null | undefined;
|
|
41
47
|
}
|
|
42
48
|
export declare function registerExportTokensTool(server: McpServer, getDesktopConnector: () => Promise<any>, opts?: RegisterTokensToolsOptions): void;
|
|
43
49
|
export declare function registerImportTokensTool(server: McpServer, getDesktopConnector: () => Promise<any>, opts?: RegisterTokensToolsOptions): void;
|
|
@@ -45,6 +51,59 @@ export declare function registerImportTokensTool(server: McpServer, getDesktopCo
|
|
|
45
51
|
* Convenience: register both tools at once.
|
|
46
52
|
*/
|
|
47
53
|
export declare function registerTokensTools(server: McpServer, getDesktopConnector: () => Promise<any>, opts?: RegisterTokensToolsOptions): void;
|
|
54
|
+
/** Where an export's variables were read from. */
|
|
55
|
+
export interface ExportSource {
|
|
56
|
+
fileKey: string | null;
|
|
57
|
+
fileName: string | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Throw unless every requested collection id exists in the pulled payload.
|
|
61
|
+
* All-or-nothing on purpose: exporting 1 of 2 requested collections over an
|
|
62
|
+
* existing file is still data loss, just a quieter one.
|
|
63
|
+
*/
|
|
64
|
+
export declare function assertRequestedCollectionsFound(requested: string[] | undefined, available: Array<{
|
|
65
|
+
id: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
}>, source: ExportSource): void;
|
|
68
|
+
/**
|
|
69
|
+
* Record which Figma file a generated TEXT output came from, as a
|
|
70
|
+
* `Source: Figma file <key>` comment line directly under the formatter's
|
|
71
|
+
* existing "Generated by figma-console-mcp" header β in that header's own
|
|
72
|
+
* comment style, so this works for every current and future text format
|
|
73
|
+
* without touching the formatters. JSON outputs are left alone here: DTCG
|
|
74
|
+
* carries the key in its root `$extensions`, and the plain-JSON formats have
|
|
75
|
+
* nowhere to put a comment.
|
|
76
|
+
*/
|
|
77
|
+
export declare function stampSourceFile(content: string, sourceFileKey: string | null): string;
|
|
78
|
+
/**
|
|
79
|
+
* Read back the Figma file key a previously exported file was generated from:
|
|
80
|
+
* DTCG root `$extensions["figma-console-mcp"].figmaFileKey`, or the
|
|
81
|
+
* `Source: Figma file <key>` header line of a text output. Null when the file
|
|
82
|
+
* carries no stamp (exported before stamping existed, or a plain-JSON format).
|
|
83
|
+
*/
|
|
84
|
+
export declare function readSourceFileStamp(rawContent: string): string | null;
|
|
85
|
+
/** Tokens an overwrite would destroy that the export has no authority over. */
|
|
86
|
+
export interface UnmanagedTokenLoss {
|
|
87
|
+
/** Tokens with no figma-console-mcp variable stamp β hand-added / code-only. */
|
|
88
|
+
codeOnly: string[];
|
|
89
|
+
/** Tokens stamped with a collection that is not part of this export. */
|
|
90
|
+
otherCollections: string[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Compare an existing DTCG token file with the content about to replace it.
|
|
94
|
+
*
|
|
95
|
+
* A token that disappears is only a legitimate drop when the export MANAGES
|
|
96
|
+
* it: stamped with a variableId AND belonging to a collection included in this
|
|
97
|
+
* export β i.e. the variable was deleted in Figma. Anything else that would
|
|
98
|
+
* vanish is loss the caller never asked for:
|
|
99
|
+
* β’ code-only tokens (no stamp) someone added by hand, and
|
|
100
|
+
* β’ tokens from collections outside this export β a scoped export pointed at
|
|
101
|
+
* a shared file, or an export read from the WRONG Figma file.
|
|
102
|
+
*
|
|
103
|
+
* Returns null when nothing unmanaged is lost, or when the existing file is
|
|
104
|
+
* not a DTCG JSON document (CSS/SCSS/TS outputs are generated artifacts).
|
|
105
|
+
*/
|
|
106
|
+
export declare function findUnmanagedTokenLoss(existingContent: string, newContent: string, exportedCollectionIds: Set<string>): UnmanagedTokenLoss | null;
|
|
48
107
|
/** A single file the export is going to write, with its resolved destination. */
|
|
49
108
|
export interface PlannedExportWrite {
|
|
50
109
|
format: string;
|
|
@@ -53,6 +112,8 @@ export interface PlannedExportWrite {
|
|
|
53
112
|
/** Absolute destination on disk. */
|
|
54
113
|
fullPath: string;
|
|
55
114
|
content: string;
|
|
115
|
+
/** True when this file holds ONE collection (a splitByCollection target). */
|
|
116
|
+
splitByCollection?: boolean;
|
|
56
117
|
}
|
|
57
118
|
/**
|
|
58
119
|
* Decide where every formatted file lands. `outputPath` is interpreted as:
|
|
@@ -72,6 +133,7 @@ export declare function planExportWrites(files: Array<{
|
|
|
72
133
|
format: string;
|
|
73
134
|
path: string;
|
|
74
135
|
content: string;
|
|
136
|
+
splitByCollection?: boolean;
|
|
75
137
|
}>, outputPath: string | undefined, loaded: ReturnType<typeof loadTokensConfig>): {
|
|
76
138
|
writes: PlannedExportWrite[];
|
|
77
139
|
warnings: string[];
|