@atomixstudio/mcp 1.0.34 → 1.0.35

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,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ FIGMA_DESIGN_CATALOG,
4
+ FIGMA_DESIGN_SKILL_MD,
5
+ buildFigmaPayloadsFromDS,
6
+ buildResolvers,
7
+ formatCatalogForMCP,
8
+ getDesignMethodNames,
9
+ getQueryMethodNames,
3
10
  isAllowedMethod,
4
- normalizeBridgeMethod
5
- } from "./chunk-CE6J5MJX.js";
11
+ normalizeBridgeMethod,
12
+ resolveStepParams
13
+ } from "./chunk-426RNS3G.js";
6
14
 
7
15
  // src/index.ts
8
16
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -222,7 +230,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
222
230
  return changes;
223
231
  }
224
232
  async function fetchDesignSystem(options) {
225
- const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomixstudio.eu", etag, forceRefresh = false } = options;
233
+ const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomix.studio", etag, forceRefresh = false } = options;
226
234
  if (!dsId2) {
227
235
  throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
228
236
  }
@@ -1093,9 +1101,7 @@ function formatSyncResponse(options) {
1093
1101
  `;
1094
1102
  });
1095
1103
  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.
1104
+ These changes are reflected in the design system governance; run /--sync to update the skill and call getRules for the latest rules.
1099
1105
  `;
1100
1106
  }
1101
1107
  if (diff) {
@@ -1218,709 +1224,6 @@ function getTokenStats(data) {
1218
1224
  };
1219
1225
  }
1220
1226
 
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
1227
  // src/index.ts
1925
1228
  import * as path from "path";
1926
1229
  import * as fs from "fs";
@@ -2124,7 +1427,7 @@ function parseArgs() {
2124
1427
  var cliArgs = parseArgs();
2125
1428
  var { dsId, apiKey, accessToken } = cliArgs;
2126
1429
  var apiBase = cliArgs.apiBase || "https://atomix.studio";
2127
- var MCP_VERSION = "1.0.33";
1430
+ var MCP_VERSION = "1.0.34";
2128
1431
  var cachedData = null;
2129
1432
  var cachedETag = null;
2130
1433
  var cachedMcpTier = null;
@@ -2230,7 +1533,7 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
2230
1533
  return result.data;
2231
1534
  }
2232
1535
  var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
2233
- function typesetKeyToFontFamilyRole2(key) {
1536
+ function typesetKeyToFontFamilyRole(key) {
2234
1537
  const prefix = (key.split("-")[0] ?? key).toLowerCase();
2235
1538
  if (prefix === "display" || prefix.startsWith("display")) return "display";
2236
1539
  if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
@@ -2250,7 +1553,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
2250
1553
  const p = cssPrefix ? `${cssPrefix}-` : "";
2251
1554
  const typesets = [];
2252
1555
  for (const key of Object.keys(fontSize)) {
2253
- const role = typesetKeyToFontFamilyRole2(key);
1556
+ const role = typesetKeyToFontFamilyRole(key);
2254
1557
  const familyName = fontFamily[role] ?? fontFamily.body;
2255
1558
  const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
2256
1559
  const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
@@ -2439,7 +1742,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2439
1742
  },
2440
1743
  {
2441
1744
  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.",
1745
+ description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-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
1746
  inputSchema: {
2444
1747
  type: "object",
2445
1748
  properties: {
@@ -2506,6 +1809,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2506
1809
  }
2507
1810
  }
2508
1811
  ];
1812
+ if (cachedMcpTier === "pro") {
1813
+ toolsList.push({
1814
+ name: "designInFigma",
1815
+ description: "Design UI in the connected Figma file using the design system tokens. Call with action:'catalog' to discover available bridge methods, their parameters, and the file's variables/styles. Call with action:'query' to read from Figma: get_selection (current selection), get_node_info (nodeId), get_design_screenshot (frameId; returns PNG image for review). Call with action:'execute' and an array of steps to create the design on canvas. Requires the Atomix Figma plugin to be connected.",
1816
+ inputSchema: {
1817
+ type: "object",
1818
+ properties: {
1819
+ action: {
1820
+ type: "string",
1821
+ enum: ["catalog", "query", "execute"],
1822
+ description: "catalog = discover methods + file context; query = read selection/node/screenshot; execute = run design steps"
1823
+ },
1824
+ queryMethod: {
1825
+ type: "string",
1826
+ description: "Required when action is 'query'. One of: get_selection, get_node_info, get_document_info, get_design_screenshot, get_figma_variables_and_styles, list_local_components, get_component_catalog, get_variable_collection_modes, get_frame_variable_mode."
1827
+ },
1828
+ queryParams: {
1829
+ type: "object",
1830
+ description: "Optional params for query. get_node_info needs { nodeId }. get_design_screenshot needs { frameId } and optional { scale }."
1831
+ },
1832
+ steps: {
1833
+ type: "array",
1834
+ description: "Required when action is 'execute'. Array of { method, params } design commands.",
1835
+ items: {
1836
+ type: "object",
1837
+ properties: {
1838
+ method: { type: "string" },
1839
+ params: { type: "object" }
1840
+ },
1841
+ required: ["method"]
1842
+ }
1843
+ }
1844
+ },
1845
+ required: ["action"]
1846
+ }
1847
+ });
1848
+ }
2509
1849
  return { tools: toolsList };
2510
1850
  });
2511
1851
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -2648,7 +1988,7 @@ Version: ${designSystemData.meta.version}`,
2648
1988
  ` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
2649
1989
  changeLine,
2650
1990
  "",
2651
- "Would write skills: .cursor/skills/atomix-ds/SKILL.md",
1991
+ "Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md",
2652
1992
  "",
2653
1993
  "Call syncAll again with dryRun: false to apply."
2654
1994
  ].filter(Boolean).join("\n");
@@ -2681,7 +2021,8 @@ Version: ${designSystemData.meta.version}`,
2681
2021
  hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
2682
2022
  deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
2683
2023
  });
2684
- return { responseText: response, rulesResults, validation };
2024
+ const tokenSyncChangeSummary = fileExists && ["css", "scss", "less"].includes(format) ? { fileExisted: true, previousVarCount: existingTokens.size, currentVarCount: tokenCount } : { fileExisted: false, previousVarCount: void 0, currentVarCount: tokenCount };
2025
+ return { responseText: response, rulesResults, validation, tokenSyncChangeSummary };
2685
2026
  }
2686
2027
  switch (name) {
2687
2028
  case "getToken": {
@@ -3113,10 +2454,12 @@ Version: ${designSystemData.meta.version}`,
3113
2454
  const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
3114
2455
  let tokenResponseText = "";
3115
2456
  const allValidation = [];
2457
+ let tokenSyncChangeSummary;
3116
2458
  if (!skipTokens) {
3117
2459
  const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
3118
2460
  tokenResponseText = result.responseText;
3119
2461
  allValidation.push(...result.validation);
2462
+ tokenSyncChangeSummary = result.tokenSyncChangeSummary;
3120
2463
  if (dryRun) {
3121
2464
  parts.push(`Would write tokens: ${output} (${format})`);
3122
2465
  } else {
@@ -3126,10 +2469,11 @@ Version: ${designSystemData.meta.version}`,
3126
2469
  const dsVersion = String(data.meta.version ?? "1.0.0");
3127
2470
  const dsExportedAt = data.meta.exportedAt;
3128
2471
  const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
3129
- const skillPath1 = path.join(skillsDir, "SKILL.md");
3130
2472
  const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
2473
+ const dependencySkills = getSyncDependencySkills(data, dsVersion, dsExportedAt);
3131
2474
  if (dryRun) {
3132
- parts.push("Would write skills: .cursor/skills/atomix-ds/SKILL.md");
2475
+ const skillList = dependencySkills.map((s) => s.path).join(", ");
2476
+ parts.push(`Would write skills: ${skillList}`);
3133
2477
  parts.push("Would write manifest: atomix-dependencies.json");
3134
2478
  const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
3135
2479
  return {
@@ -3139,10 +2483,18 @@ ${reportText}` }]
3139
2483
  };
3140
2484
  }
3141
2485
  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 + ")");
2486
+ const skillsWritten = [];
2487
+ for (const sk of dependencySkills) {
2488
+ const absPath = path.resolve(projectRoot, sk.path);
2489
+ const existed = fs.existsSync(absPath);
2490
+ const prevVersion = existed ? readSkillVersionFromFile(absPath) : null;
2491
+ const fromLabel = existed ? prevVersion ? `DS v${prevVersion}` : "existing" : "missing";
2492
+ fs.writeFileSync(absPath, sk.content);
2493
+ allValidation.push({ path: absPath, status: fs.existsSync(absPath) ? "OK" : "FAIL", detail: "Written." });
2494
+ skillsWritten.push({ path: sk.path, shortName: sk.shortName, from: fromLabel });
2495
+ }
2496
+ const skillShortNames = skillsWritten.map((s) => s.shortName).join(" + ");
2497
+ parts.push(`Skills: ${skillShortNames} (DS v${dsVersion})`);
3146
2498
  const tokens = data.tokens;
3147
2499
  const typography = tokens?.typography;
3148
2500
  const fontFamily = typography?.fontFamily;
@@ -3161,6 +2513,10 @@ ${reportText}` }]
3161
2513
  };
3162
2514
  const lib = icons?.library || "lucide";
3163
2515
  const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
2516
+ const skillsManifestEntry = { skill: ".cursor/skills/atomix-ds/SKILL.md", syncedAtVersion: String(data.meta.version ?? "1.0.0") };
2517
+ if (dependencySkills.some((s) => s.shortName === "figma-design-SKILL.md")) {
2518
+ skillsManifestEntry.figmaDesignSkill = ".cursor/skills/atomix-ds/figma-design-SKILL.md";
2519
+ }
3164
2520
  const manifest = {
3165
2521
  designSystem: { name: data.meta.name, version: data.meta.version },
3166
2522
  tokenFile: skipTokens ? void 0 : output,
@@ -3171,23 +2527,67 @@ ${reportText}` }]
3171
2527
  strokeWidthValue: icons?.strokeWidth
3172
2528
  },
3173
2529
  fonts: { families: fontNames },
3174
- skills: {
3175
- skill: ".cursor/skills/atomix-ds/SKILL.md",
3176
- syncedAtVersion: data.meta.version ?? "1.0.0"
3177
- }
2530
+ skills: skillsManifestEntry
3178
2531
  };
2532
+ const manifestExisted = fs.existsSync(manifestPath);
3179
2533
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3180
2534
  allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
3181
2535
  parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
2536
+ const changeRows = [];
2537
+ if (!skipTokens && tokenSyncChangeSummary) {
2538
+ const fromStr = tokenSyncChangeSummary.fileExisted && tokenSyncChangeSummary.previousVarCount != null ? `${tokenSyncChangeSummary.previousVarCount} tokens in ${output}` : "missing";
2539
+ changeRows.push({
2540
+ what: "Tokens",
2541
+ from: fromStr,
2542
+ to: `${tokenSyncChangeSummary.currentVarCount} tokens in ${output} (${format})`
2543
+ });
2544
+ }
2545
+ const skillsFrom = skillsWritten.length ? skillsWritten.every((s) => s.from === "missing") ? "missing" : "updated" : "";
2546
+ if (skillsWritten.length > 0) {
2547
+ changeRows.push({
2548
+ what: "Skills",
2549
+ from: skillsFrom,
2550
+ to: `${skillShortNames} (DS v${dsVersion})`
2551
+ });
2552
+ }
2553
+ changeRows.push({
2554
+ what: "Manifest",
2555
+ from: manifestExisted ? "existing" : "missing",
2556
+ to: "atomix-dependencies.json"
2557
+ });
2558
+ if (lastSyncAffectedTokens && (lastSyncAffectedTokens.modified.length > 0 || lastSyncAffectedTokens.added.length > 0 || lastSyncAffectedTokens.removed.length > 0)) {
2559
+ const mod = lastSyncAffectedTokens.modified;
2560
+ const add = lastSyncAffectedTokens.added;
2561
+ const rem = lastSyncAffectedTokens.removed;
2562
+ const tokenNames = [
2563
+ ...mod.map((m) => m.token),
2564
+ ...add,
2565
+ ...rem.map((r) => r.token)
2566
+ ];
2567
+ const fromParts = [];
2568
+ if (mod.length) fromParts.push(`${mod.length} modified`);
2569
+ if (add.length) fromParts.push(`${add.length} added`);
2570
+ if (rem.length) fromParts.push(`${rem.length} removed`);
2571
+ const toDetail = tokenNames.length <= 8 ? tokenNames.join(", ") : `${tokenNames.length} tokens (${tokenNames.slice(0, 4).join(", ")}...)`;
2572
+ changeRows.push({
2573
+ what: "Token changes (this run)",
2574
+ from: fromParts.join(", ") || "\u2014",
2575
+ to: toDetail
2576
+ });
2577
+ if (mod.length > 0 && mod.length <= 15) {
2578
+ mod.forEach((m) => {
2579
+ changeRows.push({ what: ` ${m.token}`, from: m.oldValue, to: m.newValue });
2580
+ });
2581
+ }
2582
+ }
2583
+ 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
2584
  const summary = parts.join("\n");
3183
2585
  const validationBlock = formatValidationBlock(allValidation);
3184
2586
  const hasFailure = allValidation.some((e) => e.status === "FAIL");
3185
2587
  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}
3187
-
3188
- ---
2588
+ const fullText = resultLine + changesList + "\n\n" + (tokenResponseText ? `---
3189
2589
 
3190
- ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
2590
+ ${tokenResponseText}` : "") + validationBlock;
3191
2591
  return {
3192
2592
  content: [{ type: "text", text: fullText }]
3193
2593
  };
@@ -3225,10 +2625,17 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3225
2625
  families: fontNames,
3226
2626
  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
2627
  },
3228
- skill: {
3229
- path: ".cursor/skills/atomix-ds/SKILL.md",
3230
- content: GENERIC_SKILL_MD
3231
- },
2628
+ skill: (() => {
2629
+ const list = getSyncDependencySkills(data, String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
2630
+ const generic = list.find((s) => s.shortName === "SKILL.md");
2631
+ return generic ? { path: generic.path, content: GENERIC_SKILL_MD } : { path: ".cursor/skills/atomix-ds/SKILL.md", content: GENERIC_SKILL_MD };
2632
+ })(),
2633
+ ...getEffectiveTier() === "pro" ? {
2634
+ skillFigmaDesign: {
2635
+ path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
2636
+ content: FIGMA_DESIGN_SKILL_MD
2637
+ }
2638
+ } : {},
3232
2639
  tokenFiles: {
3233
2640
  files: ["tokens.css", "tokens.json"],
3234
2641
  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."
@@ -3453,13 +2860,282 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3453
2860
  ...out.error ? { isError: true } : {}
3454
2861
  };
3455
2862
  }
2863
+ case "designInFigma": {
2864
+ if (cachedMcpTier !== "pro") {
2865
+ return {
2866
+ content: [{ type: "text", text: JSON.stringify({ error: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio" }, null, 2) }],
2867
+ isError: true
2868
+ };
2869
+ }
2870
+ const action = args?.action;
2871
+ if (action === "catalog") {
2872
+ try {
2873
+ const reachable = await isBridgeReachable();
2874
+ let fileCtx;
2875
+ if (reachable) {
2876
+ try {
2877
+ const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
2878
+ fileCtx = raw;
2879
+ } catch {
2880
+ }
2881
+ }
2882
+ const localVars = [];
2883
+ const libVars = [];
2884
+ const textStyleNames = [];
2885
+ const effectStyleNames = [];
2886
+ if (fileCtx) {
2887
+ for (const coll of fileCtx.variableCollections ?? []) {
2888
+ for (const v of coll.variables) localVars.push(v.name);
2889
+ }
2890
+ for (const coll of fileCtx.variableCollectionsLibrary ?? []) {
2891
+ for (const v of coll.variables) libVars.push(v.name);
2892
+ }
2893
+ for (const s of fileCtx.textStyles ?? []) textStyleNames.push(s.name);
2894
+ for (const s of fileCtx.effectStyles ?? []) effectStyleNames.push(s.name);
2895
+ }
2896
+ const catalogPayload = formatCatalogForMCP(FIGMA_DESIGN_CATALOG, {
2897
+ localVariables: localVars,
2898
+ libraryVariables: libVars,
2899
+ textStyles: textStyleNames,
2900
+ effectStyles: effectStyleNames
2901
+ });
2902
+ let designAssets = void 0;
2903
+ if (reachable) {
2904
+ try {
2905
+ const raw = await sendBridgeRequest("get_component_catalog", {});
2906
+ if (raw && typeof raw === "object") {
2907
+ designAssets = raw;
2908
+ }
2909
+ } catch {
2910
+ }
2911
+ }
2912
+ return {
2913
+ content: [{
2914
+ type: "text",
2915
+ text: JSON.stringify({
2916
+ ...catalogPayload,
2917
+ pluginConnected: reachable,
2918
+ ...designAssets ? { designAssets } : {},
2919
+ ...reachable ? {
2920
+ queryMethods: getQueryMethodNames(),
2921
+ queryHint: "Use action:'query' with queryMethod (e.g. get_selection, get_node_info, get_design_screenshot) and optional queryParams to read selection, node details, or a frame screenshot for review."
2922
+ } : {},
2923
+ ...reachable ? {} : { hint: "Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect." }
2924
+ }, null, 2)
2925
+ }]
2926
+ };
2927
+ } catch (e) {
2928
+ const msg = e instanceof Error ? e.message : String(e);
2929
+ return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
2930
+ }
2931
+ }
2932
+ if (action === "query") {
2933
+ const queryMethodRaw = args?.queryMethod;
2934
+ const queryParams = args?.queryParams ?? {};
2935
+ if (!queryMethodRaw || typeof queryMethodRaw !== "string") {
2936
+ return {
2937
+ content: [{ type: "text", text: JSON.stringify({ error: "query requires queryMethod (e.g. get_selection, get_node_info, get_design_screenshot)" }, null, 2) }],
2938
+ isError: true
2939
+ };
2940
+ }
2941
+ const queryMethod = queryMethodRaw.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
2942
+ const allowedQueryMethods = new Set(getQueryMethodNames());
2943
+ if (!allowedQueryMethods.has(queryMethod)) {
2944
+ return {
2945
+ content: [{ type: "text", text: JSON.stringify({ error: `Unknown query method "${queryMethod}". Allowed: ${[...allowedQueryMethods].join(", ")}` }, null, 2) }],
2946
+ isError: true
2947
+ };
2948
+ }
2949
+ try {
2950
+ const reachable = await isBridgeReachable();
2951
+ if (!reachable) {
2952
+ return {
2953
+ content: [{
2954
+ type: "text",
2955
+ text: JSON.stringify({
2956
+ error: "Figma plugin not connected.",
2957
+ bridgeNotRunning: true,
2958
+ agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma with action:'query' again."
2959
+ }, null, 2)
2960
+ }],
2961
+ isError: true
2962
+ };
2963
+ }
2964
+ const result = await sendBridgeRequest(queryMethod, queryParams);
2965
+ const content = [];
2966
+ const summary = { method: queryMethod, success: true };
2967
+ if (result && typeof result === "object" && "imageBase64" in result && typeof result.imageBase64 === "string") {
2968
+ summary.screenshot = "included below (base64 PNG)";
2969
+ summary.format = result.format ?? "PNG";
2970
+ summary.scale = result.scale;
2971
+ content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2972
+ content.push({
2973
+ type: "image",
2974
+ data: result.imageBase64,
2975
+ mimeType: "image/png"
2976
+ });
2977
+ } else {
2978
+ summary.result = result;
2979
+ content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2980
+ }
2981
+ return { content };
2982
+ } catch (e) {
2983
+ const msg = e instanceof Error ? e.message : String(e);
2984
+ return { content: [{ type: "text", text: JSON.stringify({ method: queryMethod, success: false, error: msg }, null, 2) }], isError: true };
2985
+ }
2986
+ }
2987
+ if (action === "execute") {
2988
+ const rawSteps = args?.steps;
2989
+ if (!rawSteps || !Array.isArray(rawSteps) || rawSteps.length === 0) {
2990
+ return {
2991
+ content: [{ type: "text", text: JSON.stringify({ error: "steps array is required for action:'execute'" }, null, 2) }],
2992
+ isError: true
2993
+ };
2994
+ }
2995
+ try {
2996
+ const reachable = await isBridgeReachable();
2997
+ if (!reachable) {
2998
+ return {
2999
+ content: [{
3000
+ type: "text",
3001
+ text: JSON.stringify({
3002
+ error: "Figma plugin not connected.",
3003
+ bridgeNotRunning: true,
3004
+ agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma again."
3005
+ }, null, 2)
3006
+ }],
3007
+ isError: true
3008
+ };
3009
+ }
3010
+ const designMethods = /* @__PURE__ */ new Set([
3011
+ ...getDesignMethodNames(),
3012
+ "list_local_components",
3013
+ "get_component_catalog",
3014
+ "get_design_screenshot",
3015
+ "get_variable_collection_modes",
3016
+ "get_frame_variable_mode"
3017
+ ]);
3018
+ const warnings = [];
3019
+ let fileCtx;
3020
+ try {
3021
+ const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
3022
+ fileCtx = raw;
3023
+ } catch {
3024
+ }
3025
+ const resolvers = fileCtx ? buildResolvers(fileCtx) : null;
3026
+ const isFigmaNodeId = (s) => /^\d+:\d+$/.test(s);
3027
+ let rootFrameId = null;
3028
+ let lastCreatedFrameNodeId = null;
3029
+ const namedNodeIds = /* @__PURE__ */ new Map();
3030
+ const results = [];
3031
+ for (let i = 0; i < rawSteps.length; i++) {
3032
+ const step = rawSteps[i];
3033
+ const method = (step.method ?? "").replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
3034
+ if (!designMethods.has(method)) {
3035
+ warnings.push({ step: i, method, issue: `Unknown method "${method}" \u2014 skipped` });
3036
+ continue;
3037
+ }
3038
+ let params = { ...step.params ?? {} };
3039
+ if (resolvers) {
3040
+ params = resolveStepParams(params, resolvers);
3041
+ }
3042
+ if (rootFrameId) {
3043
+ const parentId = params.parentId;
3044
+ const frameId = params.frameId;
3045
+ if (typeof parentId === "string" && !isFigmaNodeId(parentId)) {
3046
+ params.parentId = namedNodeIds.get(parentId) ?? rootFrameId;
3047
+ }
3048
+ if (typeof frameId === "string" && !isFigmaNodeId(frameId)) {
3049
+ params.frameId = namedNodeIds.get(frameId) ?? rootFrameId;
3050
+ }
3051
+ if (method === "finalize_design_frame" && !params.frameId) {
3052
+ params.frameId = rootFrameId;
3053
+ }
3054
+ const needsParent = method.startsWith("design_create_") || method === "design_create_component";
3055
+ if (needsParent && !params.parentId) {
3056
+ params.parentId = rootFrameId;
3057
+ }
3058
+ }
3059
+ const nodeId = params.nodeId;
3060
+ if (typeof nodeId === "string" && nodeId && !isFigmaNodeId(nodeId)) {
3061
+ const resolved = namedNodeIds.get(nodeId) ?? lastCreatedFrameNodeId ?? rootFrameId;
3062
+ params.nodeId = resolved ?? nodeId;
3063
+ }
3064
+ const needsNode = [
3065
+ "design_set_auto_layout",
3066
+ "design_set_layout_sizing",
3067
+ "design_set_effects",
3068
+ "design_set_strokes",
3069
+ "design_resize_node",
3070
+ "design_set_resize_constraints",
3071
+ "design_set_layout_constraints",
3072
+ "design_set_node_position",
3073
+ "design_set_text_properties"
3074
+ ].includes(method);
3075
+ if (needsNode && !params.nodeId) {
3076
+ params.nodeId = lastCreatedFrameNodeId ?? rootFrameId;
3077
+ }
3078
+ const childId = params.childId;
3079
+ if (typeof childId === "string" && childId && !isFigmaNodeId(childId)) {
3080
+ params.childId = namedNodeIds.get(childId) ?? childId;
3081
+ }
3082
+ try {
3083
+ const response = await sendBridgeRequest(method, params);
3084
+ const res = response;
3085
+ if (method === "create_design_placeholder" && typeof res?.frameId === "string") {
3086
+ rootFrameId = res.frameId;
3087
+ }
3088
+ const isCreateMethod = method.startsWith("design_create_") || method === "design_convert_to_component" || method === "design_combine_as_variants" || method === "design_create_frame_from_preset" || method === "design_group_nodes";
3089
+ if (isCreateMethod && typeof res?.nodeId === "string") {
3090
+ const createdNodeId = res.nodeId;
3091
+ if (method === "design_create_frame" || method === "design_create_frame_from_preset") {
3092
+ lastCreatedFrameNodeId = createdNodeId;
3093
+ if (!rootFrameId && res?.isRoot === true) {
3094
+ rootFrameId = createdNodeId;
3095
+ }
3096
+ }
3097
+ const stepName = step.params?.name ?? "";
3098
+ if (stepName) namedNodeIds.set(stepName, createdNodeId);
3099
+ }
3100
+ results.push({ step: i, method, result: response });
3101
+ } catch (e) {
3102
+ const errMsg = e instanceof Error ? e.message : String(e);
3103
+ results.push({ step: i, method, error: errMsg });
3104
+ if (!rootFrameId && (method === "design_create_frame" || method === "create_design_placeholder")) break;
3105
+ }
3106
+ }
3107
+ const errorCount = results.filter((r) => r.error).length;
3108
+ const success = errorCount === 0;
3109
+ return {
3110
+ content: [{
3111
+ type: "text",
3112
+ text: JSON.stringify({
3113
+ success,
3114
+ stepsExecuted: results.length,
3115
+ errors: errorCount,
3116
+ warnings,
3117
+ results
3118
+ }, null, 2)
3119
+ }],
3120
+ ...errorCount > 0 ? { isError: true } : {}
3121
+ };
3122
+ } catch (e) {
3123
+ const msg = e instanceof Error ? e.message : String(e);
3124
+ return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
3125
+ }
3126
+ }
3127
+ return {
3128
+ content: [{ type: "text", text: JSON.stringify({ error: "action must be 'catalog', 'query', or 'execute'" }, null, 2) }],
3129
+ isError: true
3130
+ };
3131
+ }
3456
3132
  default:
3457
3133
  return {
3458
3134
  content: [{
3459
3135
  type: "text",
3460
3136
  text: JSON.stringify({
3461
3137
  error: `Unknown tool: ${name}`,
3462
- availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma"]
3138
+ availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma", "designInFigma"]
3463
3139
  }, null, 2)
3464
3140
  }]
3465
3141
  };
@@ -3501,6 +3177,43 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3501
3177
  };
3502
3178
  }
3503
3179
  });
3180
+ function getEffectiveTier() {
3181
+ return cachedMcpTier;
3182
+ }
3183
+ function getSyncDependencySkills(data, dsVersion, dsExportedAt) {
3184
+ const tier = getEffectiveTier();
3185
+ const skills = [];
3186
+ const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
3187
+ skills.push({
3188
+ path: ".cursor/skills/atomix-ds/SKILL.md",
3189
+ content: genericWithVersion,
3190
+ shortName: "SKILL.md"
3191
+ });
3192
+ if (tier === "pro") {
3193
+ const figmaSkillWithFrontmatter = `---
3194
+ name: atomix-figma-design
3195
+ description: Figma design skill \u2014 create high-quality, token-bound designs in Figma from any reference (screenshots, images, code snippets, URLs). Covers mobile, web, token application, and component/variant creation. Use with syncToFigma and designInFigma MCP tools when the Atomix plugin is connected.
3196
+ ---
3197
+ ${FIGMA_DESIGN_SKILL_MD}`;
3198
+ const figmaSkillWithVersion = injectSkillVersion(figmaSkillWithFrontmatter, dsVersion, dsExportedAt);
3199
+ skills.push({
3200
+ path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
3201
+ content: figmaSkillWithVersion,
3202
+ shortName: "figma-design-SKILL.md"
3203
+ });
3204
+ }
3205
+ return skills;
3206
+ }
3207
+ function readSkillVersionFromFile(filePath) {
3208
+ if (!fs.existsSync(filePath)) return null;
3209
+ try {
3210
+ const raw = fs.readFileSync(filePath, "utf-8");
3211
+ const match = raw.match(/atomixDsVersion:\s*["']([^"']+)["']/);
3212
+ return match ? match[1] : null;
3213
+ } catch {
3214
+ return null;
3215
+ }
3216
+ }
3504
3217
  function injectSkillVersion(content, version, exportedAt) {
3505
3218
  const endOfFrontmatter = content.indexOf("\n---\n", 3);
3506
3219
  if (endOfFrontmatter === -1) return content;
@@ -3593,7 +3306,7 @@ When a semantic token doesn't exist for your use case, use the closest primitive
3593
3306
 
3594
3307
  ### 4. Syncing tokens to a file
3595
3308
 
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.
3309
+ \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
3597
3310
 
3598
3311
  ## Workflow
3599
3312
 
@@ -3620,7 +3333,7 @@ If no token matches, call \`searchTokens\` to find the closest option. Never inv
3620
3333
  - **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
3621
3334
  - **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
3622
3335
  - **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.
3336
+ - **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
3337
  - **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
3625
3338
  - **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
3339
  - **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.
@@ -3912,6 +3625,9 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
3912
3625
  { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
3913
3626
  { name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
3914
3627
  ];
3628
+ if (cachedMcpTier === "pro") {
3629
+ prompts.push({ name: "--design-in-figma", description: "Load Figma design skill and use designInFigma tool to design UI in the connected Figma file. Pro only." });
3630
+ }
3915
3631
  return { prompts };
3916
3632
  });
3917
3633
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
@@ -3928,7 +3644,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3928
3644
  }]
3929
3645
  };
3930
3646
  }
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;
3647
+ 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 === "--design-in-figma" || name === "designInFigma" ? "design-in-figma" : name;
3932
3648
  const shouldForceRefresh = [
3933
3649
  "hello",
3934
3650
  "atomix-setup",
@@ -3937,7 +3653,8 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3937
3653
  // --rules
3938
3654
  "sync",
3939
3655
  "refactor",
3940
- "sync-to-figma"
3656
+ "sync-to-figma",
3657
+ "design-in-figma"
3941
3658
  ].includes(canonicalName);
3942
3659
  let data = null;
3943
3660
  let stats = null;
@@ -4297,6 +4014,80 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4297
4014
  ]
4298
4015
  });
4299
4016
  }
4017
+ case "design-in-figma": {
4018
+ if (getEffectiveTier() !== "pro") {
4019
+ return withMcpNotice({
4020
+ description: "Design in Figma (Pro required)",
4021
+ messages: [
4022
+ {
4023
+ role: "user",
4024
+ content: {
4025
+ type: "text",
4026
+ text: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio to use Design in Figma."
4027
+ }
4028
+ }
4029
+ ]
4030
+ });
4031
+ }
4032
+ if (!data) {
4033
+ return withMcpNotice({
4034
+ description: "Design in Figma",
4035
+ messages: [
4036
+ {
4037
+ role: "user",
4038
+ content: {
4039
+ type: "text",
4040
+ text: "Failed to fetch design system. Check your --ds-id and --atomix-token configuration, then try /--design-in-figma again."
4041
+ }
4042
+ }
4043
+ ]
4044
+ });
4045
+ }
4046
+ const figmaSkillPath = path.resolve(process.cwd(), ".cursor/skills/atomix-ds/figma-design-SKILL.md");
4047
+ const skillExists = fs.existsSync(figmaSkillPath);
4048
+ const currentVersion = String(data.meta.version ?? "1.0.0");
4049
+ const fileVersion = skillExists ? readSkillVersionFromFile(figmaSkillPath) : null;
4050
+ const isOutdated = !skillExists || fileVersion === null || fileVersion !== currentVersion;
4051
+ if (isOutdated) {
4052
+ return withMcpNotice({
4053
+ description: "Figma design skill missing or outdated",
4054
+ messages: [
4055
+ {
4056
+ role: "user",
4057
+ content: {
4058
+ type: "text",
4059
+ text: skillExists ? `The Figma design skill at \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` is outdated (design system version ${currentVersion}). Run **/--sync** to update the skill and other design system files, then run **/--design-in-figma** again.` : `The Figma design skill is missing. Run **/--sync** to write \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` (and other design system files), then run **/--design-in-figma** again.`
4060
+ }
4061
+ }
4062
+ ]
4063
+ });
4064
+ }
4065
+ return withMcpNotice({
4066
+ description: "Load Figma design skill and use designInFigma tool",
4067
+ messages: [
4068
+ {
4069
+ role: "user",
4070
+ content: {
4071
+ type: "text",
4072
+ text: `You are using **/--design-in-figma**: load the Figma design skill below and use the **designInFigma** MCP tool to design UI in the connected Figma file.
4073
+
4074
+ **Instructions:**
4075
+ 1. Treat the following as the Figma design skill (advisory and generative UI on the Figma canvas). Follow it when creating or editing designs.
4076
+ 2. Call **designInFigma** with \`action: "catalog"\` first to discover available bridge methods, file variables/styles, and query/execute capabilities.
4077
+ 3. Use \`action: "query"\` to read from Figma (e.g. get_selection, get_node_info, get_design_screenshot).
4078
+ 4. Use \`action: "execute"\` with an array of steps to create or modify the design on the canvas.
4079
+ 5. If the response includes \`bridgeNotRunning\` or \`pluginConnected: false\`, tell the user to run the Atomix plugin in Figma and tap Connect, then try again.
4080
+
4081
+ ---
4082
+
4083
+ ## Figma design skill
4084
+
4085
+ ${FIGMA_DESIGN_SKILL_MD}`
4086
+ }
4087
+ }
4088
+ ]
4089
+ });
4090
+ }
4300
4091
  case "refactor": {
4301
4092
  const refactorOutput = args?.output || "./tokens.css";
4302
4093
  const refactorFormat = args?.format || "css";
@@ -4443,8 +4234,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4443
4234
 
4444
4235
  - 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
4236
  - Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
4446
- - 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.
4237
+ - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-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).
4238
+ - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skills (SKILL.md and figma-design-SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4448
4239
  - Do not write, create, or add anything in Phase 1.
4449
4240
 
4450
4241
  ## Phase 2 \u2013 Report and ask
@@ -4457,8 +4248,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4457
4248
 
4458
4249
  - Run only when the user has said yes (all or specific items).
4459
4250
  - 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.
4251
+ - **Skills:** Prefer calling **syncAll** (it writes both skills into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` and \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` under the project root.
4252
+ - **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, .cursor/skills/atomix-ds/figma-design-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
4253
  - **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
4254
  - **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
4255
  - **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).
@@ -4544,6 +4335,7 @@ ${tokenSummary}
4544
4335
  | **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
4545
4336
  | **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
4546
4337
  | **/--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. |
4338
+ | **/--design-in-figma** | Design in Figma: loads Figma design skill and use designInFigma tool (catalog, query, execute). Pro only. |
4547
4339
  | **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
4548
4340
 
4549
4341
  **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.