@atomixstudio/mcp 1.0.34 → 1.0.37

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/dist/index.js CHANGED
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- isAllowedMethod,
4
- normalizeBridgeMethod
5
- } from "./chunk-CE6J5MJX.js";
6
2
 
7
3
  // src/index.ts
8
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -222,7 +218,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
222
218
  return changes;
223
219
  }
224
220
  async function fetchDesignSystem(options) {
225
- const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomixstudio.eu", etag, forceRefresh = false } = options;
221
+ const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomix.studio", etag, forceRefresh = false } = options;
226
222
  if (!dsId2) {
227
223
  throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
228
224
  }
@@ -1093,9 +1089,7 @@ function formatSyncResponse(options) {
1093
1089
  `;
1094
1090
  });
1095
1091
  response += `
1096
- These changes are reflected in your AI tool rules files (e.g., .cursorrules).
1097
- `;
1098
- response += `Review the updated rules files to see the new guidance.
1092
+ These changes are reflected in the design system governance; run /--sync to update the skill and call getRules for the latest rules.
1099
1093
  `;
1100
1094
  }
1101
1095
  if (diff) {
@@ -1218,886 +1212,9 @@ function getTokenStats(data) {
1218
1212
  };
1219
1213
  }
1220
1214
 
1221
- // ../figma-tools/dist/index.js
1222
- var LEGACY_GROUP_PREFIX = {
1223
- background: "bg",
1224
- text: "text",
1225
- icon: "icon",
1226
- border: "border",
1227
- brand: "brand",
1228
- action: "action",
1229
- feedback: "feedback"
1230
- };
1231
- var RESERVED_COLOR_KEYS = /* @__PURE__ */ new Set(["customScales", "neutral"]);
1232
- function getColorGroupOrder(storedColors) {
1233
- if (Array.isArray(storedColors._groupOrder) && storedColors._groupOrder.length > 0) {
1234
- return storedColors._groupOrder;
1235
- }
1236
- return Object.keys(storedColors).filter(
1237
- (k) => !k.startsWith("_") && !RESERVED_COLOR_KEYS.has(k) && typeof storedColors[k] === "object"
1238
- );
1239
- }
1240
- function getGroupPrefix(storedColors, groupName) {
1241
- const custom = storedColors._groupPrefix;
1242
- if (custom && typeof custom[groupName] === "string") return custom[groupName];
1243
- return LEGACY_GROUP_PREFIX[groupName] ?? groupName;
1244
- }
1245
- function refScaleToFigmaDisplayName(scaleFromRef) {
1246
- const s = scaleFromRef.trim();
1247
- if (!s) return s;
1248
- return s.charAt(0).toUpperCase() + s.slice(1);
1249
- }
1250
- function referenceToFigmaPrimitiveName(ref, primitiveNames) {
1251
- if (!ref || typeof ref !== "string") return null;
1252
- const r = ref.trim();
1253
- const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
1254
- if (scaleStep) {
1255
- const [, scale, step] = scaleStep;
1256
- const scaleDisplay = refScaleToFigmaDisplayName(scale);
1257
- const name = `${scaleDisplay} / ${step}`;
1258
- return primitiveNames.has(name) ? name : null;
1259
- }
1260
- const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
1261
- if (radixMatch) {
1262
- const [, family, step] = radixMatch;
1263
- const scaleName = refScaleToFigmaDisplayName(family);
1264
- const name = `${scaleName} / ${step}`;
1265
- return primitiveNames.has(name) ? name : null;
1266
- }
1267
- const oneOffMatch = /^(?:new|oneOff)\.(.+)$/.exec(r);
1268
- if (oneOffMatch) {
1269
- const name = `One-off / ${oneOffMatch[1]}`;
1270
- return primitiveNames.has(name) ? name : null;
1271
- }
1272
- const whiteAlpha = /^whiteAlpha\.(.+)$/.exec(r);
1273
- if (whiteAlpha) {
1274
- const name = `WhiteAlpha / ${whiteAlpha[1]}`;
1275
- return primitiveNames.has(name) ? name : null;
1276
- }
1277
- const blackAlpha = /^blackAlpha\.(.+)$/.exec(r);
1278
- if (blackAlpha) {
1279
- const name = `BlackAlpha / ${blackAlpha[1]}`;
1280
- return primitiveNames.has(name) ? name : null;
1281
- }
1282
- return null;
1283
- }
1284
- function referenceToScaleName(ref) {
1285
- if (!ref || typeof ref !== "string") return null;
1286
- const r = ref.trim();
1287
- const scaleStep = /^([a-zA-Z]+)\.(\d+|[a-z]+)$/.exec(r);
1288
- if (scaleStep) return refScaleToFigmaDisplayName(scaleStep[1]);
1289
- const radixMatch = /^radix\.([a-zA-Z]+)\.(\d+)$/.exec(r);
1290
- if (radixMatch) return refScaleToFigmaDisplayName(radixMatch[1]);
1291
- return null;
1292
- }
1293
- function getReferencedScaleNames(storedColors) {
1294
- const names = /* @__PURE__ */ new Set();
1295
- const groupOrder = getColorGroupOrder(storedColors);
1296
- for (const groupName of groupOrder) {
1297
- const group = storedColors[groupName];
1298
- if (!group || typeof group !== "object") continue;
1299
- const keys = Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
1300
- for (const key of keys) {
1301
- const value = group[key];
1302
- if (value === void 0 || typeof value !== "object" || value === null) continue;
1303
- const theme = value;
1304
- const lightScale = theme.light?.reference ? referenceToScaleName(theme.light.reference) : null;
1305
- const darkScale = theme.dark?.reference ? referenceToScaleName(theme.dark.reference) : null;
1306
- if (lightScale) names.add(lightScale);
1307
- if (darkScale) names.add(darkScale);
1308
- }
1309
- }
1310
- return names;
1311
- }
1312
- function getFullScaleFromStored(storedColors, scaleName) {
1313
- const key = scaleName.toLowerCase();
1314
- if (key === "neutral" || key === "gray") {
1315
- const neutral = storedColors.neutral;
1316
- if (!neutral?.steps || typeof neutral.steps !== "object") return null;
1317
- const out = {};
1318
- for (const [step, stepValue] of Object.entries(neutral.steps)) {
1319
- const hex = stepValue?.hex;
1320
- if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
1321
- out[step] = hex.startsWith("#") ? hex : `#${hex}`;
1322
- }
1323
- }
1324
- return Object.keys(out).length > 0 ? out : null;
1325
- }
1326
- const customScales = storedColors.customScales;
1327
- if (Array.isArray(customScales)) {
1328
- const keyNorm = key.replace(/\s+/g, "");
1329
- const scale = customScales.find(
1330
- (s) => s.name?.toLowerCase() === key || s.name?.toLowerCase().replace(/\s+/g, "") === keyNorm
1331
- );
1332
- if (scale?.steps && typeof scale.steps === "object") {
1333
- const out = {};
1334
- for (const [step, stepValue] of Object.entries(scale.steps)) {
1335
- const hex = stepValue?.hex;
1336
- if (typeof hex === "string" && /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/i.test(hex)) {
1337
- out[step] = hex.startsWith("#") ? hex : `#${hex}`;
1338
- }
1339
- }
1340
- if (Object.keys(out).length > 0) return out;
1341
- }
1342
- }
1343
- return null;
1344
- }
1345
- function buildSemanticRefMap(storedColors, primitiveNames) {
1346
- const out = {};
1347
- const groupOrder = getColorGroupOrder(storedColors);
1348
- for (const groupName of groupOrder) {
1349
- const group = storedColors[groupName];
1350
- if (!group || typeof group !== "object") continue;
1351
- const prefix = getGroupPrefix(storedColors, groupName);
1352
- const rowOrder = Array.isArray(group._rowOrder) ? group._rowOrder : void 0;
1353
- const keys = Array.isArray(rowOrder) ? rowOrder : Object.keys(group).filter((k) => !k.startsWith("_") && k !== "governance");
1354
- const toKebab = prefix === "action" || prefix === "brand" || prefix === "feedback";
1355
- for (const key of keys) {
1356
- const value = group[key];
1357
- if (value === void 0 || typeof value !== "object" || value === null) continue;
1358
- const theme = value;
1359
- let cssKey = key === "app" ? "page" : key;
1360
- if (toKebab) {
1361
- cssKey = String(key).replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1362
- }
1363
- const fullKey = `${prefix}-${cssKey}`;
1364
- const lightRef = theme.light?.reference ? referenceToFigmaPrimitiveName(theme.light.reference, primitiveNames) : null;
1365
- const darkRef = theme.dark?.reference ? referenceToFigmaPrimitiveName(theme.dark.reference, primitiveNames) : null;
1366
- if (lightRef || darkRef) {
1367
- out[fullKey] = {};
1368
- if (lightRef) out[fullKey].Light = lightRef;
1369
- if (darkRef) out[fullKey].Dark = darkRef;
1370
- }
1371
- }
1372
- }
1373
- return out;
1374
- }
1375
- function figmaColorNameWithGroup(key) {
1376
- if (key.includes("/")) {
1377
- const [group2, ...rest] = key.split("/");
1378
- const name2 = rest.join("/").trim();
1379
- if (!name2) return key;
1380
- const groupDisplay2 = group2.charAt(0).toUpperCase() + group2.slice(1).toLowerCase();
1381
- return `${groupDisplay2} / ${name2}`;
1382
- }
1383
- const firstDash = key.indexOf("-");
1384
- if (firstDash <= 0) return key;
1385
- const group = key.slice(0, firstDash);
1386
- const name = key.slice(firstDash + 1);
1387
- const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
1388
- return `${groupDisplay} / ${name}`;
1389
- }
1390
- var FIGMA_SHADOW_ORDER = {
1391
- none: 0,
1392
- xs: 1,
1393
- sm: 2,
1394
- md: 3,
1395
- lg: 4,
1396
- xl: 5,
1397
- "2xl": 6,
1398
- focus: 7
1399
- };
1400
- function tokenValueToNumber(s) {
1401
- if (typeof s !== "string" || !s.trim()) return 0;
1402
- const t = s.trim();
1403
- if (t.endsWith("rem")) {
1404
- const n2 = parseFloat(t.replace(/rem$/, ""));
1405
- return Number.isFinite(n2) ? Math.round(n2 * 16) : 0;
1406
- }
1407
- if (t.endsWith("px")) {
1408
- const n2 = parseFloat(t.replace(/px$/, ""));
1409
- return Number.isFinite(n2) ? Math.round(n2) : 0;
1410
- }
1411
- const n = parseFloat(t);
1412
- return Number.isFinite(n) ? n : 0;
1413
- }
1414
- function parseBoxShadowToFigmaEffect(shadowStr) {
1415
- const s = shadowStr.trim();
1416
- if (!s || s.toLowerCase() === "none") return null;
1417
- const parsePx = (x) => typeof x === "string" ? parseFloat(x.replace(/px$/i, "")) : NaN;
1418
- const colorMatch = s.match(/(rgba?\s*\([^)]+\)|#[0-9A-Fa-f]{3,8})\s*$/i);
1419
- const colorStr = colorMatch ? colorMatch[1].trim() : void 0;
1420
- const rest = (colorMatch ? s.slice(0, colorMatch.index) : s).trim();
1421
- const parts = rest ? rest.split(/\s+/) : [];
1422
- if (parts.length < 3) return null;
1423
- const offsetX = parsePx(parts[0]);
1424
- const offsetY = parsePx(parts[1]);
1425
- const blur = parsePx(parts[2]);
1426
- let spread = 0;
1427
- if (parts.length >= 4) spread = parsePx(parts[3]);
1428
- let r = 0, g = 0, b = 0, a = 0.1;
1429
- if (colorStr) {
1430
- const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
1431
- if (rgbaMatch) {
1432
- r = Number(rgbaMatch[1]) / 255;
1433
- g = Number(rgbaMatch[2]) / 255;
1434
- b = Number(rgbaMatch[3]) / 255;
1435
- a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1;
1436
- } else {
1437
- const hexMatch = colorStr.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/);
1438
- if (hexMatch) {
1439
- r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
1440
- g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
1441
- b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
1442
- a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 0.1;
1443
- }
1444
- }
1445
- }
1446
- if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) return null;
1447
- return {
1448
- type: "DROP_SHADOW",
1449
- offset: { x: offsetX, y: offsetY },
1450
- radius: Math.max(0, blur),
1451
- spread: Number.isFinite(spread) ? spread : 0,
1452
- color: { r, g, b, a },
1453
- visible: true,
1454
- blendMode: "NORMAL"
1455
- };
1456
- }
1457
- function parseBoxShadowToFigmaEffects(shadowStr) {
1458
- const s = (shadowStr || "").trim();
1459
- if (!s || s.toLowerCase() === "none") return [];
1460
- const out = [];
1461
- const segments = s.split(/\s*,\s*/);
1462
- for (const seg of segments) {
1463
- const effect = parseBoxShadowToFigmaEffect(seg.trim());
1464
- if (effect) out.push(effect);
1465
- }
1466
- return out;
1467
- }
1468
- function colorTo8DigitHex(color) {
1469
- if (!color || typeof color !== "string") return null;
1470
- const s = color.trim();
1471
- const rgbaMatch = s.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
1472
- if (rgbaMatch) {
1473
- const r = Math.max(0, Math.min(255, parseInt(rgbaMatch[1], 10)));
1474
- const g = Math.max(0, Math.min(255, parseInt(rgbaMatch[2], 10)));
1475
- const b = Math.max(0, Math.min(255, parseInt(rgbaMatch[3], 10)));
1476
- const a = rgbaMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4]))) : 1;
1477
- const aByte = Math.round(a * 255);
1478
- const hex = "#" + [r, g, b, aByte].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
1479
- return hex;
1480
- }
1481
- const hexMatch = s.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/i);
1482
- if (hexMatch) {
1483
- const rgb = hexMatch[1];
1484
- const aa = hexMatch[2] ?? "FF";
1485
- return `#${rgb}${aa}`.toUpperCase();
1486
- }
1487
- return null;
1488
- }
1489
- function typesetKeyToFontFamilyRole(key) {
1490
- const prefix = (key.split("-")[0] ?? key).toLowerCase();
1491
- if (prefix === "display" || prefix.startsWith("display")) return "display";
1492
- if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
1493
- if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
1494
- if (prefix.startsWith("body")) return "body";
1495
- return "body";
1496
- }
1497
- function buildFigmaPayloadsFromDS(data) {
1498
- const tokens = data.tokens;
1499
- const colors = tokens?.colors;
1500
- const typography = tokens?.typography;
1501
- const hexRe = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
1502
- const modes = [];
1503
- if (colors?.modes) {
1504
- const light = colors.modes.light ?? {};
1505
- const dark = colors.modes.dark ?? {};
1506
- if (Object.keys(light).length > 0) modes.push("Light");
1507
- if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
1508
- }
1509
- if (modes.length === 0) modes.push("Light");
1510
- const primitiveModes = ["Value"];
1511
- const dsName = data.meta?.name;
1512
- const collectionPrefix = dsName ? `${dsName} ` : "";
1513
- const referencedScaleNames = data.storedColors ? getReferencedScaleNames(data.storedColors) : /* @__PURE__ */ new Set();
1514
- const primitiveVariables = [];
1515
- const primitiveNames = /* @__PURE__ */ new Set();
1516
- const oneOffHexToFigmaName = /* @__PURE__ */ new Map();
1517
- const alphaScales = colors?.alphaScales;
1518
- if (alphaScales?.whiteAlpha && typeof alphaScales.whiteAlpha === "object") {
1519
- for (const [step, value] of Object.entries(alphaScales.whiteAlpha)) {
1520
- if (typeof value !== "string") continue;
1521
- const hex = colorTo8DigitHex(value);
1522
- if (!hex || !hexRe.test(hex)) continue;
1523
- const figmaName = `WhiteAlpha / ${step}`;
1524
- if (primitiveNames.has(figmaName)) continue;
1525
- primitiveNames.add(figmaName);
1526
- const values = {};
1527
- for (const m of primitiveModes) values[m] = hex;
1528
- primitiveVariables.push({ name: figmaName, values });
1529
- }
1530
- }
1531
- if (alphaScales?.blackAlpha && typeof alphaScales.blackAlpha === "object") {
1532
- for (const [step, value] of Object.entries(alphaScales.blackAlpha)) {
1533
- if (typeof value !== "string") continue;
1534
- const hex = colorTo8DigitHex(value);
1535
- if (!hex || !hexRe.test(hex)) continue;
1536
- const figmaName = `BlackAlpha / ${step}`;
1537
- if (primitiveNames.has(figmaName)) continue;
1538
- primitiveNames.add(figmaName);
1539
- const values = {};
1540
- for (const m of primitiveModes) values[m] = hex;
1541
- primitiveVariables.push({ name: figmaName, values });
1542
- }
1543
- }
1544
- if (colors?.scales && typeof colors.scales === "object") {
1545
- for (const [scaleName, steps] of Object.entries(colors.scales)) {
1546
- if (!steps || typeof steps !== "object") continue;
1547
- let groupDisplay = scaleName.charAt(0).toUpperCase() + scaleName.slice(1);
1548
- if (scaleName.toLowerCase() === "neutral" && data.storedColors) {
1549
- const neutral = data.storedColors.neutral;
1550
- if (neutral?.baseHueFamily) {
1551
- groupDisplay = refScaleToFigmaDisplayName(neutral.baseHueFamily);
1552
- }
1553
- }
1554
- for (const [step, hexVal] of Object.entries(steps)) {
1555
- if (typeof hexVal !== "string") continue;
1556
- const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
1557
- if (!hex || !hexRe.test(hex)) continue;
1558
- const figmaName = `${groupDisplay} / ${step}`;
1559
- if (primitiveNames.has(figmaName)) continue;
1560
- primitiveNames.add(figmaName);
1561
- const values = {};
1562
- for (const m of primitiveModes) values[m] = hex;
1563
- primitiveVariables.push({ name: figmaName, values });
1564
- }
1565
- }
1566
- }
1567
- const radixVariables = [];
1568
- const radixCollectionName = `${collectionPrefix}Colors Radix`;
1569
- for (const scaleName of referencedScaleNames) {
1570
- if (primitiveNames.has(`${scaleName} / 50`) || primitiveNames.has(`${scaleName} / 100`)) continue;
1571
- const fromStored = data.storedColors ? getFullScaleFromStored(data.storedColors, scaleName) : null;
1572
- const scaleData = fromStored ?? data.getRadixScale?.(scaleName) ?? null;
1573
- if (!scaleData) continue;
1574
- for (const [step, hexVal] of Object.entries(scaleData)) {
1575
- const figmaName = `${scaleName} / ${step}`;
1576
- if (primitiveNames.has(figmaName)) continue;
1577
- const hex = colorTo8DigitHex(hexVal) ?? (hexRe.test(hexVal) ? hexVal : null);
1578
- if (!hex || !hexRe.test(hex)) continue;
1579
- primitiveNames.add(figmaName);
1580
- const values = {};
1581
- for (const m of primitiveModes) values[m] = hex;
1582
- primitiveVariables.push({ name: figmaName, values });
1583
- }
1584
- }
1585
- if (colors?.oneOffs && Array.isArray(colors.oneOffs)) {
1586
- for (const oneOff of colors.oneOffs) {
1587
- if (!oneOff || typeof oneOff !== "object" || typeof oneOff.hex !== "string") continue;
1588
- const hex = colorTo8DigitHex(oneOff.hex) ?? (hexRe.test(oneOff.hex) ? oneOff.hex : null);
1589
- if (!hex || !hexRe.test(hex)) continue;
1590
- const name = typeof oneOff.name === "string" && oneOff.name ? oneOff.name : oneOff.id ?? "unnamed";
1591
- const figmaName = `One-off / ${name}`;
1592
- if (primitiveNames.has(figmaName)) continue;
1593
- primitiveNames.add(figmaName);
1594
- const values = {};
1595
- for (const m of primitiveModes) values[m] = hex;
1596
- primitiveVariables.push({ name: figmaName, values });
1597
- const normalizedHex = hex.replace(/^#/, "").toUpperCase();
1598
- const key8 = normalizedHex.length === 6 ? normalizedHex + "FF" : normalizedHex;
1599
- oneOffHexToFigmaName.set(key8, figmaName);
1600
- }
1601
- }
1602
- const semanticRefMap = data.storedColors && primitiveNames.size > 0 ? buildSemanticRefMap(data.storedColors, primitiveNames) : {};
1603
- const semanticVariables = [];
1604
- const semanticNames = /* @__PURE__ */ new Set();
1605
- const primitivesCollectionName = `${collectionPrefix}Colors Primitives`;
1606
- if (colors?.modes) {
1607
- const light = colors.modes.light ?? {};
1608
- const dark = colors.modes.dark ?? {};
1609
- const orderedKeys = [...Object.keys(light)];
1610
- for (const k of Object.keys(dark)) {
1611
- if (!orderedKeys.includes(k)) orderedKeys.push(k);
1612
- }
1613
- for (const key of orderedKeys) {
1614
- const lightVal = light[key];
1615
- const darkVal = dark[key];
1616
- const lightHex = typeof lightVal === "string" ? colorTo8DigitHex(lightVal) ?? (hexRe.test(lightVal) ? lightVal : null) : null;
1617
- if (lightHex && hexRe.test(lightHex)) {
1618
- const figmaName = figmaColorNameWithGroup(key);
1619
- if (semanticNames.has(figmaName)) continue;
1620
- semanticNames.add(figmaName);
1621
- const darkHex = typeof darkVal === "string" ? colorTo8DigitHex(darkVal) ?? (hexRe.test(darkVal) ? darkVal : null) : null;
1622
- const refs = semanticRefMap[key];
1623
- const values = {
1624
- ...modes.includes("Light") && { Light: lightHex },
1625
- ...modes.includes("Dark") && { Dark: darkHex && hexRe.test(darkHex) ? darkHex : lightHex }
1626
- };
1627
- const aliasByMode = {};
1628
- for (const m of modes) {
1629
- const aliasFromRef = m === "Light" ? refs?.Light : refs?.Dark;
1630
- if (aliasFromRef && primitiveNames.has(aliasFromRef)) {
1631
- aliasByMode[m] = aliasFromRef;
1632
- continue;
1633
- }
1634
- const hexForMode = m === "Light" ? lightHex : darkHex && hexRe.test(darkHex) ? darkHex : lightHex;
1635
- const norm = hexForMode.replace(/^#/, "").toUpperCase();
1636
- const key8 = norm.length === 6 ? norm + "FF" : norm;
1637
- const oneOffAlias = oneOffHexToFigmaName.get(key8);
1638
- if (oneOffAlias) {
1639
- aliasByMode[m] = oneOffAlias;
1640
- }
1641
- }
1642
- semanticVariables.push({
1643
- name: figmaName,
1644
- values,
1645
- ...Object.keys(aliasByMode).length > 0 ? { aliasByMode } : {}
1646
- });
1647
- }
1648
- }
1649
- }
1650
- if (colors?.static?.brand && typeof colors.static.brand === "object") {
1651
- for (const [key, hex] of Object.entries(colors.static.brand)) {
1652
- if (typeof hex !== "string" || !hexRe.test(hex)) continue;
1653
- const figmaName = figmaColorNameWithGroup(`brand/${key}`);
1654
- if (semanticNames.has(figmaName)) continue;
1655
- semanticNames.add(figmaName);
1656
- const values = {};
1657
- for (const m of modes) values[m] = hex;
1658
- semanticVariables.push({ name: figmaName, values });
1659
- }
1660
- }
1661
- const colorVariableCollections = [];
1662
- if (primitiveVariables.length > 0) {
1663
- colorVariableCollections.push({
1664
- collectionName: primitivesCollectionName,
1665
- modes: primitiveModes,
1666
- variables: primitiveVariables,
1667
- applyScopes: false
1668
- });
1669
- }
1670
- if (radixVariables.length > 0) {
1671
- colorVariableCollections.push({
1672
- collectionName: radixCollectionName,
1673
- modes,
1674
- variables: radixVariables,
1675
- applyScopes: false
1676
- });
1677
- }
1678
- if (semanticVariables.length > 0) {
1679
- const primitiveCollections = [];
1680
- if (primitiveVariables.length > 0) primitiveCollections.push(primitivesCollectionName);
1681
- if (radixVariables.length > 0) primitiveCollections.push(radixCollectionName);
1682
- colorVariableCollections.push({
1683
- collectionName: `${collectionPrefix}Colors Semantic`,
1684
- modes,
1685
- variables: semanticVariables,
1686
- applyScopes: true,
1687
- ...primitiveCollections.length > 1 ? { primitiveCollectionNames: primitiveCollections } : primitiveCollections.length === 1 ? { primitiveCollectionName: primitiveCollections[0] } : {}
1688
- });
1689
- }
1690
- const paintStyles = [];
1691
- const textStyles = [];
1692
- const sizeToPx = (val, basePx = 16) => {
1693
- if (typeof val === "number") return Math.round(val);
1694
- const s = String(val).trim();
1695
- const pxMatch = s.match(/^([\d.]+)\s*px$/i);
1696
- if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
1697
- const remMatch = s.match(/^([\d.]+)\s*rem$/i);
1698
- if (remMatch) return Math.round(parseFloat(remMatch[1]) * basePx);
1699
- const n = parseFloat(s);
1700
- if (Number.isFinite(n)) return n <= 0 ? basePx : n < 50 ? Math.round(n * basePx) : Math.round(n);
1701
- return basePx;
1702
- };
1703
- const letterSpacingToPx = (val, fontSizePx) => {
1704
- if (val === void 0 || val === null) return void 0;
1705
- if (typeof val === "number") return Math.round(val);
1706
- const s = String(val).trim();
1707
- const pxMatch = s.match(/^([-\d.]+)\s*px$/i);
1708
- if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
1709
- const emMatch = s.match(/^([-\d.]+)\s*em$/i);
1710
- if (emMatch) return Math.round(parseFloat(emMatch[1]) * fontSizePx);
1711
- const n = parseFloat(s);
1712
- return Number.isFinite(n) ? Math.round(n) : void 0;
1713
- };
1714
- const firstFont = (obj) => {
1715
- if (typeof obj === "string") {
1716
- const primary = obj.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
1717
- return primary || "Inter";
1718
- }
1719
- if (obj && typeof obj === "object" && !Array.isArray(obj)) {
1720
- const v = obj.body ?? obj.heading ?? obj.display ?? Object.values(obj)[0];
1721
- return firstFont(v);
1722
- }
1723
- return "Inter";
1724
- };
1725
- const toFontFamilyString = (val) => {
1726
- if (typeof val === "string") {
1727
- const s = val.trim().replace(/^["']|["']$/g, "");
1728
- return s || "Inter";
1729
- }
1730
- return firstFont(val);
1731
- };
1732
- const fontFamilyMap = typography?.fontFamily ?? {};
1733
- const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
1734
- const fontSizeMap = typography?.fontSize;
1735
- const fontWeightMap = typography?.fontWeight;
1736
- const lineHeightMap = typography?.lineHeight;
1737
- const letterSpacingMap = typography?.letterSpacing;
1738
- const textTransformMap = typography?.textTransform;
1739
- const textDecorationMap = typography?.textDecoration;
1740
- if (fontSizeMap && typeof fontSizeMap === "object" && Object.keys(fontSizeMap).length > 0) {
1741
- for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
1742
- const fontSize = sizeToPx(sizeVal);
1743
- if (fontSize <= 0) continue;
1744
- const role = typesetKeyToFontFamilyRole(key);
1745
- const fontFamily = toFontFamilyString(
1746
- fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
1747
- );
1748
- const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
1749
- const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
1750
- const fontWeight = weight != null ? String(weight) : "400";
1751
- const letterSpacingPx = letterSpacingToPx(
1752
- letterSpacingMap && typeof letterSpacingMap === "object" ? letterSpacingMap[key] : void 0,
1753
- fontSize
1754
- );
1755
- const textTransform = textTransformMap && typeof textTransformMap === "object" ? textTransformMap[key] : void 0;
1756
- const textDecoration = textDecorationMap && typeof textDecorationMap === "object" ? textDecorationMap[key] : void 0;
1757
- const namePart = key.replace(/-/g, " / ");
1758
- const style = {
1759
- name: namePart.startsWith("Typography") ? namePart : `Typography / ${namePart}`,
1760
- fontFamily,
1761
- fontWeight,
1762
- fontSize,
1763
- lineHeightUnit: "PERCENT",
1764
- letterSpacingUnit: "PIXELS",
1765
- ...letterSpacingPx !== void 0 && letterSpacingPx !== 0 ? { letterSpacingValue: letterSpacingPx } : {}
1766
- };
1767
- if (lh != null && typeof lh === "number" && lh > 0) {
1768
- style.lineHeightValue = lh >= 10 ? Math.round(lh / fontSize * 100) : Math.round(lh * 100);
1769
- } else {
1770
- style.lineHeightValue = 150;
1771
- }
1772
- if (textTransform === "uppercase") style.textCase = "UPPER";
1773
- else if (textTransform === "lowercase") style.textCase = "LOWER";
1774
- else if (textTransform === "capitalize") style.textCase = "TITLE";
1775
- else style.textCase = "ORIGINAL";
1776
- if (textDecoration === "underline") style.textDecoration = "UNDERLINE";
1777
- else style.textDecoration = "NONE";
1778
- textStyles.push(style);
1779
- }
1780
- }
1781
- const textStylesMap = typography?.textStyles;
1782
- if (textStyles.length === 0 && textStylesMap && typeof textStylesMap === "object") {
1783
- for (const [styleName, style] of Object.entries(textStylesMap)) {
1784
- if (!style || typeof style !== "object") continue;
1785
- const fontSize = sizeToPx(style.fontSize ?? "1rem");
1786
- const lhStr = style.lineHeight;
1787
- const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
1788
- const payload = {
1789
- name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
1790
- fontFamily: defaultFontFamily,
1791
- fontWeight: String(style.fontWeight ?? "400"),
1792
- fontSize,
1793
- lineHeightUnit: "PERCENT",
1794
- lineHeightValue: Math.round((Number.isFinite(lineHeightUnitless) ? lineHeightUnitless : 1.5) * 100),
1795
- letterSpacingUnit: "PIXELS",
1796
- textCase: "ORIGINAL",
1797
- textDecoration: "NONE"
1798
- };
1799
- textStyles.push(payload);
1800
- }
1801
- }
1802
- textStyles.sort((a, b) => {
1803
- if (a.fontSize !== b.fontSize) return a.fontSize - b.fontSize;
1804
- return (a.name || "").localeCompare(b.name || "");
1805
- });
1806
- const numberVariableCollections = [];
1807
- const spacing = tokens?.spacing;
1808
- if (spacing?.scale && typeof spacing.scale === "object") {
1809
- const vars = [];
1810
- for (const [key, val] of Object.entries(spacing.scale)) {
1811
- const n = tokenValueToNumber(val);
1812
- if (n >= 0) vars.push({ name: `Spacing / ${key}`, value: n });
1813
- }
1814
- vars.sort((a, b) => a.value - b.value);
1815
- if (vars.length > 0)
1816
- numberVariableCollections.push({
1817
- collectionName: `${collectionPrefix}Spacing`,
1818
- categoryKey: "Spacing",
1819
- variables: vars,
1820
- scopes: ["GAP"]
1821
- });
1822
- }
1823
- const radius = tokens?.radius;
1824
- if (radius?.scale && typeof radius.scale === "object") {
1825
- const vars = [];
1826
- for (const [key, val] of Object.entries(radius.scale)) {
1827
- const n = tokenValueToNumber(val);
1828
- if (n >= 0) vars.push({ name: `Radius / ${key}`, value: n });
1829
- }
1830
- vars.sort((a, b) => a.value - b.value);
1831
- if (vars.length > 0)
1832
- numberVariableCollections.push({
1833
- collectionName: `${collectionPrefix}Radius`,
1834
- categoryKey: "Radius",
1835
- variables: vars,
1836
- scopes: ["CORNER_RADIUS"]
1837
- });
1838
- }
1839
- const borders = tokens?.borders;
1840
- if (borders?.width && typeof borders.width === "object") {
1841
- const vars = [];
1842
- for (const [key, val] of Object.entries(borders.width)) {
1843
- const n = tokenValueToNumber(val);
1844
- if (n >= 0) vars.push({ name: `Borders / ${key}`, value: n });
1845
- }
1846
- vars.sort((a, b) => a.value - b.value);
1847
- if (vars.length > 0)
1848
- numberVariableCollections.push({
1849
- collectionName: `${collectionPrefix}Borders`,
1850
- categoryKey: "Borders",
1851
- variables: vars,
1852
- scopes: ["STROKE_FLOAT"]
1853
- });
1854
- }
1855
- const sizing = tokens?.sizing;
1856
- const sizingVariables = [];
1857
- if (sizing?.height && typeof sizing.height === "object") {
1858
- for (const [key, val] of Object.entries(sizing.height)) {
1859
- const n = tokenValueToNumber(val);
1860
- if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
1861
- }
1862
- }
1863
- if (sizing?.icon && typeof sizing.icon === "object") {
1864
- for (const [key, val] of Object.entries(sizing.icon)) {
1865
- const n = tokenValueToNumber(val);
1866
- if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
1867
- }
1868
- }
1869
- sizingVariables.sort((a, b) => a.value - b.value);
1870
- if (sizingVariables.length > 0) {
1871
- numberVariableCollections.push({
1872
- collectionName: `${collectionPrefix}Sizing`,
1873
- categoryKey: "Sizing",
1874
- variables: sizingVariables,
1875
- scopes: ["WIDTH_HEIGHT"]
1876
- });
1877
- }
1878
- const layout = tokens?.layout;
1879
- if (layout?.breakpoint && typeof layout.breakpoint === "object") {
1880
- const vars = [];
1881
- for (const [key, val] of Object.entries(layout.breakpoint)) {
1882
- const n = tokenValueToNumber(val);
1883
- if (n >= 0) vars.push({ name: `Breakpoint / ${key}`, value: n });
1884
- }
1885
- vars.sort((a, b) => a.value - b.value);
1886
- if (vars.length > 0)
1887
- numberVariableCollections.push({
1888
- collectionName: `${collectionPrefix}Layout`,
1889
- categoryKey: "Layout",
1890
- variables: vars,
1891
- scopes: ["WIDTH_HEIGHT"]
1892
- });
1893
- }
1894
- const effectStyles = [];
1895
- const shadows = tokens?.shadows;
1896
- if (shadows?.elevation && typeof shadows.elevation === "object") {
1897
- for (const [key, val] of Object.entries(shadows.elevation)) {
1898
- if (typeof val !== "string") continue;
1899
- const effects = parseBoxShadowToFigmaEffects(val);
1900
- if (effects.length > 0) effectStyles.push({ name: `Shadow / ${key}`, effects });
1901
- }
1902
- }
1903
- if (shadows?.focus && typeof shadows.focus === "string") {
1904
- const effects = parseBoxShadowToFigmaEffects(shadows.focus);
1905
- if (effects.length > 0) effectStyles.push({ name: "Shadow / focus", effects });
1906
- }
1907
- effectStyles.sort((a, b) => {
1908
- const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
1909
- const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
1910
- const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
1911
- const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
1912
- if (orderA !== orderB) return orderA - orderB;
1913
- return nameA.localeCompare(nameB);
1914
- });
1915
- return {
1916
- colorVariableCollections,
1917
- paintStyles,
1918
- textStyles,
1919
- numberVariableCollections,
1920
- effectStyles
1921
- };
1922
- }
1923
-
1924
1215
  // src/index.ts
1925
1216
  import * as path from "path";
1926
1217
  import * as fs from "fs";
1927
- import { execSync } from "child_process";
1928
- import { platform } from "os";
1929
- import WebSocket, { WebSocketServer } from "ws";
1930
- var FIGMA_BRIDGE_PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 8765;
1931
- var FIGMA_BRIDGE_HOST = process.env.FIGMA_BRIDGE_HOST || "127.0.0.1";
1932
- var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
1933
- var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
1934
- var FIGMA_CONNECTION_INSTRUCTIONS = {
1935
- 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.",
1936
- connect: 'In the plugin UI, tap **Connect** and wait until the status shows "Connected".',
1937
- 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."
1938
- };
1939
- var bridgeWss = null;
1940
- var pluginWs = null;
1941
- var pendingBridgeRequests = /* @__PURE__ */ new Map();
1942
- function ensureFigmaBridgePortFree(port) {
1943
- const portStr = String(port);
1944
- const ourPid = String(process.pid);
1945
- try {
1946
- if (platform() === "win32") {
1947
- const out = execSync(`netstat -ano`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1948
- const pids = /* @__PURE__ */ new Set();
1949
- for (const line of out.split(/\r?\n/)) {
1950
- if (line.includes(`:${portStr}`) && line.includes("LISTENING")) {
1951
- const parts = line.trim().split(/\s+/);
1952
- const pid = parts[parts.length - 1];
1953
- if (/^\d+$/.test(pid) && pid !== ourPid) pids.add(pid);
1954
- }
1955
- }
1956
- for (const pid of pids) {
1957
- try {
1958
- execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1959
- console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
1960
- } catch (_) {
1961
- }
1962
- }
1963
- } else {
1964
- const out = execSync(`lsof -ti :${portStr}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1965
- if (!out) return;
1966
- const pids = out.split(/\s+/).filter((p) => p && p !== ourPid);
1967
- for (const pid of pids) {
1968
- try {
1969
- execSync(`kill -9 ${pid}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1970
- console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
1971
- } catch (_) {
1972
- }
1973
- }
1974
- }
1975
- } catch {
1976
- }
1977
- }
1978
- function startFigmaBridge() {
1979
- if (bridgeWss) return;
1980
- try {
1981
- ensureFigmaBridgePortFree(FIGMA_BRIDGE_PORT);
1982
- bridgeWss = new WebSocketServer({
1983
- host: FIGMA_BRIDGE_HOST,
1984
- port: FIGMA_BRIDGE_PORT,
1985
- clientTracking: true
1986
- });
1987
- bridgeWss.on("connection", (ws, req) => {
1988
- const url = req.url || "";
1989
- const params = new URLSearchParams(url.startsWith("/") ? url.slice(1) : url);
1990
- const token = params.get("token");
1991
- const role = params.get("role");
1992
- if (FIGMA_BRIDGE_TOKEN && token !== FIGMA_BRIDGE_TOKEN) {
1993
- ws.close(4003, "Invalid or missing bridge token");
1994
- return;
1995
- }
1996
- if (role !== "plugin") {
1997
- ws.close(4002, "Only role=plugin is accepted (bridge runs in MCP server)");
1998
- return;
1999
- }
2000
- if (pluginWs) {
2001
- try {
2002
- pluginWs.close();
2003
- } catch (_) {
2004
- }
2005
- pluginWs = null;
2006
- }
2007
- pluginWs = ws;
2008
- ws.on("message", (raw) => {
2009
- const text = typeof raw === "string" ? raw : raw.toString("utf8");
2010
- let msg;
2011
- try {
2012
- msg = JSON.parse(text);
2013
- } catch {
2014
- return;
2015
- }
2016
- const parsed = msg;
2017
- if (parsed?.type === "ping" && typeof parsed.id === "string") {
2018
- try {
2019
- ws.send(JSON.stringify({ type: "pong", id: parsed.id }));
2020
- } catch (_) {
2021
- }
2022
- return;
2023
- }
2024
- if (typeof parsed.id === "string" && ("result" in parsed || "error" in parsed)) {
2025
- const pending = pendingBridgeRequests.get(parsed.id);
2026
- if (pending) {
2027
- clearTimeout(pending.timeout);
2028
- pendingBridgeRequests.delete(parsed.id);
2029
- if (parsed.error) pending.reject(new Error(parsed.error));
2030
- else pending.resolve(parsed.result);
2031
- }
2032
- }
2033
- });
2034
- ws.on("close", () => {
2035
- if (pluginWs === ws) pluginWs = null;
2036
- });
2037
- ws.on("error", () => {
2038
- if (pluginWs === ws) pluginWs = null;
2039
- });
2040
- });
2041
- bridgeWss.on("listening", () => {
2042
- console.error(`[atomix-mcp] Figma bridge listening on ws://${FIGMA_BRIDGE_HOST}:${FIGMA_BRIDGE_PORT} (local only)`);
2043
- if (FIGMA_BRIDGE_TOKEN) {
2044
- console.error("[atomix-mcp] Figma bridge token required (FIGMA_BRIDGE_TOKEN)");
2045
- }
2046
- });
2047
- bridgeWss.on("error", (err) => {
2048
- console.error("[atomix-mcp] Figma bridge server error:", err);
2049
- });
2050
- } catch (err) {
2051
- console.error("[atomix-mcp] Failed to start Figma bridge:", err);
2052
- }
2053
- }
2054
- function closeFigmaBridge() {
2055
- if (pluginWs) {
2056
- try {
2057
- pluginWs.close();
2058
- } catch (_) {
2059
- }
2060
- pluginWs = null;
2061
- }
2062
- if (bridgeWss) {
2063
- try {
2064
- bridgeWss.close();
2065
- } catch (_) {
2066
- }
2067
- bridgeWss = null;
2068
- }
2069
- }
2070
- function isBridgeReachable() {
2071
- return Promise.resolve(!!(pluginWs && pluginWs.readyState === WebSocket.OPEN));
2072
- }
2073
- function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS) {
2074
- const normalized = normalizeBridgeMethod(method);
2075
- if (!isAllowedMethod(normalized)) {
2076
- return Promise.reject(new Error(`Bridge method not allowed: ${method}`));
2077
- }
2078
- const ws = pluginWs;
2079
- if (!ws || ws.readyState !== WebSocket.OPEN) {
2080
- return Promise.reject(
2081
- new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect.")
2082
- );
2083
- }
2084
- const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2085
- return new Promise((resolve3, reject) => {
2086
- const timeout = setTimeout(() => {
2087
- if (pendingBridgeRequests.delete(id)) {
2088
- reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
2089
- }
2090
- }, timeoutMs);
2091
- pendingBridgeRequests.set(id, { resolve: resolve3, reject, timeout });
2092
- try {
2093
- ws.send(JSON.stringify({ id, method: normalized, params }));
2094
- } catch (e) {
2095
- pendingBridgeRequests.delete(id);
2096
- clearTimeout(timeout);
2097
- reject(e instanceof Error ? e : new Error(String(e)));
2098
- }
2099
- });
2100
- }
2101
1218
  function parseArgs() {
2102
1219
  const args = process.argv.slice(2);
2103
1220
  let dsId2 = null;
@@ -2124,7 +1241,7 @@ function parseArgs() {
2124
1241
  var cliArgs = parseArgs();
2125
1242
  var { dsId, apiKey, accessToken } = cliArgs;
2126
1243
  var apiBase = cliArgs.apiBase || "https://atomix.studio";
2127
- var MCP_VERSION = "1.0.33";
1244
+ var MCP_VERSION = "1.0.36";
2128
1245
  var cachedData = null;
2129
1246
  var cachedETag = null;
2130
1247
  var cachedMcpTier = null;
@@ -2230,7 +1347,7 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
2230
1347
  return result.data;
2231
1348
  }
2232
1349
  var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
2233
- function typesetKeyToFontFamilyRole2(key) {
1350
+ function typesetKeyToFontFamilyRole(key) {
2234
1351
  const prefix = (key.split("-")[0] ?? key).toLowerCase();
2235
1352
  if (prefix === "display" || prefix.startsWith("display")) return "display";
2236
1353
  if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
@@ -2250,7 +1367,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
2250
1367
  const p = cssPrefix ? `${cssPrefix}-` : "";
2251
1368
  const typesets = [];
2252
1369
  for (const key of Object.keys(fontSize)) {
2253
- const role = typesetKeyToFontFamilyRole2(key);
1370
+ const role = typesetKeyToFontFamilyRole(key);
2254
1371
  const familyName = fontFamily[role] ?? fontFamily.body;
2255
1372
  const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
2256
1373
  const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
@@ -2298,7 +1415,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2298
1415
  if (cachedMcpTier === "pro") {
2299
1416
  console.error("[Atomix MCP] Resolved tier = pro.");
2300
1417
  } else if (cachedMcpTier === "free") {
2301
- console.error("[Atomix MCP] Resolved tier = free. syncToFigma and /--sync-to-figma are available.");
1418
+ console.error("[Atomix MCP] Resolved tier = free.");
2302
1419
  }
2303
1420
  } catch (err) {
2304
1421
  const msg = err instanceof Error ? err.message : String(err);
@@ -2439,7 +1556,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2439
1556
  },
2440
1557
  {
2441
1558
  name: "syncAll",
2442
- description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
1559
+ description: "Sync tokens, AI rules, skill (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
2443
1560
  inputSchema: {
2444
1561
  type: "object",
2445
1562
  properties: {
@@ -2495,15 +1612,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2495
1612
  properties: {},
2496
1613
  required: []
2497
1614
  }
2498
- },
2499
- {
2500
- name: "syncToFigma",
2501
- description: "Push the owner's design system to Figma: creates color variable collection (Light/Dark), color and paint styles, number variables (spacing, radius, borders, sizing, breakpoints), text styles, and shadow effect styles. Uses local WebSocket bridge and Atomix Figma plugin (no Figma REST API). No arguments. If the bridge is not running, the response includes agentInstruction to start it; only if that fails should the user start the bridge and connect the plugin. Call this when the user asks to 'sync to Figma' or 'push DS to Figma'.",
2502
- inputSchema: {
2503
- type: "object",
2504
- properties: {},
2505
- required: []
2506
- }
2507
1615
  }
2508
1616
  ];
2509
1617
  return { tools: toolsList };
@@ -2681,7 +1789,8 @@ Version: ${designSystemData.meta.version}`,
2681
1789
  hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
2682
1790
  deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
2683
1791
  });
2684
- return { responseText: response, rulesResults, validation };
1792
+ const tokenSyncChangeSummary = fileExists && ["css", "scss", "less"].includes(format) ? { fileExisted: true, previousVarCount: existingTokens.size, currentVarCount: tokenCount } : { fileExisted: false, previousVarCount: void 0, currentVarCount: tokenCount };
1793
+ return { responseText: response, rulesResults, validation, tokenSyncChangeSummary };
2685
1794
  }
2686
1795
  switch (name) {
2687
1796
  case "getToken": {
@@ -2853,7 +1962,7 @@ Version: ${designSystemData.meta.version}`,
2853
1962
  const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
2854
1963
  console.error(`[getRules] Fetching: ${rulesUrl}${topic ? ` topic=${topic}` : ""}`);
2855
1964
  const headers = { "Content-Type": "application/json" };
2856
- if (apiKey) headers["x-api-key"] = apiKey;
1965
+ if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
2857
1966
  try {
2858
1967
  const response = await fetch(rulesUrl, { headers });
2859
1968
  console.error(`[getRules] Response status: ${response.status}`);
@@ -3113,10 +2222,12 @@ Version: ${designSystemData.meta.version}`,
3113
2222
  const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
3114
2223
  let tokenResponseText = "";
3115
2224
  const allValidation = [];
2225
+ let tokenSyncChangeSummary;
3116
2226
  if (!skipTokens) {
3117
2227
  const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
3118
2228
  tokenResponseText = result.responseText;
3119
2229
  allValidation.push(...result.validation);
2230
+ tokenSyncChangeSummary = result.tokenSyncChangeSummary;
3120
2231
  if (dryRun) {
3121
2232
  parts.push(`Would write tokens: ${output} (${format})`);
3122
2233
  } else {
@@ -3126,10 +2237,11 @@ Version: ${designSystemData.meta.version}`,
3126
2237
  const dsVersion = String(data.meta.version ?? "1.0.0");
3127
2238
  const dsExportedAt = data.meta.exportedAt;
3128
2239
  const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
3129
- const skillPath1 = path.join(skillsDir, "SKILL.md");
3130
2240
  const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
2241
+ const dependencySkills = getSyncDependencySkills(dsVersion, dsExportedAt);
3131
2242
  if (dryRun) {
3132
- parts.push("Would write skills: .cursor/skills/atomix-ds/SKILL.md");
2243
+ const skillList = dependencySkills.map((s) => s.path).join(", ");
2244
+ parts.push(`Would write skills: ${skillList}`);
3133
2245
  parts.push("Would write manifest: atomix-dependencies.json");
3134
2246
  const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
3135
2247
  return {
@@ -3139,10 +2251,18 @@ ${reportText}` }]
3139
2251
  };
3140
2252
  }
3141
2253
  if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
3142
- const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
3143
- fs.writeFileSync(skillPath1, genericWithVersion);
3144
- allValidation.push({ path: skillPath1, status: fs.existsSync(skillPath1) ? "OK" : "FAIL", detail: "Written." });
3145
- parts.push("Skills: .cursor/skills/atomix-ds/SKILL.md (synced at DS v" + dsVersion + ")");
2254
+ const skillsWritten = [];
2255
+ for (const sk of dependencySkills) {
2256
+ const absPath = path.resolve(projectRoot, sk.path);
2257
+ const existed = fs.existsSync(absPath);
2258
+ const prevVersion = existed ? readSkillVersionFromFile(absPath) : null;
2259
+ const fromLabel = existed ? prevVersion ? `DS v${prevVersion}` : "existing" : "missing";
2260
+ fs.writeFileSync(absPath, sk.content);
2261
+ allValidation.push({ path: absPath, status: fs.existsSync(absPath) ? "OK" : "FAIL", detail: "Written." });
2262
+ skillsWritten.push({ path: sk.path, shortName: sk.shortName, from: fromLabel });
2263
+ }
2264
+ const skillShortNames = skillsWritten.map((s) => s.shortName).join(" + ");
2265
+ parts.push(`Skills: ${skillShortNames} (DS v${dsVersion})`);
3146
2266
  const tokens = data.tokens;
3147
2267
  const typography = tokens?.typography;
3148
2268
  const fontFamily = typography?.fontFamily;
@@ -3161,6 +2281,7 @@ ${reportText}` }]
3161
2281
  };
3162
2282
  const lib = icons?.library || "lucide";
3163
2283
  const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
2284
+ const skillsManifestEntry = { skill: ".cursor/skills/atomix-ds/SKILL.md", syncedAtVersion: String(data.meta.version ?? "1.0.0") };
3164
2285
  const manifest = {
3165
2286
  designSystem: { name: data.meta.name, version: data.meta.version },
3166
2287
  tokenFile: skipTokens ? void 0 : output,
@@ -3171,29 +2292,73 @@ ${reportText}` }]
3171
2292
  strokeWidthValue: icons?.strokeWidth
3172
2293
  },
3173
2294
  fonts: { families: fontNames },
3174
- skills: {
3175
- skill: ".cursor/skills/atomix-ds/SKILL.md",
3176
- syncedAtVersion: data.meta.version ?? "1.0.0"
3177
- }
2295
+ skills: skillsManifestEntry
3178
2296
  };
2297
+ const manifestExisted = fs.existsSync(manifestPath);
3179
2298
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3180
2299
  allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
3181
2300
  parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
2301
+ const changeRows = [];
2302
+ if (!skipTokens && tokenSyncChangeSummary) {
2303
+ const fromStr = tokenSyncChangeSummary.fileExisted && tokenSyncChangeSummary.previousVarCount != null ? `${tokenSyncChangeSummary.previousVarCount} tokens in ${output}` : "missing";
2304
+ changeRows.push({
2305
+ what: "Tokens",
2306
+ from: fromStr,
2307
+ to: `${tokenSyncChangeSummary.currentVarCount} tokens in ${output} (${format})`
2308
+ });
2309
+ }
2310
+ const skillsFrom = skillsWritten.length ? skillsWritten.every((s) => s.from === "missing") ? "missing" : "updated" : "";
2311
+ if (skillsWritten.length > 0) {
2312
+ changeRows.push({
2313
+ what: "Skills",
2314
+ from: skillsFrom,
2315
+ to: `${skillShortNames} (DS v${dsVersion})`
2316
+ });
2317
+ }
2318
+ changeRows.push({
2319
+ what: "Manifest",
2320
+ from: manifestExisted ? "existing" : "missing",
2321
+ to: "atomix-dependencies.json"
2322
+ });
2323
+ if (lastSyncAffectedTokens && (lastSyncAffectedTokens.modified.length > 0 || lastSyncAffectedTokens.added.length > 0 || lastSyncAffectedTokens.removed.length > 0)) {
2324
+ const mod = lastSyncAffectedTokens.modified;
2325
+ const add = lastSyncAffectedTokens.added;
2326
+ const rem = lastSyncAffectedTokens.removed;
2327
+ const tokenNames = [
2328
+ ...mod.map((m) => m.token),
2329
+ ...add,
2330
+ ...rem.map((r) => r.token)
2331
+ ];
2332
+ const fromParts = [];
2333
+ if (mod.length) fromParts.push(`${mod.length} modified`);
2334
+ if (add.length) fromParts.push(`${add.length} added`);
2335
+ if (rem.length) fromParts.push(`${rem.length} removed`);
2336
+ const toDetail = tokenNames.length <= 8 ? tokenNames.join(", ") : `${tokenNames.length} tokens (${tokenNames.slice(0, 4).join(", ")}...)`;
2337
+ changeRows.push({
2338
+ what: "Token changes (this run)",
2339
+ from: fromParts.join(", ") || "\u2014",
2340
+ to: toDetail
2341
+ });
2342
+ if (mod.length > 0 && mod.length <= 15) {
2343
+ mod.forEach((m) => {
2344
+ changeRows.push({ what: ` ${m.token}`, from: m.oldValue, to: m.newValue });
2345
+ });
2346
+ }
2347
+ }
2348
+ const changesList = "Report the following changes to the user (From \u2192 To). Do not mention what did not change.\n\n**Changes applied**\n\n" + changeRows.map((r) => `\u2022 **${r.what}:** ${r.from} \u2192 ${r.to}`).join("\n");
3182
2349
  const summary = parts.join("\n");
3183
2350
  const validationBlock = formatValidationBlock(allValidation);
3184
2351
  const hasFailure = allValidation.some((e) => e.status === "FAIL");
3185
2352
  const resultLine = hasFailure ? "syncAllResult: FAIL \u2014 Check VALIDATION section below. Do not report success to the user.\n\n" : "syncAllResult: OK\n\n";
3186
- const fullText = resultLine + (tokenResponseText ? `${summary}
2353
+ const fullText = resultLine + changesList + "\n\n" + (tokenResponseText ? `---
3187
2354
 
3188
- ---
3189
-
3190
- ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
2355
+ ${tokenResponseText}` : "") + validationBlock;
3191
2356
  return {
3192
2357
  content: [{ type: "text", text: fullText }]
3193
2358
  };
3194
2359
  }
3195
2360
  case "getDependencies": {
3196
- const platform2 = args?.platform;
2361
+ const platform = args?.platform;
3197
2362
  const stack = args?.stack;
3198
2363
  const tokens = data.tokens;
3199
2364
  const typography = tokens?.typography;
@@ -3225,22 +2390,23 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3225
2390
  families: fontNames,
3226
2391
  performanceHint: "Link fonts via URL (e.g. Google Fonts <link> or CSS @import); no need to download font files or add them to the repo. Prefer font-display: swap when possible. You must also build a complete typeset CSS: call listTypesets to get every typeset from the design system, then 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 so the result is 1:1 with the DS. Use the CSS variable names returned by listTypesets. Do not create a file that only contains a font import."
3227
2392
  },
3228
- skill: {
3229
- path: ".cursor/skills/atomix-ds/SKILL.md",
3230
- content: GENERIC_SKILL_MD
3231
- },
2393
+ skill: (() => {
2394
+ const list = getSyncDependencySkills(String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
2395
+ const generic = list.find((s) => s.shortName === "SKILL.md");
2396
+ return generic ? { path: generic.path, content: GENERIC_SKILL_MD } : { path: ".cursor/skills/atomix-ds/SKILL.md", content: GENERIC_SKILL_MD };
2397
+ })(),
3232
2398
  tokenFiles: {
3233
2399
  files: ["tokens.css", "tokens.json"],
3234
2400
  copyInstructions: "Call the syncAll MCP tool to create the token file, skills, and atomix-dependencies.json; do not only suggest the user run sync later."
3235
2401
  },
3236
- showcase: platform2 === "web" || !platform2 ? {
2402
+ showcase: platform === "web" || !platform ? {
3237
2403
  path: "atomix-setup-showcase.html",
3238
2404
  template: SHOWCASE_HTML_TEMPLATE,
3239
2405
  substitutionInstructions: 'The synced token file (from syncAll) always uses the --atmx- prefix for every CSS variable. Keep all var(--atmx-*) references in the template; do not remove or change the prefix. Replace placeholders with values from the synced token file. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css). {{TYPESETS_LINK}} = if a typeset CSS file was created, the full <link rel=\\"stylesheet\\" href=\\"typesets.css\\"> tag, otherwise empty string. {{DS_NAME}} = design system name. {{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 empty string if no typeset file. {{LARGEST_BODY_TYPESET_CLASS}} = largest body typeset class from listTypesets (body role, largest font size; e.g. typeset-body-lg), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = default body typeset class from listTypesets (e.g. typeset-body-md), or empty string. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). Icon on circle uses luminance of brand primary (script sets white or black); no semantic foreground var. {{BUTTON_PADDING_VAR}} = var(--atmx-spacing-scale-md) or closest spacing token for button padding. {{BUTTON_HEIGHT_VAR}} = var(--atmx-sizing-height-md) or closest height token. {{BUTTON_RADIUS_VAR}} = var(--atmx-radius-scale-md) or var(--atmx-radius-scale-lg). {{CIRCLE_PADDING_VAR}} = var(--atmx-spacing-scale-md) or var(--atmx-spacing-scale-sm) for icon circle padding. {{ICON_SIZE_VAR}} = var(--atmx-sizing-icon-md) or var(--atmx-sizing-icon-lg). {{CHECK_ICON_SVG}} = inline SVG for Check icon from the design system icon library (getDependencies.iconLibrary.package: lucide-react, @heroicons/react, or phosphor-react). Use 24x24 viewBox; stroke=\\"currentColor\\" for Lucide/Heroicons, fill=\\"currentColor\\" for Phosphor so the script can set icon color by luminance. If unavailable, use: <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\\"><path d=\\"M20 6L9 17l-5-5\\"/></svg>. Do not invent CSS variable names; use only vars that exist in the export.'
3240
2406
  } : void 0,
3241
2407
  meta: {
3242
2408
  dsName: data.meta.name,
3243
- platform: platform2 ?? void 0,
2409
+ platform: platform ?? void 0,
3244
2410
  stack: stack ?? void 0,
3245
2411
  designSystemVersion: data.meta.version ?? "1.0.0",
3246
2412
  designSystemExportedAt: data.meta.exportedAt
@@ -3253,213 +2419,13 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3253
2419
  }]
3254
2420
  };
3255
2421
  }
3256
- case "syncToFigma": {
3257
- const payloads = buildFigmaPayloadsFromDS(data);
3258
- const out = {};
3259
- 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.`;
3260
- const userSteps = [
3261
- FIGMA_CONNECTION_INSTRUCTIONS.startBridge,
3262
- FIGMA_CONNECTION_INSTRUCTIONS.installAndRun,
3263
- FIGMA_CONNECTION_INSTRUCTIONS.connect,
3264
- "Then run Sync to Figma again."
3265
- ].join(" ");
3266
- try {
3267
- const reachable = await isBridgeReachable();
3268
- if (!reachable) {
3269
- out.bridgeNotRunning = true;
3270
- out.agentInstruction = agentStartBridge;
3271
- out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
3272
- out.figmaPayload = {
3273
- colorVariableCollections: payloads.colorVariableCollections,
3274
- paintStyles: payloads.paintStyles,
3275
- textStyles: payloads.textStyles,
3276
- numberVariableCollections: payloads.numberVariableCollections,
3277
- effectStyles: payloads.effectStyles
3278
- };
3279
- return {
3280
- content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
3281
- };
3282
- }
3283
- const colorResults = [];
3284
- for (const coll of payloads.colorVariableCollections) {
3285
- if (coll.variables.length > 0) {
3286
- const result = await sendBridgeRequest("create_color_variables", {
3287
- collectionName: coll.collectionName,
3288
- modes: coll.modes,
3289
- variables: coll.variables,
3290
- removeVariablesNotInPayload: true,
3291
- applyScopes: coll.applyScopes
3292
- });
3293
- colorResults.push({ collectionName: coll.collectionName, result });
3294
- }
3295
- }
3296
- if (colorResults.length > 0) {
3297
- out.colorVariables = colorResults.length === 1 ? colorResults[0].result : colorResults;
3298
- }
3299
- if (payloads.paintStyles.length > 0) {
3300
- out.paintStyles = await sendBridgeRequest("create_paint_styles", {
3301
- styles: payloads.paintStyles,
3302
- removePaintStylesNotInPayload: true
3303
- });
3304
- }
3305
- if (payloads.numberVariableCollections.length > 0) {
3306
- const numberResults = [];
3307
- try {
3308
- for (const coll of payloads.numberVariableCollections) {
3309
- const result = await sendBridgeRequest("create_number_variables", {
3310
- collectionName: coll.collectionName,
3311
- variables: coll.variables.map((v) => ({ name: v.name, value: v.value })),
3312
- scopes: coll.scopes,
3313
- removeVariablesNotInPayload: true
3314
- });
3315
- numberResults.push({ categoryKey: coll.categoryKey, result });
3316
- }
3317
- out.numberVariables = numberResults;
3318
- } catch (e) {
3319
- const msg = e instanceof Error ? e.message : String(e);
3320
- out.numberVariables = { error: msg };
3321
- if (msg.includes("Method not allowed") && msg.includes("create_number_variables")) {
3322
- out.numberVariablesHint = "Number variables require the latest Atomix Figma plugin and bridge. Rebuild the plugin and bridge, reload the plugin in Figma, restart the bridge, then sync again.";
3323
- }
3324
- }
3325
- }
3326
- if (payloads.textStyles.length > 0) {
3327
- out.textStyles = await sendBridgeRequest("create_text_styles", {
3328
- styles: payloads.textStyles,
3329
- removeTextStylesNotInPayload: true
3330
- });
3331
- }
3332
- if (payloads.effectStyles.length > 0) {
3333
- out.effectStyles = await sendBridgeRequest("create_effect_styles", {
3334
- styles: payloads.effectStyles,
3335
- removeShadowStylesNotInPayload: true
3336
- });
3337
- }
3338
- out.figmaPayload = {
3339
- colorVariableCollections: payloads.colorVariableCollections,
3340
- paintStyles: payloads.paintStyles,
3341
- textStyles: payloads.textStyles,
3342
- numberVariableCollections: payloads.numberVariableCollections,
3343
- effectStyles: payloads.effectStyles
3344
- };
3345
- } catch (e) {
3346
- out.error = e instanceof Error ? e.message : String(e);
3347
- out.figmaPayload = {
3348
- colorVariableCollections: payloads.colorVariableCollections,
3349
- paintStyles: payloads.paintStyles,
3350
- textStyles: payloads.textStyles,
3351
- numberVariableCollections: payloads.numberVariableCollections,
3352
- effectStyles: payloads.effectStyles
3353
- };
3354
- const errMsg = out.error.toLowerCase();
3355
- const connectionFailure = errMsg.includes("econnrefused") || errMsg.includes("bridge timeout") || errMsg.includes("websocket") || errMsg.includes("network");
3356
- if (connectionFailure) {
3357
- out.bridgeNotRunning = true;
3358
- out.agentInstruction = agentStartBridge;
3359
- out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
3360
- } else if (errMsg.includes("plugin not connected") || errMsg.includes("figma plugin")) {
3361
- out.userInstruction = `${FIGMA_CONNECTION_INSTRUCTIONS.installAndRun} ${FIGMA_CONNECTION_INSTRUCTIONS.connect}`;
3362
- }
3363
- }
3364
- const textStylesResult = out.textStyles;
3365
- if (textStylesResult?.failed && textStylesResult.failures?.length) {
3366
- const firstReason = textStylesResult.failures[0].reason;
3367
- out.summary = `Text styles: ${textStylesResult.failed} could not be created. ${firstReason}`;
3368
- }
3369
- if (out.numberVariablesHint) {
3370
- out.summary = [out.summary, out.numberVariablesHint].filter(Boolean).join(" ");
3371
- }
3372
- const summaryParts = [];
3373
- const colorResult = out.colorVariables;
3374
- if (colorResult) {
3375
- const results = Array.isArray(colorResult) ? colorResult : [{ result: colorResult }];
3376
- let totalSynced = 0;
3377
- let totalRemoved = 0;
3378
- for (const r of results) {
3379
- const res = r.result;
3380
- if (res?.variableNames?.length) totalSynced += res.variableNames.length;
3381
- if ((res?.removed ?? 0) > 0) totalRemoved += res.removed ?? 0;
3382
- }
3383
- if (totalSynced > 0 || totalRemoved > 0) {
3384
- const parts = [];
3385
- if (totalSynced > 0) parts.push(`${totalSynced} synced`);
3386
- if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
3387
- summaryParts.push(`Colors: ${parts.join(", ")}.`);
3388
- }
3389
- }
3390
- const paintResult = out.paintStyles;
3391
- if (paintResult) {
3392
- const c = paintResult.created ?? 0;
3393
- const u = paintResult.updated ?? 0;
3394
- const r = paintResult.removed ?? 0;
3395
- if (c + u + r > 0) {
3396
- const parts = [];
3397
- if (c > 0) parts.push(`${c} created`);
3398
- if (u > 0) parts.push(`${u} updated`);
3399
- if (r > 0) parts.push(`${r} removed`);
3400
- summaryParts.push(`Paint styles: ${parts.join(", ")}.`);
3401
- }
3402
- }
3403
- const effectResult = out.effectStyles;
3404
- if (effectResult) {
3405
- const c = effectResult.created ?? 0;
3406
- const u = effectResult.updated ?? 0;
3407
- const r = effectResult.removed ?? 0;
3408
- if (c + u + r > 0) {
3409
- const parts = [];
3410
- if (c > 0) parts.push(`${c} created`);
3411
- if (u > 0) parts.push(`${u} updated`);
3412
- if (r > 0) parts.push(`${r} removed`);
3413
- summaryParts.push(`Effect styles (shadows): ${parts.join(", ")}.`);
3414
- if (effectResult.removedNames?.length) {
3415
- summaryParts.push(`Removed: ${effectResult.removedNames.join(", ")}.`);
3416
- }
3417
- }
3418
- }
3419
- const numResult = out.numberVariables;
3420
- if (Array.isArray(numResult)) {
3421
- const total = numResult.reduce((acc, r) => acc + (r.result?.variableNames?.length ?? 0), 0);
3422
- const totalRemoved = numResult.reduce((acc, r) => acc + (r.result?.removed ?? 0), 0);
3423
- if (total > 0 || totalRemoved > 0) {
3424
- const parts = [];
3425
- if (total > 0) parts.push(`${total} synced`);
3426
- if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
3427
- summaryParts.push(`Number variables: ${parts.join(", ")}.`);
3428
- }
3429
- }
3430
- const textStylesWithRemoved = out.textStyles;
3431
- if (textStylesWithRemoved && (textStylesWithRemoved.created ?? 0) + (textStylesWithRemoved.updated ?? 0) + (textStylesWithRemoved.removed ?? 0) > 0) {
3432
- const c = textStylesWithRemoved.created ?? 0;
3433
- const u = textStylesWithRemoved.updated ?? 0;
3434
- const r = textStylesWithRemoved.removed ?? 0;
3435
- const parts = [];
3436
- if (c > 0) parts.push(`${c} created`);
3437
- if (u > 0) parts.push(`${u} updated`);
3438
- if (r > 0) parts.push(`${r} removed`);
3439
- summaryParts.push(`Text styles: ${parts.join(", ")}.`);
3440
- }
3441
- if (summaryParts.length > 0 && !out.error) {
3442
- out.summary = [out.summary, summaryParts.join(" ")].filter(Boolean).join(" ");
3443
- }
3444
- 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.";
3445
- const responseText = out.summary ? `${out.summary}
3446
-
3447
- ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3448
- return {
3449
- content: [{
3450
- type: "text",
3451
- text: responseText
3452
- }],
3453
- ...out.error ? { isError: true } : {}
3454
- };
3455
- }
3456
2422
  default:
3457
2423
  return {
3458
2424
  content: [{
3459
2425
  type: "text",
3460
2426
  text: JSON.stringify({
3461
2427
  error: `Unknown tool: ${name}`,
3462
- availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma"]
2428
+ availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion"]
3463
2429
  }, null, 2)
3464
2430
  }]
3465
2431
  };
@@ -3501,6 +2467,26 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3501
2467
  };
3502
2468
  }
3503
2469
  });
2470
+ function getSyncDependencySkills(dsVersion, dsExportedAt) {
2471
+ const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
2472
+ return [
2473
+ {
2474
+ path: ".cursor/skills/atomix-ds/SKILL.md",
2475
+ content: genericWithVersion,
2476
+ shortName: "SKILL.md"
2477
+ }
2478
+ ];
2479
+ }
2480
+ function readSkillVersionFromFile(filePath) {
2481
+ if (!fs.existsSync(filePath)) return null;
2482
+ try {
2483
+ const raw = fs.readFileSync(filePath, "utf-8");
2484
+ const match = raw.match(/atomixDsVersion:\s*["']([^"']+)["']/);
2485
+ return match ? match[1] : null;
2486
+ } catch {
2487
+ return null;
2488
+ }
2489
+ }
3504
2490
  function injectSkillVersion(content, version, exportedAt) {
3505
2491
  const endOfFrontmatter = content.indexOf("\n---\n", 3);
3506
2492
  if (endOfFrontmatter === -1) return content;
@@ -3593,7 +2579,7 @@ When a semantic token doesn't exist for your use case, use the closest primitive
3593
2579
 
3594
2580
  ### 4. Syncing tokens to a file
3595
2581
 
3596
- \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
2582
+ \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skill (.cursor/skills/atomix-ds/SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
3597
2583
 
3598
2584
  ## Workflow
3599
2585
 
@@ -3620,7 +2606,7 @@ If no token matches, call \`searchTokens\` to find the closest option. Never inv
3620
2606
  - **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
3621
2607
  - **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
3622
2608
  - **Icons:** Size via \`getToken("sizing.icon.sm")\`; stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it.
3623
- - **Typography:** Use typography tokens for all text. For global typeset output, call **listTypesets** and emit every entry; include text-transform and text-decoration for 1:1 match.
2609
+ - **Typography:** Use **typeset classes** (e.g. from \`typesets.css\` or \`listTypesets()\` output \u2014 \`typeset-heading-h1\`, \`typeset-body-md-regular\`, etc.) for all text; do not re-declare font-family, font-size, font-weight, line-height, or letter-spacing in component CSS, which duplicates the design system. For global typeset output, call **listTypesets** and emit every entry; include text-transform and text-decoration for 1:1 match.
3624
2610
  - **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
3625
2611
  - **Platform agnostic:** Token values work across CSS, Tailwind, React Native, SwiftUI, Compose, Flutter, and any style system. Use the output format appropriate to your platform.
3626
2612
  - **Version check:** If this file has frontmatter \`atomixDsVersion\`, compare to the version from **getDependencies** (\`meta.designSystemVersion\`). If the DS is newer, suggest running **syncAll** to update.
@@ -3852,7 +2838,7 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
3852
2838
  const topicRaw = rulesMatch[1]?.toLowerCase().trim();
3853
2839
  const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3854
2840
  const headers = { "Content-Type": "application/json" };
3855
- if (apiKey) headers["x-api-key"] = apiKey;
2841
+ if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
3856
2842
  const response = await fetch(rulesUrl, { headers });
3857
2843
  if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
3858
2844
  const payload = await response.json();
@@ -3909,8 +2895,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
3909
2895
  { name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
3910
2896
  { name: "--rules", description: "Get design system governance rules (optionally by topic: colors, typo, motion, icons, layout, visual)." },
3911
2897
  { name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
3912
- { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
3913
- { name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
2898
+ { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." }
3914
2899
  ];
3915
2900
  return { prompts };
3916
2901
  });
@@ -3928,7 +2913,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3928
2913
  }]
3929
2914
  };
3930
2915
  }
3931
- const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name;
2916
+ const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name;
3932
2917
  const shouldForceRefresh = [
3933
2918
  "hello",
3934
2919
  "atomix-setup",
@@ -3936,8 +2921,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3936
2921
  "design-system-rules",
3937
2922
  // --rules
3938
2923
  "sync",
3939
- "refactor",
3940
- "sync-to-figma"
2924
+ "refactor"
3941
2925
  ].includes(canonicalName);
3942
2926
  let data = null;
3943
2927
  let stats = null;
@@ -4084,7 +3068,7 @@ ${welcome}`;
4084
3068
  const topic = args?.topic?.toLowerCase().trim();
4085
3069
  const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
4086
3070
  const headers = { "Content-Type": "application/json" };
4087
- if (apiKey) headers["x-api-key"] = apiKey;
3071
+ if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
4088
3072
  const response = await fetch(rulesUrl, { headers });
4089
3073
  if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
4090
3074
  const payload = await response.json();
@@ -4283,20 +3267,6 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4283
3267
  ]
4284
3268
  });
4285
3269
  }
4286
- case "sync-to-figma": {
4287
- return withMcpNotice({
4288
- description: "Push design system to Figma via MCP tool",
4289
- messages: [
4290
- {
4291
- role: "user",
4292
- content: {
4293
- type: "text",
4294
- 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.`
4295
- }
4296
- }
4297
- ]
4298
- });
4299
- }
4300
3270
  case "refactor": {
4301
3271
  const refactorOutput = args?.output || "./tokens.css";
4302
3272
  const refactorFormat = args?.format || "css";
@@ -4444,7 +3414,7 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4444
3414
  - Resolve platform/stack: infer from the project (e.g. package.json, build.gradle, Xcode) or ask once: "Which platform? (e.g. web, Android, iOS)" and if relevant "Which stack? (e.g. React, Vue, Next, Swift, Kotlin)." Do not assume a default.
4445
3415
  - Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
4446
3416
  - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
4447
- - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
3417
+ - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4448
3418
  - Do not write, create, or add anything in Phase 1.
4449
3419
 
4450
3420
  ## Phase 2 \u2013 Report and ask
@@ -4457,8 +3427,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4457
3427
 
4458
3428
  - Run only when the user has said yes (all or specific items).
4459
3429
  - For each approved item:
4460
- - **Skill:** Prefer calling **syncAll** (it writes the skill into the repo). If writing the skill manually, write getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md) under the project root.
4461
- - **Token file and skills in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
3430
+ - **Skill:** Prefer calling **syncAll** (it writes the skill into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` under the project root.
3431
+ - **Token file and skill in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
4462
3432
  - **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.
4463
3433
  - **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.
4464
3434
  - **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, DS_NAME, HEADING_FONT_VAR, FONT_FAMILY_VAR, LARGEST_DISPLAY_TYPESET_CLASS, LARGEST_BODY_TYPESET_CLASS, BODY_TYPESET_CLASS, FONT_LINK_TAG, BRAND_PRIMARY_VAR, BUTTON_PADDING_VAR, BUTTON_HEIGHT_VAR, BUTTON_RADIUS_VAR, CIRCLE_PADDING_VAR, ICON_SIZE_VAR, CHECK_ICON_SVG (inline SVG from the design system icon library). The page uses semantic colors (mode-aware) and a Dark/Light toggle. 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).
@@ -4543,7 +3513,6 @@ ${tokenSummary}
4543
3513
  | **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
4544
3514
  | **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
4545
3515
  | **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
4546
- | **/--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. |
4547
3516
  | **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
4548
3517
 
4549
3518
  **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.
@@ -4568,7 +3537,6 @@ async function startServer() {
4568
3537
  console.error("");
4569
3538
  process.exit(1);
4570
3539
  }
4571
- startFigmaBridge();
4572
3540
  const transport = new StdioServerTransport();
4573
3541
  await server.connect(transport);
4574
3542
  console.error(`Atomix MCP Server started for design system: ${dsId}`);
@@ -4578,7 +3546,6 @@ async function startServer() {
4578
3546
  );
4579
3547
  }
4580
3548
  function onShutdown() {
4581
- closeFigmaBridge();
4582
3549
  }
4583
3550
  process.on("SIGINT", onShutdown);
4584
3551
  process.on("SIGTERM", onShutdown);