figma-console-mcp 1.29.1 → 1.30.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
@@ -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
- > **🆕 Shared Library Inspection (v1.29.0, patched v1.29.1):** Three new tools fill the gap between "I see a component key in search results" and "I can actually use it." `figma_get_library_component_by_key` resolves any component key to full property definitions + variant keys + visual specs without needing the source library file's URL. `figma_get_library_variables` lists every variable from your subscribed libraries (no Enterprise plan required uses the Plugin API path that works on Pro and Org). `figma_import_library_variable` brings a library token into the current file so it can be bound to nodes alongside the file's own variables. Works on every Figma plan. 106 tools total. **Patched v1.29.1:** `figma_get_design_system_kit` now reads variables on any plan via the bridge too — no more 403 for non-Enterprise users. [See what's new →](CHANGELOG.md#1291---2026-05-30)
11
+ > **🆕 Native variable binding & typography (v1.30.0):** The structured write tools now handle the operations that used to force you into raw `figma_execute`. `figma_set_fills` / `figma_set_strokes` bind a fill or stroke to a color variable via a new `variableId` param (works on any Figma plan through the bridge no Enterprise Variables API). `figma_set_text` gains `fontFamily` / `fontStyle` with space-insensitive normalization (`SemiBold` `Semi Bold`) and graceful fallback. `figma_instantiate_component` now pre-loads instance fonts before text overrides (no more silently-skipped overrides) and reports failed overrides in a `warnings` array. Works on every Figma plan. [See what's new →](CHANGELOG.md#1300---2026-06-02)
12
12
 
13
13
  ## What is this?
14
14
 
@@ -808,9 +808,11 @@ The architecture supports adding new apps with minimal boilerplate — each app
808
808
 
809
809
  ## 🛤️ Roadmap
810
810
 
811
- **Current Status:** v1.29.1 (Stable) - Production-ready with 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, **106 tools** (Local) / **95 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
811
+ **Current Status:** v1.30.0 (Stable) - Production-ready with 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, **106 tools** (Local) / **95 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
812
812
 
813
813
  **Recent Releases:**
814
+ - [x] **v1.30.0** - Native variable binding + typography in the structured write tools, closing the Plugin API gaps that used to force raw `figma_execute`. `figma_set_fills` / `figma_set_strokes` accept a `variableId` to bind a fill/stroke to a color variable via `setBoundVariableForPaint` (any plan, via the bridge). `figma_set_text` gains `fontFamily` / `fontStyle` with space-insensitive normalization (`SemiBold` → `Semi Bold`) and graceful `Regular` fallback. `figma_instantiate_component` pre-loads instance text fonts before applying overrides (fixes silently-skipped text overrides on non-Regular weights) and returns a `warnings` array for failed overrides. Also fixes a mixed-font crash in `figma_set_text` and a `ui.html` relay that was dropping new message fields. No new tools; **plugin re-import required** (bridge `ui.html` + `code.js` changed). Validated live; 1185 tests passing.
815
+ - [x] **v1.29.2** - Bug fix: `figma_generate_component_doc` now renders Figma component descriptions faithfully and reliably tags atomic-design level. Single-`#` headings in descriptions render as real sections (Usage Guidelines, Implementation Considerations, Accessibility Requirements, Content Configuration) instead of leaking as `- # Heading` list items; frontmatter `description` takes the first sentence instead of truncating on the word "Accessibility"; the generated Figma URL no longer doubles `?node-id=`; and the component's atomic level (atom/molecule/organism/template) is auto-detected via a single `ids=<node>` file request + divider walk-back, with no dependency on library publishing. No new tools; plugin re-import not required.
814
816
  - [x] **v1.29.1** - Bug fix: `figma_get_design_system_kit` now resolves variables bridge-first (Desktop Bridge / cloud relay → REST fallback) instead of calling the Enterprise-only Variables REST API directly. Non-Enterprise users no longer hit a 403 on the kit's token section when a bridge is connected, and a REST 403 now points the caller back to the bridge instead of dead-ending. 7 new tests, 1185 total passing. No new tools; plugin re-import not required.
815
817
  - [x] **v1.29.0** - Shared library inspection: three new tools close the gap between "I have a component key" and "I can actually use it." `figma_get_library_component_by_key` resolves any 40-char component key to full `componentPropertyDefinitions` + variants (with their published keys) + per-variant visual specs — without needing the source library file's URL. `figma_get_library_variables` lists library tokens via Plugin API (works on every Figma plan; the REST equivalent is Enterprise-only). `figma_import_library_variable` imports a library token to the current file so it can be bound to nodes. 27 new tests, 1178 total passing. Plugin re-import optional.
816
818
  - [x] **v1.28.1** - Bug fix patch surfacing from live-fire testing of the v1.28.0 formatters against multi-tier semantic-token design systems. Fixes: Tailwind v3 emitted empty `module.exports` for alias-only sets (now resolves alias chains to literal values); TypeScript module + JSON flat + JSON nested formatters emitted `"{alias.path}"` strings as literal values (now resolves); Tailwind v4 namespace-prefix doubling (`--color-theme-color-X` is now `--color-theme-X`). Adds `resolveAliasChain` public helper. 1151 tests still passing.
@@ -174,7 +174,7 @@ export function parseComponentDescription(description) {
174
174
  for (const line of lines) {
175
175
  const trimmed = line.trim();
176
176
  // Detect section headers: bold text (**Header**), markdown headers (## Header), or plain text exact matches
177
- const markdownHeaderMatch = trimmed.match(/^(?:\*\*|###?\s*)(.+?)(?:\*\*)?$/);
177
+ const markdownHeaderMatch = trimmed.match(/^(?:\*\*|#{1,6}\s*)(.+?)(?:\*\*)?$/);
178
178
  const headerText = markdownHeaderMatch ? markdownHeaderMatch[1].trim().replace(/\*\*/g, "") : null;
179
179
  // Check if this is a Figma per-property documentation block (e.g., "Show Left Icon: True – Purpose")
180
180
  // These should be routed to "other" to avoid polluting content guidelines and accessibility sections
@@ -1380,7 +1380,47 @@ function buildParityInstruction(componentName, parityScore, counts, canonicalSou
1380
1380
  // ============================================================================
1381
1381
  // Documentation Section Generators
1382
1382
  // ============================================================================
1383
- function generateFrontmatter(componentName, description, node, componentMeta, fileUrl, codeInfo, canonicalSource) {
1383
+ /**
1384
+ * Detect the atomic-design level (atom | molecule | organism | template) of a component
1385
+ * by finding its Figma page and walking the ordered page list back to the nearest
1386
+ * section-divider page (e.g. "ATOMS", "MOLECULES", "ORGANISMS"). Returns null when the
1387
+ * file doesn't use atomic-design page sections or the page can't be resolved — callers
1388
+ * then simply omit the `level` frontmatter. Best-effort and never throws.
1389
+ */
1390
+ async function detectAtomicLevel(api, fileKey, nodeId, setNodeId, _componentMeta, _allComponentsMeta) {
1391
+ try {
1392
+ const targetId = setNodeId || nodeId;
1393
+ // Resolve the page the component lives on — independent of library-publish
1394
+ // status (published `containing_frame` metadata is empty for many files).
1395
+ // Requesting the file with `ids` returns every page in document order, but
1396
+ // prunes each page's children to only the path reaching the requested node,
1397
+ // so the single page whose subtree still contains the node is its home page.
1398
+ const pages = (await api.getFile(fileKey, { ids: [targetId] }))?.document?.children || [];
1399
+ const contains = (n) => n?.id === targetId || (Array.isArray(n?.children) && n.children.some(contains));
1400
+ const idx = pages.findIndex((p) => contains(p));
1401
+ if (idx < 0)
1402
+ return null;
1403
+ // Walk back to the nearest atomic-design divider page.
1404
+ const LEVELS = [
1405
+ ["ATOM", "atom"],
1406
+ ["MOLECULE", "molecule"],
1407
+ ["ORGANISM", "organism"],
1408
+ ["TEMPLATE", "template"],
1409
+ ];
1410
+ for (let i = idx; i >= 0; i--) {
1411
+ const stripped = (pages[i]?.name || "").toUpperCase().replace(/[^A-Z]/g, "");
1412
+ for (const [marker, level] of LEVELS) {
1413
+ if (stripped.startsWith(marker))
1414
+ return level;
1415
+ }
1416
+ }
1417
+ return null;
1418
+ }
1419
+ catch {
1420
+ return null;
1421
+ }
1422
+ }
1423
+ function generateFrontmatter(componentName, description, node, componentMeta, fileUrl, codeInfo, canonicalSource, level) {
1384
1424
  const status = codeInfo?.changelog?.[0]
1385
1425
  ? "stable"
1386
1426
  : componentMeta?.description?.toLowerCase().includes("deprecated")
@@ -1388,6 +1428,8 @@ function generateFrontmatter(componentName, description, node, componentMeta, fi
1388
1428
  : "stable";
1389
1429
  const version = codeInfo?.changelog?.[0]?.version || "1.0.0";
1390
1430
  const tags = [componentName.toLowerCase()];
1431
+ if (level)
1432
+ tags.push(level);
1391
1433
  if (node.type === "COMPONENT_SET")
1392
1434
  tags.push("variants");
1393
1435
  if (node.componentPropertyDefinitions)
@@ -1395,10 +1437,11 @@ function generateFrontmatter(componentName, description, node, componentMeta, fi
1395
1437
  const lines = [
1396
1438
  "---",
1397
1439
  `title: ${componentName}`,
1398
- `description: ${(description.split(/(?:When to Use|When NOT to Use|Variants|Content Requirements|Accessibility)/i)[0] || description).replace(/\n/g, " ").replace(/\s+/g, " ").trim() || `${componentName} component`}`,
1440
+ `description: ${((description.split(/\n\s*\n|\n#{1,6}\s|\n\*\*/)[0] || description).replace(/\n/g, " ").replace(/\s+/g, " ").trim().split(/(?<=[.!?])\s+/)[0] || `${componentName} component`)}`,
1399
1441
  `status: ${status}`,
1400
1442
  `version: ${version}`,
1401
1443
  `category: components`,
1444
+ ...(level ? [`level: ${level}`] : []),
1402
1445
  `tags: [${tags.join(", ")}]`,
1403
1446
  `figma: ${fileUrl}`,
1404
1447
  ];
@@ -2560,7 +2603,9 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2560
2603
  logger.warn("Desktop Bridge fetch failed, proceeding without bridge-sourced data");
2561
2604
  }
2562
2605
  }
2563
- const fileUrl_ = `${url}?node-id=${nodeId.replace(":", "-")}`;
2606
+ // Strip any existing query (e.g. the connected file's ?node-id=<page>) before
2607
+ // appending the target node, otherwise the URL ends up with a doubled ?node-id=.
2608
+ const fileUrl_ = `${url.split("?")[0]}?node-id=${nodeId.replace(":", "-")}`;
2564
2609
  // Parse the component description for structured content
2565
2610
  const parsedDesc = parseComponentDescription(description);
2566
2611
  // Determine canonical source
@@ -2589,7 +2634,8 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2589
2634
  const parts = [];
2590
2635
  const includedSections = [];
2591
2636
  if (includeFrontmatter) {
2592
- parts.push(generateFrontmatter(componentName, description, node, componentMeta, fileUrl_, codeInfo, canonicalSource));
2637
+ const atomicLevel = await detectAtomicLevel(api, fileKey, nodeId, setInfo.setNodeId, componentMeta, allComponentsMeta);
2638
+ parts.push(generateFrontmatter(componentName, description, node, componentMeta, fileUrl_, codeInfo, canonicalSource, atomicLevel));
2593
2639
  parts.push("");
2594
2640
  }
2595
2641
  if (s.overview) {
@@ -7,7 +7,8 @@
7
7
  * to generate code with structural fidelity to the real design system.
8
8
  */
9
9
  import { z } from "zod";
10
- import { extractFileKey, formatVariables } from "./figma-api.js";
10
+ import { extractFileKey } from "./figma-api.js";
11
+ import { resolveFormattedVariables } from "./variable-resolver.js";
11
12
  import { createChildLogger } from "./logger.js";
12
13
  const logger = createChildLogger({ component: "design-system-tools" });
13
14
  // ============================================================================
@@ -420,13 +421,15 @@ function compressKit(kit, level) {
420
421
  // ============================================================================
421
422
  // Tool Registration
422
423
  // ============================================================================
423
- export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, variablesCache, options) {
424
+ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, variablesCache, options, getDesktopConnector) {
424
425
  server.tool("figma_get_design_system_kit", "PREFERRED TOOL for design system extraction — replaces separate figma_get_styles, figma_get_variables, and figma_get_component calls. " +
425
426
  "Returns tokens, components, and styles in a single optimized response with adaptive compression for large systems. " +
426
427
  "Includes component visual specs (exact colors, padding, typography, layout), rendered screenshots, " +
427
428
  "token values per mode (light/dark), and resolved style values. " +
428
429
  "Use this instead of calling individual tools to avoid context window overflow. " +
429
- "Ideal for AI code generation — use visualSpec for pixel-accurate reproduction.", {
430
+ "Ideal for AI code generation — use visualSpec for pixel-accurate reproduction. " +
431
+ "Tokens/variables are read through the connected Desktop Bridge or cloud relay and work on ANY Figma plan — no Enterprise required. " +
432
+ "If a tokens fetch ever reports the Variables REST API is plan-limited (403), the bridge/relay is the plan-independent path: ensure it's connected and retry rather than abandoning variables.", {
430
433
  fileKey: z
431
434
  .string()
432
435
  .optional()
@@ -482,25 +485,32 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
482
485
  try {
483
486
  logger.info({ fileKey: resolvedFileKey }, "Fetching design tokens");
484
487
  // Check cache first
485
- let variablesData = null;
486
488
  const cacheKey = `vars:${resolvedFileKey}`;
489
+ let formatted = null;
487
490
  if (variablesCache) {
488
491
  const cached = variablesCache.get(cacheKey);
489
492
  if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
490
- variablesData = cached.data;
493
+ formatted = cached.data;
491
494
  logger.info("Using cached variables data");
492
495
  }
493
496
  }
494
- if (!variablesData) {
495
- variablesData = await withTimeout(api.getLocalVariables(resolvedFileKey), 30000, "getLocalVariables");
497
+ if (!formatted) {
498
+ // Bridge-first: the Desktop Bridge / cloud relay reads variables on
499
+ // ANY plan via the Plugin API. The Enterprise-only REST Variables API
500
+ // is the fallback, used only when no bridge is connected — so most
501
+ // users (non-Enterprise) no longer dead-end on a 403 here.
502
+ formatted = await resolveFormattedVariables({
503
+ getDesktopConnector,
504
+ getFigmaAPI,
505
+ fileKey: resolvedFileKey,
506
+ });
496
507
  if (variablesCache) {
497
508
  variablesCache.set(cacheKey, {
498
- data: variablesData,
509
+ data: formatted,
499
510
  timestamp: Date.now(),
500
511
  });
501
512
  }
502
513
  }
503
- const formatted = formatVariables(variablesData);
504
514
  const collections = groupVariablesByCollection(formatted);
505
515
  kit.tokens = {
506
516
  collections,
@@ -30,7 +30,7 @@ const logger = createChildLogger({ component: "tokens-tools" });
30
30
  * on every exported token document. Kept in sync with package.json by
31
31
  * scripts/release.sh — see step 3 of the release flow.
32
32
  */
33
- const MCP_VERSION = "1.29.0";
33
+ const MCP_VERSION = "1.29.2";
34
34
  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.
35
35
 
36
36
  FULLY-IMPLEMENTED OUTPUT FORMATS:
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Bridge-first variable resolution.
3
+ *
4
+ * Figma's Variables REST API (`/files/:key/variables/local`) is **Enterprise-only**
5
+ * and returns 403 for the majority of users (Starter/Pro/Org). The Desktop Bridge
6
+ * and cloud relay read variables through the Plugin API
7
+ * (`figma.variables.getLocalVariablesAsync`), which works on **every** Figma plan.
8
+ *
9
+ * Orchestration tools (e.g. `figma_get_design_system_kit`) historically called the
10
+ * REST API directly and dead-ended on a 403 for non-Enterprise users — even when a
11
+ * bridge was connected. This helper mirrors `figma_get_variables`' resolution order
12
+ * so every variable-reading tool behaves consistently:
13
+ *
14
+ * 1. Desktop Bridge / cloud relay (any plan) ← preferred
15
+ * 2. REST Variables API (Enterprise only) ← fallback
16
+ *
17
+ * It returns the same normalized shape as `formatVariables()`.
18
+ */
19
+ import { formatVariables, withTimeout } from "./figma-api.js";
20
+ import { createChildLogger } from "./logger.js";
21
+ const logger = createChildLogger({ component: "variable-resolver" });
22
+ /**
23
+ * The Desktop Bridge returns variables/collections as **arrays**, while
24
+ * `formatVariables()` expects **objects keyed by id** (the REST shape). Convert so
25
+ * the same formatter handles both transports.
26
+ */
27
+ function bridgeArraysToFormatInput(data) {
28
+ const variables = {};
29
+ for (const v of data.variables || []) {
30
+ if (v && v.id)
31
+ variables[v.id] = v;
32
+ }
33
+ const variableCollections = {};
34
+ for (const c of data.variableCollections || []) {
35
+ if (c && c.id)
36
+ variableCollections[c.id] = c;
37
+ }
38
+ return { variables, variableCollections };
39
+ }
40
+ /**
41
+ * Resolve + format local variables, preferring the Desktop Bridge / cloud relay and
42
+ * falling back to the Enterprise-only REST API only when no bridge is connected.
43
+ *
44
+ * @throws a bridge-pointing error when the bridge is unavailable AND the REST API
45
+ * fails (e.g. 403 without Enterprise), so callers/LLMs retry via the bridge
46
+ * instead of treating variables as inaccessible.
47
+ */
48
+ export async function resolveFormattedVariables(opts) {
49
+ const { getDesktopConnector, getFigmaAPI, fileKey } = opts;
50
+ const timeoutMs = opts.timeoutMs ?? 30000;
51
+ // 1. Desktop Bridge / cloud relay — works on ANY plan. Preferred.
52
+ if (getDesktopConnector) {
53
+ try {
54
+ const connector = await getDesktopConnector();
55
+ const raw = await withTimeout(connector.getVariables(fileKey), timeoutMs, "Desktop Bridge variables");
56
+ // EXECUTE_CODE responses nest the return value under `result`; unwrap so
57
+ // both the live and cached plugin paths produce a uniform shape. See #68.
58
+ const data = raw?.result?.variables ? raw.result : raw;
59
+ if (data?.success && Array.isArray(data.variables)) {
60
+ const formatted = formatVariables(bridgeArraysToFormatInput(data));
61
+ logger.info({ source: "desktop_bridge", variableCount: formatted.variables.length }, "Resolved variables via Desktop Bridge");
62
+ return { ...formatted, source: "desktop_bridge" };
63
+ }
64
+ logger.warn({ fileKey, error: data?.error }, "Desktop Bridge returned no variables; falling back to REST API");
65
+ }
66
+ catch (err) {
67
+ logger.warn({ fileKey, error: err instanceof Error ? err.message : String(err) }, "Desktop Bridge variable fetch failed; falling back to REST API");
68
+ }
69
+ }
70
+ // 2. REST Variables API — Enterprise only. Last resort.
71
+ const api = await getFigmaAPI();
72
+ try {
73
+ const local = await withTimeout(api.getLocalVariables(fileKey), timeoutMs, "getLocalVariables");
74
+ const formatted = formatVariables(local);
75
+ logger.info({ source: "rest_api", variableCount: formatted.variables.length }, "Resolved variables via REST API");
76
+ return { ...formatted, source: "rest_api" };
77
+ }
78
+ catch (restErr) {
79
+ const msg = restErr instanceof Error ? restErr.message : String(restErr);
80
+ throw new Error(`[figma-console-mcp] Could not read variables. The Figma Variables REST API ` +
81
+ `is unavailable for this file (${msg}) — it requires an Enterprise plan. ` +
82
+ `Connect the Figma Console MCP Desktop Bridge plugin (or pair it via Cloud Mode) ` +
83
+ `to read variables on ANY plan, then retry.`);
84
+ }
85
+ }
@@ -74,7 +74,7 @@ export class FigmaConsoleMCPv3 extends McpAgent {
74
74
  this.server = (() => {
75
75
  const s = new McpServer({
76
76
  name: "Figma Console MCP",
77
- version: "1.29.0",
77
+ version: "1.29.2",
78
78
  });
79
79
  // Identity wrap — every tool's response and thrown error gets stamped
80
80
  // with our MCP name so cross-MCP attribution is unambiguous.
@@ -809,7 +809,7 @@ export class FigmaConsoleMCPv3 extends McpAgent {
809
809
  registerVersionTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, { isRemoteMode: true });
810
810
  // Register Design System Kit tool
811
811
  registerDesignSystemTools(this.server, async () => await this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, undefined, // variablesCache
812
- { isRemoteMode: true });
812
+ { isRemoteMode: true }, getCloudDesktopConnector);
813
813
  // Register Library Tools (key-based component inspection across shared libraries)
814
814
  registerLibraryTools(this.server, async () => await this.getFigmaAPI());
815
815
  // Register Library Variable Tools (Plugin-API based — list + import variables
@@ -1083,7 +1083,7 @@ export default {
1083
1083
  });
1084
1084
  const statelessServer = new McpServer({
1085
1085
  name: "Figma Console MCP",
1086
- version: "1.29.0",
1086
+ version: "1.29.2",
1087
1087
  });
1088
1088
  wrapServerForIdentity(statelessServer);
1089
1089
  // ================================================================
@@ -1180,7 +1180,7 @@ export default {
1180
1180
  registerCommentTools(statelessServer, async () => statelessApi, getCloudFileUrl);
1181
1181
  registerVersionTools(statelessServer, async () => statelessApi, getCloudFileUrl);
1182
1182
  registerDesignSystemTools(statelessServer, async () => statelessApi, getCloudFileUrl, new Map(), // Fresh variables cache per request
1183
- { isRemoteMode: true });
1183
+ { isRemoteMode: true }, getCloudDesktopConnector);
1184
1184
  registerLibraryTools(statelessServer, async () => statelessApi);
1185
1185
  registerLibraryVariableTools(statelessServer, getCloudDesktopConnector);
1186
1186
  await statelessServer.connect(transport);
@@ -1720,7 +1720,7 @@ export default {
1720
1720
  return new Response(JSON.stringify({
1721
1721
  status: "healthy",
1722
1722
  service: "Figma Console MCP",
1723
- version: "1.29.0",
1723
+ version: "1.29.2",
1724
1724
  endpoints: {
1725
1725
  mcp: ["/sse", "/mcp"],
1726
1726
  oauth_mcp_spec: ["/.well-known/oauth-authorization-server", "/authorize", "/token", "/oauth/register"],
@@ -1 +1 @@
1
- {"version":3,"file":"design-code-tools.d.ts","sourceRoot":"","sources":["../../src/core/design-code-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,KAAK,EAUX,uBAAuB,EACvB,MAAM,wBAAwB,CAAC;AAUhC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1F,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzG;AAiED,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBpG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUxD;AAMD,sEAAsE;AACtE,UAAU,iBAAiB;IAC1B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sCAAsC;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,uEAAuE;IACvE,iBAAiB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC/D,0BAA0B;IAC1B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,yCAAyC;IACzC,eAAe,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,CA0KhF;AAMD,mDAAmD;AACnD,UAAU,gBAAgB;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5F,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9F,UAAU,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7C;AAED,uCAAuC;AACvC,UAAU,aAAa;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,gBAAgB,EAAE,CAqBpG;AAoED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,aAAa,EAAE,CAiCzG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAqB3F;AAmHD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAKhD;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,uDAAuD;AACvD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AA8uDD,gFAAgF;AAChF,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GACjB,uBAAuB,CAazB;AAyJD,wBAAgB,uBAAuB,CACtC,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;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,EACpC,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACtC,IAAI,CAwjBN"}
1
+ {"version":3,"file":"design-code-tools.d.ts","sourceRoot":"","sources":["../../src/core/design-code-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,KAAK,EAUX,uBAAuB,EACvB,MAAM,wBAAwB,CAAC;AAUhC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1F,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzG;AAiED,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBpG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUxD;AAMD,sEAAsE;AACtE,UAAU,iBAAiB;IAC1B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,sCAAsC;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,uEAAuE;IACvE,iBAAiB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC/D,0BAA0B;IAC1B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,yCAAyC;IACzC,eAAe,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,CA0KhF;AAMD,mDAAmD;AACnD,UAAU,gBAAgB;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5F,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9F,UAAU,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7C;AAED,uCAAuC;AACvC,UAAU,aAAa;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,gBAAgB,EAAE,CAqBpG;AAoED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,aAAa,EAAE,CAiCzG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,GAAE,MAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,MAAM,CAqB3F;AAmHD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,CAKhD;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,uDAAuD;AACvD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAiyDD,gFAAgF;AAChF,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GACjB,uBAAuB,CAazB;AAyJD,wBAAgB,uBAAuB,CACtC,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;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,EACpC,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GACtC,IAAI,CA2jBN"}
@@ -174,7 +174,7 @@ export function parseComponentDescription(description) {
174
174
  for (const line of lines) {
175
175
  const trimmed = line.trim();
176
176
  // Detect section headers: bold text (**Header**), markdown headers (## Header), or plain text exact matches
177
- const markdownHeaderMatch = trimmed.match(/^(?:\*\*|###?\s*)(.+?)(?:\*\*)?$/);
177
+ const markdownHeaderMatch = trimmed.match(/^(?:\*\*|#{1,6}\s*)(.+?)(?:\*\*)?$/);
178
178
  const headerText = markdownHeaderMatch ? markdownHeaderMatch[1].trim().replace(/\*\*/g, "") : null;
179
179
  // Check if this is a Figma per-property documentation block (e.g., "Show Left Icon: True – Purpose")
180
180
  // These should be routed to "other" to avoid polluting content guidelines and accessibility sections
@@ -1380,7 +1380,47 @@ function buildParityInstruction(componentName, parityScore, counts, canonicalSou
1380
1380
  // ============================================================================
1381
1381
  // Documentation Section Generators
1382
1382
  // ============================================================================
1383
- function generateFrontmatter(componentName, description, node, componentMeta, fileUrl, codeInfo, canonicalSource) {
1383
+ /**
1384
+ * Detect the atomic-design level (atom | molecule | organism | template) of a component
1385
+ * by finding its Figma page and walking the ordered page list back to the nearest
1386
+ * section-divider page (e.g. "ATOMS", "MOLECULES", "ORGANISMS"). Returns null when the
1387
+ * file doesn't use atomic-design page sections or the page can't be resolved — callers
1388
+ * then simply omit the `level` frontmatter. Best-effort and never throws.
1389
+ */
1390
+ async function detectAtomicLevel(api, fileKey, nodeId, setNodeId, _componentMeta, _allComponentsMeta) {
1391
+ try {
1392
+ const targetId = setNodeId || nodeId;
1393
+ // Resolve the page the component lives on — independent of library-publish
1394
+ // status (published `containing_frame` metadata is empty for many files).
1395
+ // Requesting the file with `ids` returns every page in document order, but
1396
+ // prunes each page's children to only the path reaching the requested node,
1397
+ // so the single page whose subtree still contains the node is its home page.
1398
+ const pages = (await api.getFile(fileKey, { ids: [targetId] }))?.document?.children || [];
1399
+ const contains = (n) => n?.id === targetId || (Array.isArray(n?.children) && n.children.some(contains));
1400
+ const idx = pages.findIndex((p) => contains(p));
1401
+ if (idx < 0)
1402
+ return null;
1403
+ // Walk back to the nearest atomic-design divider page.
1404
+ const LEVELS = [
1405
+ ["ATOM", "atom"],
1406
+ ["MOLECULE", "molecule"],
1407
+ ["ORGANISM", "organism"],
1408
+ ["TEMPLATE", "template"],
1409
+ ];
1410
+ for (let i = idx; i >= 0; i--) {
1411
+ const stripped = (pages[i]?.name || "").toUpperCase().replace(/[^A-Z]/g, "");
1412
+ for (const [marker, level] of LEVELS) {
1413
+ if (stripped.startsWith(marker))
1414
+ return level;
1415
+ }
1416
+ }
1417
+ return null;
1418
+ }
1419
+ catch {
1420
+ return null;
1421
+ }
1422
+ }
1423
+ function generateFrontmatter(componentName, description, node, componentMeta, fileUrl, codeInfo, canonicalSource, level) {
1384
1424
  const status = codeInfo?.changelog?.[0]
1385
1425
  ? "stable"
1386
1426
  : componentMeta?.description?.toLowerCase().includes("deprecated")
@@ -1388,6 +1428,8 @@ function generateFrontmatter(componentName, description, node, componentMeta, fi
1388
1428
  : "stable";
1389
1429
  const version = codeInfo?.changelog?.[0]?.version || "1.0.0";
1390
1430
  const tags = [componentName.toLowerCase()];
1431
+ if (level)
1432
+ tags.push(level);
1391
1433
  if (node.type === "COMPONENT_SET")
1392
1434
  tags.push("variants");
1393
1435
  if (node.componentPropertyDefinitions)
@@ -1395,10 +1437,11 @@ function generateFrontmatter(componentName, description, node, componentMeta, fi
1395
1437
  const lines = [
1396
1438
  "---",
1397
1439
  `title: ${componentName}`,
1398
- `description: ${(description.split(/(?:When to Use|When NOT to Use|Variants|Content Requirements|Accessibility)/i)[0] || description).replace(/\n/g, " ").replace(/\s+/g, " ").trim() || `${componentName} component`}`,
1440
+ `description: ${((description.split(/\n\s*\n|\n#{1,6}\s|\n\*\*/)[0] || description).replace(/\n/g, " ").replace(/\s+/g, " ").trim().split(/(?<=[.!?])\s+/)[0] || `${componentName} component`)}`,
1399
1441
  `status: ${status}`,
1400
1442
  `version: ${version}`,
1401
1443
  `category: components`,
1444
+ ...(level ? [`level: ${level}`] : []),
1402
1445
  `tags: [${tags.join(", ")}]`,
1403
1446
  `figma: ${fileUrl}`,
1404
1447
  ];
@@ -2560,7 +2603,9 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2560
2603
  logger.warn("Desktop Bridge fetch failed, proceeding without bridge-sourced data");
2561
2604
  }
2562
2605
  }
2563
- const fileUrl_ = `${url}?node-id=${nodeId.replace(":", "-")}`;
2606
+ // Strip any existing query (e.g. the connected file's ?node-id=<page>) before
2607
+ // appending the target node, otherwise the URL ends up with a doubled ?node-id=.
2608
+ const fileUrl_ = `${url.split("?")[0]}?node-id=${nodeId.replace(":", "-")}`;
2564
2609
  // Parse the component description for structured content
2565
2610
  const parsedDesc = parseComponentDescription(description);
2566
2611
  // Determine canonical source
@@ -2589,7 +2634,8 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2589
2634
  const parts = [];
2590
2635
  const includedSections = [];
2591
2636
  if (includeFrontmatter) {
2592
- parts.push(generateFrontmatter(componentName, description, node, componentMeta, fileUrl_, codeInfo, canonicalSource));
2637
+ const atomicLevel = await detectAtomicLevel(api, fileKey, nodeId, setInfo.setNodeId, componentMeta, allComponentsMeta);
2638
+ parts.push(generateFrontmatter(componentName, description, node, componentMeta, fileUrl_, codeInfo, canonicalSource, atomicLevel));
2593
2639
  parts.push("");
2594
2640
  }
2595
2641
  if (s.overview) {