@atomixstudio/mcp 1.0.26 → 1.0.28

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP (Model Context Protocol) server and CLI for Atomix Design System. Query and sync design tokens from AI coding tools (Cursor, Claude Desktop, Windsurf, etc.) or from the command line.
4
4
 
5
- **Version:** 1.0.25Pro Figma skill gated by `pro_figma_export`; design-in-Figma skill file renamed to `FIGMA-SKILL.md`; get-started and syncAll only add/write FIGMA-SKILL when the design system has Pro Figma access.
5
+ **Version:** 1.0.28Sync to Figma: removes variables/styles not in payload (same collections only); color scopes (Bg = frame fill only, Action/Feedback = all fill); update-or-create for paint/text/effect styles to avoid duplicates; change summary in sync response; instructions AI-tool agnostic.
6
6
 
7
7
  ## Getting Your Credentials
8
8
 
@@ -100,7 +100,7 @@ Run these prompts from your AI tool (e.g. **/--hello**, **/--get-started**):
100
100
 
101
101
  | Prompt | Description |
102
102
  |--------|-------------|
103
- | **/--hello** | Get started — overview, tokens, and tools. Run this first. |
103
+ | **/--hello** | Get started — overview, tokens, essential commands (including **/--sync-to-figma**). Run this first. |
104
104
  | **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
105
105
  | **/--rules** | Governance rules for your AI tool (Cursor, Copilot, Windsurf, etc.). |
106
106
  | **/--sync** | Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use **/--refactor** to migrate deprecated tokens. |
package/dist/index.js CHANGED
@@ -1300,8 +1300,8 @@ var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
1300
1300
  var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
1301
1301
  var FIGMA_CONNECTION_INSTRUCTIONS = {
1302
1302
  installAndRun: "In Figma: Open Plugins and run the Atomix plugin (Atomix Token Extractor). If it's not installed yet, install it from the Figma Community or your team's plugin library, then run it.",
1303
- connect: 'In the plugin UI, tap **Connect to Cursor** and wait until the status shows "Connected".',
1304
- startBridge: "The Figma bridge runs with this MCP server. Ensure Cursor has started this MCP server (e.g. in Cursor settings), then in Figma run the Atomix plugin and click Connect to Cursor."
1303
+ connect: 'In the plugin UI, tap **Connect** and wait until the status shows "Connected".',
1304
+ startBridge: "The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server running (e.g. in MCP settings), then in Figma run the Atomix plugin and tap Connect."
1305
1305
  };
1306
1306
  var bridgeWss = null;
1307
1307
  var pluginWs = null;
@@ -1445,7 +1445,7 @@ function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS)
1445
1445
  const ws = pluginWs;
1446
1446
  if (!ws || ws.readyState !== WebSocket.OPEN) {
1447
1447
  return Promise.reject(
1448
- new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and click Connect to Cursor.")
1448
+ new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect.")
1449
1449
  );
1450
1450
  }
1451
1451
  const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
@@ -1607,7 +1607,8 @@ function buildFigmaPayloadsFromDS(data) {
1607
1607
  }
1608
1608
  if (variables.length === 0 && modes.length === 0) modes.push("Light");
1609
1609
  const dsName = data.meta?.name;
1610
- const collectionName = dsName ? `${dsName} Foundations` : "Foundations";
1610
+ const collectionPrefix = dsName ? `${dsName} ` : "";
1611
+ const colorCollectionName = `${collectionPrefix}Colors`;
1611
1612
  const textStyles = [];
1612
1613
  const sizeToPx = (val, basePx = 16) => {
1613
1614
  if (typeof val === "number") return Math.round(val);
@@ -1642,7 +1643,15 @@ function buildFigmaPayloadsFromDS(data) {
1642
1643
  }
1643
1644
  return "Inter";
1644
1645
  };
1645
- const fontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
1646
+ const toFontFamilyString = (val) => {
1647
+ if (typeof val === "string") {
1648
+ const s = val.trim().replace(/^["']|["']$/g, "");
1649
+ return s || "Inter";
1650
+ }
1651
+ return firstFont(val);
1652
+ };
1653
+ const fontFamilyMap = typography?.fontFamily ?? {};
1654
+ const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
1646
1655
  const fontSizeMap = typography?.fontSize;
1647
1656
  const fontWeightMap = typography?.fontWeight;
1648
1657
  const lineHeightMap = typography?.lineHeight;
@@ -1653,6 +1662,10 @@ function buildFigmaPayloadsFromDS(data) {
1653
1662
  for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
1654
1663
  const fontSize = sizeToPx(sizeVal);
1655
1664
  if (fontSize <= 0) continue;
1665
+ const role = typesetKeyToFontFamilyRole(key);
1666
+ const fontFamily = toFontFamilyString(
1667
+ fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
1668
+ );
1656
1669
  const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
1657
1670
  const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
1658
1671
  const fontWeight = weight != null ? String(weight) : "400";
@@ -1695,7 +1708,7 @@ function buildFigmaPayloadsFromDS(data) {
1695
1708
  const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
1696
1709
  const payload = {
1697
1710
  name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
1698
- fontFamily,
1711
+ fontFamily: defaultFontFamily,
1699
1712
  fontWeight: String(style.fontWeight ?? "400"),
1700
1713
  fontSize,
1701
1714
  lineHeightUnit: "PERCENT",
@@ -1717,59 +1730,58 @@ function buildFigmaPayloadsFromDS(data) {
1717
1730
  const variables2 = [];
1718
1731
  for (const [key, val] of Object.entries(spacing.scale)) {
1719
1732
  const n = tokenValueToNumber(val);
1720
- if (n >= 0) variables2.push({ name: key, value: n });
1733
+ if (n >= 0) variables2.push({ name: `Spacing / ${key}`, value: n });
1721
1734
  }
1722
1735
  variables2.sort((a, b) => a.value - b.value);
1723
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Spacing", variables: variables2, scopes: ["GAP"] });
1736
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Spacing`, categoryKey: "Spacing", variables: variables2, scopes: ["GAP"] });
1724
1737
  }
1725
1738
  const radius = tokens?.radius;
1726
1739
  if (radius?.scale && typeof radius.scale === "object") {
1727
1740
  const variables2 = [];
1728
1741
  for (const [key, val] of Object.entries(radius.scale)) {
1729
1742
  const n = tokenValueToNumber(val);
1730
- if (n >= 0) variables2.push({ name: key, value: n });
1743
+ if (n >= 0) variables2.push({ name: `Radius / ${key}`, value: n });
1731
1744
  }
1732
1745
  variables2.sort((a, b) => a.value - b.value);
1733
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Radius", variables: variables2, scopes: ["CORNER_RADIUS"] });
1746
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Radius`, categoryKey: "Radius", variables: variables2, scopes: ["CORNER_RADIUS"] });
1734
1747
  }
1735
1748
  const borders = tokens?.borders;
1736
1749
  if (borders?.width && typeof borders.width === "object") {
1737
1750
  const variables2 = [];
1738
1751
  for (const [key, val] of Object.entries(borders.width)) {
1739
1752
  const n = tokenValueToNumber(val);
1740
- if (n >= 0) variables2.push({ name: key, value: n });
1753
+ if (n >= 0) variables2.push({ name: `Borders / ${key}`, value: n });
1741
1754
  }
1742
1755
  variables2.sort((a, b) => a.value - b.value);
1743
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Borders", variables: variables2, scopes: ["STROKE_FLOAT"] });
1756
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Borders`, categoryKey: "Borders", variables: variables2, scopes: ["STROKE_FLOAT"] });
1744
1757
  }
1745
1758
  const sizing = tokens?.sizing;
1759
+ const sizingVariables = [];
1746
1760
  if (sizing?.height && typeof sizing.height === "object") {
1747
- const variables2 = [];
1748
1761
  for (const [key, val] of Object.entries(sizing.height)) {
1749
1762
  const n = tokenValueToNumber(val);
1750
- if (n >= 0) variables2.push({ name: key, value: n });
1763
+ if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
1751
1764
  }
1752
- variables2.sort((a, b) => a.value - b.value);
1753
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Height", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1754
1765
  }
1755
1766
  if (sizing?.icon && typeof sizing.icon === "object") {
1756
- const variables2 = [];
1757
1767
  for (const [key, val] of Object.entries(sizing.icon)) {
1758
1768
  const n = tokenValueToNumber(val);
1759
- if (n >= 0) variables2.push({ name: key, value: n });
1769
+ if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
1760
1770
  }
1761
- variables2.sort((a, b) => a.value - b.value);
1762
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Icon", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1771
+ }
1772
+ sizingVariables.sort((a, b) => a.value - b.value);
1773
+ if (sizingVariables.length > 0) {
1774
+ numberVariableCollections.push({ collectionName: `${collectionPrefix}Sizing`, categoryKey: "Sizing", variables: sizingVariables, scopes: ["WIDTH_HEIGHT"] });
1763
1775
  }
1764
1776
  const layout = tokens?.layout;
1765
1777
  if (layout?.breakpoint && typeof layout.breakpoint === "object") {
1766
1778
  const variables2 = [];
1767
1779
  for (const [key, val] of Object.entries(layout.breakpoint)) {
1768
1780
  const n = tokenValueToNumber(val);
1769
- if (n >= 0) variables2.push({ name: key, value: n });
1781
+ if (n >= 0) variables2.push({ name: `Breakpoint / ${key}`, value: n });
1770
1782
  }
1771
1783
  variables2.sort((a, b) => a.value - b.value);
1772
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Breakpoint", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1784
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Layout`, categoryKey: "Layout", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1773
1785
  }
1774
1786
  const effectStyles = [];
1775
1787
  const shadows = tokens?.shadows;
@@ -1800,7 +1812,7 @@ function buildFigmaPayloadsFromDS(data) {
1800
1812
  return nameA.localeCompare(nameB);
1801
1813
  });
1802
1814
  return {
1803
- colorVariables: { collectionName, modes, variables },
1815
+ colorVariables: { collectionName: colorCollectionName, modes, variables },
1804
1816
  paintStyles,
1805
1817
  textStyles,
1806
1818
  numberVariableCollections,
@@ -1810,7 +1822,7 @@ function buildFigmaPayloadsFromDS(data) {
1810
1822
  function getExpectedFigmaNamesFromDS(data) {
1811
1823
  const payloads = buildFigmaPayloadsFromDS(data);
1812
1824
  const numberVariableNames = payloads.numberVariableCollections.flatMap(
1813
- (c) => c.variables.map((v) => `${c.categoryKey} / ${v.name}`)
1825
+ (c) => c.variables.map((v) => v.name)
1814
1826
  );
1815
1827
  return {
1816
1828
  colorVariableNames: payloads.colorVariables.variables.map((v) => v.name),
@@ -1853,7 +1865,7 @@ var authFailedNoTools = false;
1853
1865
  function hasValidAuthConfig() {
1854
1866
  return !!(dsId && accessToken);
1855
1867
  }
1856
- var AUTH_REQUIRED_MESSAGE = "Atomix MCP requires authentication. Add both --ds-id and --atomix-token to your MCP config (Settings \u2192 MCP), then restart Cursor. Get your token from Atomix Studio: Export modal or Settings \u2192 Regenerate Atomix access token.";
1868
+ var AUTH_REQUIRED_MESSAGE = "Atomix MCP requires authentication. Add both --ds-id and --atomix-token to your MCP config (Settings \u2192 MCP), then restart your AI tool. Get your token from Atomix Studio: Export modal or Settings \u2192 Regenerate Atomix access token.";
1857
1869
  var lastChangeSummary = null;
1858
1870
  var FIGMA_SYNC_TOOL_NAME = "syncToFigma";
1859
1871
  var FIGMA_DESIGN_TOOL_NAMES = /* @__PURE__ */ new Set([
@@ -1941,10 +1953,10 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
1941
1953
  }
1942
1954
  var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
1943
1955
  function typesetKeyToFontFamilyRole(key) {
1944
- const prefix = key.split("-")[0] ?? "";
1945
- if (prefix === "display") return "display";
1946
- if (prefix === "heading") return "heading";
1947
- if (prefix === "mono") return "mono";
1956
+ const prefix = (key.split("-")[0] ?? key).toLowerCase();
1957
+ if (prefix === "display" || prefix.startsWith("display")) return "display";
1958
+ if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
1959
+ if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
1948
1960
  if (prefix.startsWith("body")) return "body";
1949
1961
  return "body";
1950
1962
  }
@@ -1962,12 +1974,15 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
1962
1974
  for (const key of Object.keys(fontSize)) {
1963
1975
  const role = typesetKeyToFontFamilyRole(key);
1964
1976
  const familyName = fontFamily[role] ?? fontFamily.body;
1965
- const fontFamilyVar = familyName ? `var(--${p}typography-font-family-${role})` : "";
1977
+ const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
1978
+ const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
1966
1979
  const keyKebab = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1967
1980
  typesets.push({
1968
1981
  key,
1969
1982
  cssClass: `.typeset-${keyKebab}`,
1970
1983
  fontFamilyVar: fontFamilyVar || "inherit",
1984
+ fontFamilyVarName,
1985
+ fontFamilyValue: familyName,
1971
1986
  fontSizeVar: `var(--${p}typography-${key}-size)`,
1972
1987
  fontWeightVar: `var(--${p}typography-${key}-weight)`,
1973
1988
  lineHeightVar: `var(--${p}typography-${key}-line-height)`,
@@ -1983,7 +1998,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
1983
1998
  var server = new Server(
1984
1999
  {
1985
2000
  name: "atomix-mcp-user",
1986
- version: "1.0.25"
2001
+ version: "1.0.27"
1987
2002
  },
1988
2003
  {
1989
2004
  capabilities: {
@@ -2014,7 +2029,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2014
2029
  authFailedNoTools = true;
2015
2030
  console.error(
2016
2031
  "[Atomix MCP] Design system not loaded: ds-id or token invalid or API error. No tools will be shown.",
2017
- msg.includes("401") ? " Token invalid or expired. Regenerate in Atomix Studio (Settings \u2192 Regenerate Atomix access token), update your MCP config, then restart Cursor." : msg.includes("403") ? " You do not have access to this design system (owner or invited guest)." : msg.includes("404") ? " Design system not found (invalid ds-id)." : msg
2032
+ msg.includes("401") ? " Token invalid or expired. Regenerate in Atomix Studio (Settings \u2192 Regenerate Atomix access token), update your MCP config, then restart your AI tool." : msg.includes("403") ? " You do not have access to this design system (owner or invited guest)." : msg.includes("404") ? " Design system not found (invalid ds-id)." : msg
2018
2033
  );
2019
2034
  }
2020
2035
  }
@@ -2362,7 +2377,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2362
2377
  return {
2363
2378
  content: [{
2364
2379
  type: "text",
2365
- text: "MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart Cursor. No tools are available until then."
2380
+ text: "MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart your AI tool. No tools are available until then."
2366
2381
  }],
2367
2382
  isError: true
2368
2383
  };
@@ -2652,7 +2667,7 @@ Version: ${designSystemData.meta.version}`,
2652
2667
  text: JSON.stringify({
2653
2668
  count: typesets.length,
2654
2669
  typesets,
2655
- instruction: "Emit one CSS rule per typeset using the cssClass and the listed var() properties. Include font-family, font-size, font-weight, line-height; add letter-spacing when present; add text-transform and text-decoration when hasTextTransform/hasTextDecoration are true so the result is 1:1 with the design system.",
2670
+ instruction: "Emit one CSS rule per typeset using the cssClass and the listed var() properties. Include font-family, font-size, font-weight, line-height; add letter-spacing when present; add text-transform and text-decoration when hasTextTransform/hasTextDecoration are true so the result is 1:1 with the design system. The synced token file quotes font names that contain spaces; no override is needed in the typeset file. For native (iOS/Android) projects, font names with spaces must be quoted or use the platform's canonical font name in theme or resource files.",
2656
2671
  ...underlineThickness != null && underlineOffset != null && {
2657
2672
  underlineVars: {
2658
2673
  thickness: `var(--${cssPrefix}-typography-underline-thickness)`,
@@ -2799,11 +2814,11 @@ Version: ${designSystemData.meta.version}`,
2799
2814
  case "getSetupInstructions": {
2800
2815
  const tool = args?.tool;
2801
2816
  const instructions = {
2802
- cursor: `# Cursor MCP Setup
2817
+ cursor: `# MCP Setup
2803
2818
 
2804
- 1. Create \`.cursor/mcp.json\` in your project root
2819
+ 1. Create your MCP config file in the project (e.g. \`.cursor/mcp.json\` for Cursor)
2805
2820
  2. Add the MCP configuration (use exportMCPConfig to get it)
2806
- 3. Restart Cursor IDE
2821
+ 3. Restart your IDE
2807
2822
  4. Verify by asking: "What design tokens are available?"
2808
2823
 
2809
2824
  ## File Structure
@@ -3083,7 +3098,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3083
3098
  showcase: platform2 === "web" || !platform2 ? {
3084
3099
  path: "atomix-setup-showcase.html",
3085
3100
  template: SHOWCASE_HTML_TEMPLATE,
3086
- substitutionInstructions: "Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export."
3101
+ substitutionInstructions: 'Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{TYPESETS_LINK}} = if a typeset CSS file was created (e.g. typesets.css), the full tag e.g. <link rel="stylesheet" href="typesets.css">, otherwise empty string. {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl or typeset-display-bold), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = typeset class for body text from listTypesets (e.g. typeset-body-md), or empty string if no typeset file. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export.'
3087
3102
  } : void 0,
3088
3103
  meta: {
3089
3104
  dsName: data.meta.name,
@@ -3103,7 +3118,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3103
3118
  case "syncToFigma": {
3104
3119
  const payloads = buildFigmaPayloadsFromDS(data);
3105
3120
  const out = {};
3106
- const agentStartBridge = `The Figma bridge runs with this MCP server. Ensure Cursor has this MCP configured and running, then in Figma run the Atomix plugin and click Connect to Cursor. Then call **syncToFigma** again.`;
3121
+ const agentStartBridge = `The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server configured and running, then in Figma run the Atomix plugin and tap Connect. Then call **syncToFigma** again.`;
3107
3122
  const userSteps = [
3108
3123
  FIGMA_CONNECTION_INSTRUCTIONS.startBridge,
3109
3124
  FIGMA_CONNECTION_INSTRUCTIONS.installAndRun,
@@ -3133,21 +3148,25 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3133
3148
  out.colorVariables = await sendBridgeRequest("create_color_variables", {
3134
3149
  collectionName: payloads.colorVariables.collectionName,
3135
3150
  modes: payloads.colorVariables.modes,
3136
- variables: payloads.colorVariables.variables
3151
+ variables: payloads.colorVariables.variables,
3152
+ removeVariablesNotInPayload: true
3137
3153
  });
3138
3154
  }
3139
3155
  if (payloads.paintStyles.length > 0) {
3140
- out.paintStyles = await sendBridgeRequest("create_paint_styles", { styles: payloads.paintStyles });
3156
+ out.paintStyles = await sendBridgeRequest("create_paint_styles", {
3157
+ styles: payloads.paintStyles,
3158
+ removePaintStylesNotInPayload: true
3159
+ });
3141
3160
  }
3142
3161
  if (payloads.numberVariableCollections.length > 0) {
3143
3162
  const numberResults = [];
3144
3163
  try {
3145
- const foundationsName = payloads.colorVariables.collectionName;
3146
3164
  for (const coll of payloads.numberVariableCollections) {
3147
3165
  const result = await sendBridgeRequest("create_number_variables", {
3148
- collectionName: foundationsName,
3149
- variables: coll.variables.map((v) => ({ name: `${coll.categoryKey} / ${v.name}`, value: v.value })),
3150
- scopes: coll.scopes
3166
+ collectionName: coll.collectionName,
3167
+ variables: coll.variables.map((v) => ({ name: v.name, value: v.value })),
3168
+ scopes: coll.scopes,
3169
+ removeVariablesNotInPayload: true
3151
3170
  });
3152
3171
  numberResults.push({ categoryKey: coll.categoryKey, result });
3153
3172
  }
@@ -3161,10 +3180,16 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3161
3180
  }
3162
3181
  }
3163
3182
  if (payloads.textStyles.length > 0) {
3164
- out.textStyles = await sendBridgeRequest("create_text_styles", { styles: payloads.textStyles });
3183
+ out.textStyles = await sendBridgeRequest("create_text_styles", {
3184
+ styles: payloads.textStyles,
3185
+ removeTextStylesNotInPayload: true
3186
+ });
3165
3187
  }
3166
3188
  if (payloads.effectStyles.length > 0) {
3167
- out.effectStyles = await sendBridgeRequest("create_effect_styles", { styles: payloads.effectStyles });
3189
+ out.effectStyles = await sendBridgeRequest("create_effect_styles", {
3190
+ styles: payloads.effectStyles,
3191
+ removeShadowStylesNotInPayload: true
3192
+ });
3168
3193
  }
3169
3194
  out.figmaPayload = {
3170
3195
  collectionName: payloads.colorVariables.collectionName,
@@ -3204,11 +3229,76 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3204
3229
  if (out.numberVariablesHint) {
3205
3230
  out.summary = [out.summary, out.numberVariablesHint].filter(Boolean).join(" ");
3206
3231
  }
3232
+ const summaryParts = [];
3233
+ const colorResult = out.colorVariables;
3234
+ if (colorResult && (colorResult.variableNames?.length || (colorResult.removed ?? 0) > 0)) {
3235
+ const parts = [];
3236
+ if (colorResult.variableNames?.length) parts.push(`${colorResult.variableNames.length} synced`);
3237
+ if ((colorResult.removed ?? 0) > 0) parts.push(`${colorResult.removed} removed`);
3238
+ summaryParts.push(`Colors: ${parts.join(", ")}.`);
3239
+ }
3240
+ const paintResult = out.paintStyles;
3241
+ if (paintResult) {
3242
+ const c = paintResult.created ?? 0;
3243
+ const u = paintResult.updated ?? 0;
3244
+ const r = paintResult.removed ?? 0;
3245
+ if (c + u + r > 0) {
3246
+ const parts = [];
3247
+ if (c > 0) parts.push(`${c} created`);
3248
+ if (u > 0) parts.push(`${u} updated`);
3249
+ if (r > 0) parts.push(`${r} removed`);
3250
+ summaryParts.push(`Paint styles: ${parts.join(", ")}.`);
3251
+ }
3252
+ }
3253
+ const effectResult = out.effectStyles;
3254
+ if (effectResult) {
3255
+ const c = effectResult.created ?? 0;
3256
+ const u = effectResult.updated ?? 0;
3257
+ const r = effectResult.removed ?? 0;
3258
+ if (c + u + r > 0) {
3259
+ const parts = [];
3260
+ if (c > 0) parts.push(`${c} created`);
3261
+ if (u > 0) parts.push(`${u} updated`);
3262
+ if (r > 0) parts.push(`${r} removed`);
3263
+ summaryParts.push(`Effect styles (shadows): ${parts.join(", ")}.`);
3264
+ if (effectResult.removedNames?.length) {
3265
+ summaryParts.push(`Removed: ${effectResult.removedNames.join(", ")}.`);
3266
+ }
3267
+ }
3268
+ }
3269
+ const numResult = out.numberVariables;
3270
+ if (Array.isArray(numResult)) {
3271
+ const total = numResult.reduce((acc, r) => acc + (r.result?.variableNames?.length ?? 0), 0);
3272
+ const totalRemoved = numResult.reduce((acc, r) => acc + (r.result?.removed ?? 0), 0);
3273
+ if (total > 0 || totalRemoved > 0) {
3274
+ const parts = [];
3275
+ if (total > 0) parts.push(`${total} synced`);
3276
+ if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
3277
+ summaryParts.push(`Number variables: ${parts.join(", ")}.`);
3278
+ }
3279
+ }
3280
+ const textStylesWithRemoved = out.textStyles;
3281
+ if (textStylesWithRemoved && (textStylesWithRemoved.created ?? 0) + (textStylesWithRemoved.updated ?? 0) + (textStylesWithRemoved.removed ?? 0) > 0) {
3282
+ const c = textStylesWithRemoved.created ?? 0;
3283
+ const u = textStylesWithRemoved.updated ?? 0;
3284
+ const r = textStylesWithRemoved.removed ?? 0;
3285
+ const parts = [];
3286
+ if (c > 0) parts.push(`${c} created`);
3287
+ if (u > 0) parts.push(`${u} updated`);
3288
+ if (r > 0) parts.push(`${r} removed`);
3289
+ summaryParts.push(`Text styles: ${parts.join(", ")}.`);
3290
+ }
3291
+ if (summaryParts.length > 0 && !out.error) {
3292
+ out.summary = [out.summary, summaryParts.join(" ")].filter(Boolean).join(" ");
3293
+ }
3207
3294
  out.motionEasingNote = "Motion easing tokens are not synced as Figma styles; Figma has no reusable easing style. Easing is only used in prototype transitions (e.g. smart animate). Duration/easing remain available as number variables (duration) or in export JSON.";
3295
+ const responseText = out.summary ? `${out.summary}
3296
+
3297
+ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3208
3298
  return {
3209
3299
  content: [{
3210
3300
  type: "text",
3211
- text: JSON.stringify(out, null, 2)
3301
+ text: responseText
3212
3302
  }],
3213
3303
  ...out.error ? { isError: true } : {}
3214
3304
  };
@@ -3223,7 +3313,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3223
3313
  text: JSON.stringify({
3224
3314
  error: "Figma bridge not reachable.",
3225
3315
  bridgeNotRunning: true,
3226
- agentInstruction: "The Figma bridge runs with this MCP server. Ensure Cursor has this MCP running, then in Figma run the Atomix plugin and click Connect to Cursor. Then call getFigmaVariablesAndStyles again."
3316
+ agentInstruction: "The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server running, then in Figma run the Atomix plugin and tap Connect. Then call getFigmaVariablesAndStyles again."
3227
3317
  }, null, 2)
3228
3318
  }],
3229
3319
  isError: true
@@ -3253,7 +3343,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3253
3343
  type: "text",
3254
3344
  text: JSON.stringify({
3255
3345
  error: "Figma bridge not reachable.",
3256
- hint: "The bridge runs with this MCP server. Ensure Cursor has this MCP running, then run the Atomix plugin and click Connect to Cursor."
3346
+ hint: "The bridge runs with this MCP server. Ensure your AI environment has this MCP server running, then run the Atomix plugin and tap Connect."
3257
3347
  }, null, 2)
3258
3348
  }],
3259
3349
  isError: true
@@ -3371,7 +3461,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3371
3461
  type: "text",
3372
3462
  text: JSON.stringify({
3373
3463
  error: "Figma bridge not reachable.",
3374
- hint: "Run the Atomix plugin in Figma and click Connect to Cursor, then retry."
3464
+ hint: "Run the Atomix plugin in Figma and tap Connect, then retry."
3375
3465
  }, null, 2)
3376
3466
  }],
3377
3467
  isError: true
@@ -3412,7 +3502,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3412
3502
  type: "text",
3413
3503
  text: JSON.stringify({
3414
3504
  error: "Figma bridge not reachable.",
3415
- hint: "Run the Atomix plugin in Figma and click Connect to Cursor, then retry."
3505
+ hint: "Run the Atomix plugin in Figma and tap Connect, then retry."
3416
3506
  }, null, 2)
3417
3507
  }],
3418
3508
  isError: true
@@ -3445,7 +3535,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3445
3535
  type: "text",
3446
3536
  text: JSON.stringify({
3447
3537
  error: "Figma bridge not reachable.",
3448
- hint: "Run the Atomix plugin in Figma and click Connect to Cursor, then retry."
3538
+ hint: "Run the Atomix plugin in Figma and tap Connect, then retry."
3449
3539
  }, null, 2)
3450
3540
  }],
3451
3541
  isError: true
@@ -3625,6 +3715,7 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3625
3715
  <title>Setup complete \u2014 {{DS_NAME}}</title>
3626
3716
  {{FONT_LINK_TAG}}
3627
3717
  <link rel="stylesheet" href="{{TOKENS_CSS_PATH}}">
3718
+ {{TYPESETS_LINK}}
3628
3719
  <style>
3629
3720
  * { box-sizing: border-box; }
3630
3721
  body {
@@ -3635,9 +3726,11 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3635
3726
  min-height: 100vh;
3636
3727
  padding: 2rem;
3637
3728
  display: flex;
3729
+ justify-content: center;
3730
+ align-items: center;
3638
3731
  }
3639
3732
  .wrap { width: 375px; max-width: 100%; }
3640
- .icon { width: 2rem; height: 2rem; margin: 0 auto 1rem; }
3733
+ .icon { width: 2rem; height: 2rem; margin: 0 0 1rem; }
3641
3734
  h1 {
3642
3735
  font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3643
3736
  font-size: clamp(3rem, 4vw, 5rem);
@@ -3653,12 +3746,12 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3653
3746
  .tips a { color: inherit; text-decoration: underline; }
3654
3747
  </style>
3655
3748
  </head>
3656
- <body>
3749
+ <body class="{{BODY_TYPESET_CLASS}}">
3657
3750
  <div class="wrap">
3658
3751
  <div class="icon" aria-hidden="true">
3659
3752
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="44" height="44"><path d="M20 6L9 17l-5-5"/></svg>
3660
3753
  </div>
3661
- <h1>You're all set with {{DS_NAME}}</h1>
3754
+ <h1 class="{{LARGEST_DISPLAY_TYPESET_CLASS}}">You're all set with {{DS_NAME}}</h1>
3662
3755
  <p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon.</p>
3663
3756
  <div class="now">
3664
3757
  <strong>What you can do now:</strong>
@@ -3714,7 +3807,7 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
3714
3807
  };
3715
3808
  }
3716
3809
  if (!hasValidAuthConfig() || authFailedNoTools) {
3717
- throw new Error("MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart Cursor. See atomix://setup for instructions.");
3810
+ throw new Error("MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart your AI tool. See atomix://setup for instructions.");
3718
3811
  }
3719
3812
  const data = await fetchDesignSystemForMCP();
3720
3813
  const stats = getTokenStats(data);
@@ -3789,7 +3882,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3789
3882
  role: "user",
3790
3883
  content: {
3791
3884
  type: "text",
3792
- text: "MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart Cursor. No tools or prompts are available until then."
3885
+ text: "MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart your AI tool. No tools or prompts are available until then."
3793
3886
  }
3794
3887
  }]
3795
3888
  };
@@ -3815,7 +3908,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3815
3908
  - \`--ds-id\`: Your design system ID (get it from https://atomixstudio.eu/ds/[your-ds-id])
3816
3909
  - \`--atomix-token\`: Your access token (get it from the Export modal or Settings \u2192 Regenerate Atomix access token)
3817
3910
 
3818
- Both are required. Configure the MCP server in your Cursor settings, then restart Cursor.`
3911
+ Both are required. Configure the MCP server in your AI tool's MCP settings, then restart your AI tool.`
3819
3912
  }
3820
3913
  }
3821
3914
  ]
@@ -4133,7 +4226,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4133
4226
  role: "user",
4134
4227
  content: {
4135
4228
  type: "text",
4136
- text: `Call the MCP tool **syncToFigma** now (no arguments). It pushes the design system into the open Figma file via the built-in bridge and Atomix plugin. Do not use the Figma REST API or external scripts. If the response includes \`bridgeNotRunning\` and \`agentInstruction\`, ensure Cursor has this MCP server running, then in Figma run the Atomix plugin and click Connect to Cursor, then call syncToFigma again. Only if that fails, tell the user: (1) Ensure Cursor has this MCP configured and running. (2) In Figma, open and run the Atomix plugin, then tap **Connect to Cursor**. (3) Run Sync to Figma again.`
4229
+ text: `Call the MCP tool **syncToFigma** now (no arguments). It pushes the design system into the open Figma file via the built-in bridge and Atomix plugin. Do not use the Figma REST API or external scripts. If the response includes \`bridgeNotRunning\` and \`agentInstruction\`, ensure your AI environment has this MCP server running, then in Figma run the Atomix plugin and tap Connect, then call syncToFigma again. Only if that fails, tell the user: (1) Ensure this MCP server is configured and running in your AI tool. (2) In Figma, open and run the Atomix plugin, then tap **Connect**. (3) Run Sync to Figma again. After a successful sync, report the **summary** from the response (what was created, updated, or removed)\u2014do not say "we added the whole design system" when only some items changed.`
4137
4230
  }
4138
4231
  }
4139
4232
  ]
@@ -4149,7 +4242,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4149
4242
  type: "text",
4150
4243
  text: `You must **design in Figma** by calling a **sequence of MCP tools**\u2014do not generate or run any JavaScript code. Follow the design-in-Figma skill if present; otherwise follow this **three-pass flow**.
4151
4244
 
4152
- Prerequisites: Figma bridge runs inside this MCP server. In Figma, run the Atomix plugin and click "Connect to Cursor"; leave the plugin open.
4245
+ Prerequisites: Figma bridge runs inside this MCP server. In Figma, run the Atomix plugin and tap Connect; leave the plugin open.
4153
4246
 
4154
4247
  **Setup (once):** syncToFigma \u2192 createDesignPlaceholder (save \`frameId\`) \u2192 getAIToolRules + listTokens \u2192 resolveFigmaIdsForTokens. Use the returned \`resolved\` map and \`frameId\` for all passes.
4155
4248
 
@@ -4162,7 +4255,7 @@ Apply the owner's design system: set all fills and text styles using only ids fr
4162
4255
  **Pass 3 \u2014 Confirm no hardcoded values**
4163
4256
  Audit: ensure no raw px, hex, or font values remain; every fill and text style must use an id from \`resolved\`. Call **getDesignScreenshot** once more. If everything is correct: call **finalizeDesignFrame** with \`frameId\`, \`name\` = a short description of the design + " \u2705", and \`fillVariableId\` (or \`fillPaintStyleId\`) from resolved for the surface/background so the placeholder gray is removed. Then **summarise** what was built and any fixes made across the three passes.
4164
4257
 
4165
- If the bridge is not reachable: tell the user to run the Atomix plugin in Figma and click Connect to Cursor, then retry.`
4258
+ If the bridge is not reachable: tell the user to run the Atomix plugin in Figma and tap Connect, then retry.`
4166
4259
  }
4167
4260
  }
4168
4261
  ]
@@ -4311,7 +4404,7 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4311
4404
  - **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css"). syncAll also writes skills and atomix-dependencies.json. You must call syncAll; do not only suggest the user run it later.
4312
4405
  - **Icon package:** Install per getDependencies. When rendering icons, apply the design system's icon tokens: use getToken(\`sizing.icon.*\`) or listTokens(\`sizing\`) for size, and getToken(\`icons.strokeWidth\`) for stroke width when the DS defines it; do not use hardcoded sizes or stroke widths.
4313
4406
  - **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
4314
- - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1), FONT_FAMILY_VAR (body), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4407
+ - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, TYPESETS_LINK (if you created a typeset CSS file, use the full \`<link rel="stylesheet" href="\u2026">\` tag; otherwise empty string), DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1 fallback), FONT_FAMILY_VAR (body fallback), LARGEST_DISPLAY_TYPESET_CLASS (largest display typeset from listTypesets) and BODY_TYPESET_CLASS (e.g. typeset-body-md; leave empty if no typeset file), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4315
4408
  - Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
4316
4409
  - **After reporting \u2013 styles/theme:**
4317
4410
  - **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014call **listTypesets** and emit **one CSS class per typeset** (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too for a 1:1 match. Use the CSS variable names returned by listTypesets. The output must not be only a font import; it must define every typeset with every style detail from the design system.
@@ -4391,8 +4484,9 @@ ${tokenSummary}
4391
4484
  |---------|----------------|
4392
4485
  | **/--hello** | Get started - overview, tokens, and tools. Run this first! |
4393
4486
  | **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
4394
- | **/--rules** | Governance rules for your AI tool (Cursor, Copilot, Windsurf, etc.). |
4487
+ | **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
4395
4488
  | **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
4489
+ | **/--sync-to-figma** | Push design system to Figma (variables, paint/text/effect styles). Uses built-in bridge + Atomix plugin; connect plugin in Figma then run. Available on all tiers. |
4396
4490
  | **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
4397
4491
 
4398
4492
  **Suggested next step:** Run **/--get-started** to set up global styles, icons, fonts, and token files; the AI will list options and ask before adding anything.
@@ -4423,7 +4517,7 @@ async function startServer() {
4423
4517
  console.error(`Atomix MCP Server started for design system: ${dsId}`);
4424
4518
  console.error(`Atomix MCP API base: ${apiBase}`);
4425
4519
  console.error(
4426
- "If you switched MCP config (e.g. free vs pro DS), restart Cursor so this process uses the new --ds-id and --atomix-token."
4520
+ "If you switched MCP config (e.g. free vs pro DS), restart your AI tool so this process uses the new --ds-id and --atomix-token."
4427
4521
  );
4428
4522
  }
4429
4523
  function onShutdown() {
@@ -4431,6 +4525,8 @@ function onShutdown() {
4431
4525
  }
4432
4526
  process.on("SIGINT", onShutdown);
4433
4527
  process.on("SIGTERM", onShutdown);
4528
+ process.stdin.on("end", onShutdown);
4529
+ process.stdin.on("close", onShutdown);
4434
4530
  async function main() {
4435
4531
  await startServer();
4436
4532
  }