@muloka/figma-console-mcp 0.1.2 β†’ 0.3.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.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  > **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**.
20
20
 
21
- > **πŸ†• Figma Slots Support (v1.35.0):** Create, inspect, populate, and reset Figma Slots (GA at Config 2026) programmatically β€” `figma_create_slot` adds slots to components (variants included), `figma_append_to_slot` populates instance slots with cloned or new content, plus `figma_get_slots`, `figma_reset_slot`, and `figma_add_slot_property`. Live-validated against the GA Plugin API; closes the most-requested issue [#29](https://github.com/southleft/figma-console-mcp/issues/29), based on community PR [#77](https://github.com/southleft/figma-console-mcp/pull/77). **Plugin re-import required** (`code.js` + `ui.html` changed). [See what's new β†’](CHANGELOG.md#1350---2026-07-09)
21
+ > **πŸ†• Target Lock for Parallel Work (v1.36.0):** `figma_navigate` now takes a `lock: true` flag that pins the active file β€” so an AI agent can keep working in one Figma file while you work in another, without commands silently routing to the wrong file. New connections, reconnects, and your own selection/page changes in other files no longer move the target; the pin auto-releases if that file disconnects. `figma_list_open_files` reports a `targetLocked` flag for a pre-write guard. Server-only release β€” **no plugin re-import needed**. [See what's new β†’](CHANGELOG.md#1360---2026-07-16)
22
22
 
23
23
  ## What is this?
24
24
 
@@ -829,9 +829,10 @@ The architecture supports adding new apps with minimal boilerplate β€” each app
829
829
 
830
830
  ## πŸ›€οΈ Roadmap
831
831
 
832
- **Current Status:** v1.35.0 (Stable) - Production-ready. Latest: 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, **112 tools** (Local) / **101 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
832
+ **Current Status:** v1.36.0 (Stable) - Production-ready. Latest: 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 (server-only, no plugin re-import). 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, **112 tools** (Local) / **101 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
833
833
 
834
834
  **Recent Releases:**
835
+ - [x] **v1.36.0** - Target lock for multi-file parallel work. `figma_navigate` gains a `lock: true` flag that pins the active file β€” new connections, reconnects, and the user's own selection/page changes in other files no longer move the command target, so an agent can safely write to one file while the user works in another. Auto-releases when the pinned file disconnects or navigates away; `figma_list_open_files` reports a `targetLocked` flag for pre-write guards. Server-only β€” no plugin re-import needed.
835
836
  - [x] **v1.35.0** - Figma Slots write support, closing [#29](https://github.com/southleft/figma-console-mcp/issues/29) (the tracker's most-requested feature) via community PR [#77](https://github.com/southleft/figma-console-mcp/pull/77) by @simonesalvucci, updated to the GA API. Five new tools: `figma_create_slot` (slot + auto-linked SLOT property; variants inside component sets supported β€” the beta restriction was lifted at GA), `figma_get_slots`, `figma_append_to_slot` (clone or create content into instance slots; snaps clones to the slot origin), `figma_reset_slot`, and `figma_add_slot_property` (retrofit an existing frame as a slot). Hardened by live validation: VARIANT `defaultValue` passthrough restored (Figma requires non-empty), destructive-path reorder in the append handler (content validated before `clearExisting` empties anything), relay whitelist fix for `slot` payloads. **Plugin re-import required** (`code.js` + `ui.html` changed).
836
837
  - [x] **v1.34.0** - Bidirectional Token Sync v2 + DTCG 2025.10. `figma_import_tokens` now applies the *complete* diff plan: missing collections and variables are created (with modes, inferred/recorded types, and values set in dependency order β€” aliases in a second pass), token-path renames route to the update phase by round-trip variable ID (no more create+delete pairs that would permanently destroy the original under `replace`), reference values write real `{ type: "VARIABLE_ALIAS", id }` payloads via a four-tier resolver, and deletes are strictly gated behind `strategy: "replace"`. `figma_export_tokens` gains `dtcgDialect: "2025"` (object-form colors from full-precision floats, object dimensions) while the legacy default stays byte-identical; import accepts both dialects unconditionally with dialect-insensitive diff normalization. Variable `scopes` + `codeSyntax` round-trip through `$extensions["figma-console-mcp"]`. `figma_setup_design_tokens` accepts DTCG brace references (`"{color.blue.600}"`) that resolve to real aliases, including forward references. New tool `figma_create_component_set` builds a variant set from an axes matrix (or combines existing components) with `Prop=Value` naming, optional auto-arranged grid, and variant keys in the response β€” with count-scaled timeouts and rollback on failure. **Plugin re-import required** (`code.js` + `ui.html` changed β€” the component-set handler and relay). 183 tests across the token/write-tools suites.
837
838
  - [x] **v1.33.2** - Version-handshake false-positive fix. The v1.33.0 handshake compared the plugin's reported version against the server's *package* version, so server-only releases (like the v1.33.1 dependency sweep) flagged every up-to-date plugin as stale and pushed the re-import banner for files that hadn't changed. The server now compares against the `PLUGIN_VERSION` embedded in the `figma-desktop-bridge/code.js` it ships β€” exactly what a re-import would install β€” and `PLUGIN_VERSION` itself now means "last release in which plugin files changed" (release tooling bumps it only when `figma-desktop-bridge/` actually changed since the last tag). `figma_get_status` gains `transport.websocket.bundledPluginVersion`; `figma_diagnose` blames the right version. No new tools, **no plugin re-import required** (one-time exception: if you re-imported at v1.33.1, the banner appears once more β€” clear it with one final re-import). 1245 tests passing (9 new).
@@ -1 +1 @@
1
- {"version":3,"file":"figma-tools.d.ts","sourceRoot":"","sources":["../../src/core/figma-tools.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,QAAQ,EAAgB,MAAM,gBAAgB,CAAC;AAwwB7D;;GAEG;AACH,UAAU,oBAAoB;IAC7B,iGAAiG;IACjG,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,EACpC,aAAa,EAAE,MAAM,MAAM,GAAG,IAAI,EAClC,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,EAC9D,OAAO,CAAC,EAAE,oBAAoB,EAC9B,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,QAglGxC"}
1
+ {"version":3,"file":"figma-tools.d.ts","sourceRoot":"","sources":["../../src/core/figma-tools.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,QAAQ,EAAgB,MAAM,gBAAgB,CAAC;AAwwB7D;;GAEG;AACH,UAAU,oBAAoB;IAC7B,iGAAiG;IACjG,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,EACpC,aAAa,EAAE,MAAM,MAAM,GAAG,IAAI,EAClC,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,EAC9D,OAAO,CAAC,EAAE,oBAAoB,EAC9B,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,QAqoGxC"}
@@ -951,21 +951,24 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
951
951
  .optional()
952
952
  .default(false)
953
953
  .describe("DEPRECATED β€” setting this to true now raises an explicit error. The Puppeteer-based console parser no longer exists. Open the Figma Console MCP Desktop Bridge plugin in Figma Desktop and call figma_get_variables() without parseFromConsole; the plugin returns full variable data through the WebSocket bridge."),
954
+ // NOTE: deliberately no .default() on either param. Every consumer below
955
+ // applies its own fallback (?? 1 / ?? 50), and leaving these undefined is
956
+ // what lets format='full' tell "caller asked for page 2" apart from
957
+ // "caller passed nothing" β€” without that distinction, honoring them would
958
+ // silently truncate every full response to 50 variables.
954
959
  page: z
955
960
  .number()
956
961
  .int()
957
962
  .min(1)
958
963
  .optional()
959
- .default(1)
960
- .describe("Page number for paginated results (1-based). Use when response is too large (>1MB). Each page returns up to 50 variables."),
964
+ .describe("Page number for paginated results (1-based). Use when the response is too large to handle in one call. Applies to format='filtered' and format='full'. Default: 1. IMPORTANT: a page number beyond the last page is CLAMPED to the last page, not rejected β€” it returns that page's data again with currentPage set to the real last page. Drive iteration off pagination.hasNextPage; a loop that just increments page will repeat the final page forever."),
961
965
  pageSize: z
962
966
  .number()
963
967
  .int()
964
968
  .min(1)
965
969
  .max(100)
966
970
  .optional()
967
- .default(50)
968
- .describe("Number of variables per page (1-100). Default: 50. Smaller values reduce response size."),
971
+ .describe("Number of variables per page. Hard limit 100 β€” higher values are rejected with a validation error, not clamped. Default: 50. Applies to format='filtered' and format='full'. For format='full', pagination is applied only when page or pageSize is explicitly passed; otherwise the complete dataset is returned. SOFT LIMIT: a page that exceeds ~25K estimated tokens is auto-summarized, so the page is discarded rather than returned β€” measured on a 689-variable file, pageSize=100 reached ~16K tokens at standard verbosity and ~20K with verbosity='full' plus resolveAliases. 100 is safe at roughly 200 tokens/variable; prefer 50 when resolving aliases on a mode-heavy file."),
969
972
  resolveAliases: z
970
973
  .boolean()
971
974
  .optional()
@@ -1199,6 +1202,22 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
1199
1202
  }
1200
1203
  else {
1201
1204
  // format === 'full'
1205
+ // Honor page/pageSize when the caller explicitly passed either one.
1206
+ // Previously they were accepted, silently ignored, and omitted from
1207
+ // the response, so a caller had no way to tell paging had not
1208
+ // happened (southleft/figma-console-mcp#98).
1209
+ //
1210
+ // This runs BEFORE the token estimate on purpose: slicing is exactly
1211
+ // how a caller retrieves a large dataset in pieces that fit under the
1212
+ // limit, so summarizing first would defeat the request.
1213
+ //
1214
+ // Both params are undefined unless passed (no zod .default()), so an
1215
+ // un-paginated full request still returns the complete dataset.
1216
+ if (page !== undefined || pageSize !== undefined) {
1217
+ const paginated = paginateVariables(responseData, page ?? 1, pageSize ?? 50);
1218
+ responseData = paginated.data;
1219
+ paginationInfo = paginated.pagination;
1220
+ }
1202
1221
  // Check if we need to auto-summarize
1203
1222
  const estimatedTokens = estimateTokens(responseData);
1204
1223
  if (estimatedTokens > 25000) {
@@ -1435,19 +1454,30 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
1435
1454
  };
1436
1455
  }
1437
1456
  }
1438
- // Apply pagination if requested
1457
+ // Apply pagination if requested.
1458
+ //
1459
+ // page/pageSize no longer carry zod .default() values (see the note on
1460
+ // the schema), so this branch restates the 1/50 it previously received
1461
+ // implicitly. That keeps the REST path's behavior byte-identical: it
1462
+ // has always paginated to 50 whether or not the caller asked. Reaching
1463
+ // this code requires a Figma Enterprise plan, so it cannot be exercised
1464
+ // from this fork β€” left deliberately unchanged rather than "fixed"
1465
+ // blind. The divergent field names here vs. paginateVariables() are
1466
+ // southleft/figma-console-mcp#99.
1467
+ const restPage = page ?? 1;
1468
+ const restPageSize = pageSize ?? 50;
1439
1469
  let paginationInfo;
1440
- if (pageSize) {
1441
- const startIdx = (page - 1) * pageSize;
1442
- const endIdx = startIdx + pageSize;
1470
+ if (restPageSize) {
1471
+ const startIdx = (restPage - 1) * restPageSize;
1472
+ const endIdx = startIdx + restPageSize;
1443
1473
  const totalVars = localFormatted.variables.length;
1444
1474
  paginationInfo = {
1445
- page,
1446
- pageSize,
1475
+ page: restPage,
1476
+ pageSize: restPageSize,
1447
1477
  totalItems: totalVars,
1448
- totalPages: Math.ceil(totalVars / pageSize),
1478
+ totalPages: Math.ceil(totalVars / restPageSize),
1449
1479
  hasNextPage: endIdx < totalVars,
1450
- hasPrevPage: page > 1,
1480
+ hasPrevPage: restPage > 1,
1451
1481
  };
1452
1482
  localFormatted.variables = localFormatted.variables.slice(startIdx, endIdx);
1453
1483
  if (includePublished && publishedFormatted) {
@@ -1627,6 +1657,10 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
1627
1657
  }
1628
1658
  // Apply format logic
1629
1659
  let responseData = dataForCache;
1660
+ // Declared out here so the response builder below can see it β€”
1661
+ // it was previously scoped inside the 'filtered' branch, computed,
1662
+ // and then unreachable at return time.
1663
+ let paginationInfo = null;
1630
1664
  if (format === 'summary') {
1631
1665
  responseData = generateSummary(dataForCache);
1632
1666
  logger.info({ fileKey, estimatedTokens: estimateTokens(responseData) }, 'Generated summary from fetched data');
@@ -1644,7 +1678,6 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
1644
1678
  filteredCount: responseData.variables?.length,
1645
1679
  }, 'Applied filters to fetched data');
1646
1680
  // Apply pagination (CRITICAL - was missing!)
1647
- let paginationInfo = null;
1648
1681
  const paginated = paginateVariables(responseData, page || 1, pageSize || 50);
1649
1682
  responseData = paginated.data;
1650
1683
  paginationInfo = paginated.pagination;
@@ -1694,6 +1727,19 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
1694
1727
  }
1695
1728
  else {
1696
1729
  // format === 'full'
1730
+ // Same fix as the cached path above (southleft#98): honor
1731
+ // page/pageSize when explicitly passed, before the token
1732
+ // estimate, so slicing is what keeps a large dataset under the
1733
+ // limit rather than the summarizer discarding it.
1734
+ //
1735
+ // This branch serves cache-miss requests. It is a separate code
1736
+ // path from the cached one and needed the fix independently β€”
1737
+ // a live check against a warm cache exercises only the other.
1738
+ if (page !== undefined || pageSize !== undefined) {
1739
+ const paginated = paginateVariables(responseData, page ?? 1, pageSize ?? 50);
1740
+ responseData = paginated.data;
1741
+ paginationInfo = paginated.pagination;
1742
+ }
1697
1743
  // Check if we need to auto-summarize
1698
1744
  const estimatedTokens = estimateTokens(responseData);
1699
1745
  if (estimatedTokens > 25000) {
@@ -1773,6 +1819,11 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
1773
1819
  timestamp: dataForCache.timestamp,
1774
1820
  data: responseData,
1775
1821
  cached: false,
1822
+ // paginationInfo was computed above and then dropped on the
1823
+ // floor, so a cache-miss request through the Desktop Bridge
1824
+ // received sliced data with no indication more pages existed
1825
+ // β€” silent truncation. Same shape the cache path returns.
1826
+ ...(paginationInfo && { pagination: paginationInfo }),
1776
1827
  }),
1777
1828
  },
1778
1829
  ],