@atomixstudio/mcp 1.0.16 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -14
- package/dist/chunk-FFAGTYRZ.js +44 -0
- package/dist/chunk-FFAGTYRZ.js.map +1 -0
- package/dist/figma-bridge-protocol.d.ts +27 -0
- package/dist/figma-bridge-protocol.js +14 -0
- package/dist/figma-bridge-protocol.js.map +1 -0
- package/dist/index.js +1702 -400
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isAllowedMethod,
|
|
4
|
+
normalizeBridgeMethod
|
|
5
|
+
} from "./chunk-FFAGTYRZ.js";
|
|
2
6
|
|
|
3
7
|
// src/index.ts
|
|
4
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -12,13 +16,12 @@ import {
|
|
|
12
16
|
GetPromptRequestSchema
|
|
13
17
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
18
|
|
|
15
|
-
//
|
|
19
|
+
// ../atomix-sync-core/dist/index.js
|
|
16
20
|
import * as fs from "fs";
|
|
17
21
|
import * as path from "path";
|
|
18
22
|
import * as path3 from "path";
|
|
19
23
|
function generateETag(meta) {
|
|
20
|
-
const
|
|
21
|
-
const hash = `${meta.version}-${ts}`;
|
|
24
|
+
const hash = `${meta.version}-${meta.updatedAt}`;
|
|
22
25
|
return `"v${hash}"`;
|
|
23
26
|
}
|
|
24
27
|
function compareDesignSystems(cached, fresh) {
|
|
@@ -221,7 +224,7 @@ function detectGovernanceChangesByFoundation(cached, fresh) {
|
|
|
221
224
|
return changes;
|
|
222
225
|
}
|
|
223
226
|
async function fetchDesignSystem(options) {
|
|
224
|
-
const { dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2 = "https://atomixstudio.eu", etag, forceRefresh = false } = options;
|
|
227
|
+
const { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 = "https://atomixstudio.eu", etag, forceRefresh = false } = options;
|
|
225
228
|
if (!dsId2) {
|
|
226
229
|
throw new Error("Missing dsId. Usage: fetchDesignSystem({ dsId: '...' })");
|
|
227
230
|
}
|
|
@@ -229,7 +232,9 @@ async function fetchDesignSystem(options) {
|
|
|
229
232
|
const headers = {
|
|
230
233
|
"Content-Type": "application/json"
|
|
231
234
|
};
|
|
232
|
-
if (
|
|
235
|
+
if (accessToken2) {
|
|
236
|
+
headers["Authorization"] = `Bearer ${accessToken2}`;
|
|
237
|
+
} else if (apiKey2) {
|
|
233
238
|
headers["x-api-key"] = apiKey2;
|
|
234
239
|
}
|
|
235
240
|
if (etag) {
|
|
@@ -249,7 +254,7 @@ async function fetchDesignSystem(options) {
|
|
|
249
254
|
}
|
|
250
255
|
const data = await response.json();
|
|
251
256
|
const responseETag = response.headers.get("etag");
|
|
252
|
-
const finalETag = responseETag || generateETag(data.meta);
|
|
257
|
+
const finalETag = responseETag || generateETag({ version: data.meta.version, updatedAt: data.meta.exportedAt });
|
|
253
258
|
const designSystemData = {
|
|
254
259
|
tokens: data.tokens,
|
|
255
260
|
cssVariables: data.cssVariables,
|
|
@@ -1286,10 +1291,456 @@ function getTokenStats(data) {
|
|
|
1286
1291
|
// src/index.ts
|
|
1287
1292
|
import * as path2 from "path";
|
|
1288
1293
|
import * as fs2 from "fs";
|
|
1294
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
1295
|
+
var FIGMA_BRIDGE_PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 8765;
|
|
1296
|
+
var FIGMA_BRIDGE_HOST = process.env.FIGMA_BRIDGE_HOST || "127.0.0.1";
|
|
1297
|
+
var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
|
|
1298
|
+
var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
|
|
1299
|
+
var FIGMA_CONNECTION_INSTRUCTIONS = {
|
|
1300
|
+
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.",
|
|
1301
|
+
connect: 'In the plugin UI, tap **Connect to Cursor** and wait until the status shows "Connected".',
|
|
1302
|
+
startBridge: "The Figma bridge runs with this MCP server. Ensure Cursor has started this MCP server (e.g. in Cursor settings), then in Figma run the Atomix plugin and click Connect to Cursor."
|
|
1303
|
+
};
|
|
1304
|
+
var bridgeWss = null;
|
|
1305
|
+
var pluginWs = null;
|
|
1306
|
+
var pendingBridgeRequests = /* @__PURE__ */ new Map();
|
|
1307
|
+
function startFigmaBridge() {
|
|
1308
|
+
if (bridgeWss) return;
|
|
1309
|
+
try {
|
|
1310
|
+
bridgeWss = new WebSocketServer({
|
|
1311
|
+
host: FIGMA_BRIDGE_HOST,
|
|
1312
|
+
port: FIGMA_BRIDGE_PORT,
|
|
1313
|
+
clientTracking: true
|
|
1314
|
+
});
|
|
1315
|
+
bridgeWss.on("connection", (ws, req) => {
|
|
1316
|
+
const url = req.url || "";
|
|
1317
|
+
const params = new URLSearchParams(url.startsWith("/") ? url.slice(1) : url);
|
|
1318
|
+
const token = params.get("token");
|
|
1319
|
+
const role = params.get("role");
|
|
1320
|
+
if (FIGMA_BRIDGE_TOKEN && token !== FIGMA_BRIDGE_TOKEN) {
|
|
1321
|
+
ws.close(4003, "Invalid or missing bridge token");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (role !== "plugin") {
|
|
1325
|
+
ws.close(4002, "Only role=plugin is accepted (bridge runs in MCP server)");
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (pluginWs) {
|
|
1329
|
+
try {
|
|
1330
|
+
pluginWs.close();
|
|
1331
|
+
} catch (_) {
|
|
1332
|
+
}
|
|
1333
|
+
pluginWs = null;
|
|
1334
|
+
}
|
|
1335
|
+
pluginWs = ws;
|
|
1336
|
+
ws.on("message", (raw) => {
|
|
1337
|
+
const text = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
1338
|
+
let msg;
|
|
1339
|
+
try {
|
|
1340
|
+
msg = JSON.parse(text);
|
|
1341
|
+
} catch {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const parsed = msg;
|
|
1345
|
+
if (parsed?.type === "ping" && typeof parsed.id === "string") {
|
|
1346
|
+
try {
|
|
1347
|
+
ws.send(JSON.stringify({ type: "pong", id: parsed.id }));
|
|
1348
|
+
} catch (_) {
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (typeof parsed.id === "string" && ("result" in parsed || "error" in parsed)) {
|
|
1353
|
+
const pending = pendingBridgeRequests.get(parsed.id);
|
|
1354
|
+
if (pending) {
|
|
1355
|
+
clearTimeout(pending.timeout);
|
|
1356
|
+
pendingBridgeRequests.delete(parsed.id);
|
|
1357
|
+
if (parsed.error) pending.reject(new Error(parsed.error));
|
|
1358
|
+
else pending.resolve(parsed.result);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
ws.on("close", () => {
|
|
1363
|
+
if (pluginWs === ws) pluginWs = null;
|
|
1364
|
+
});
|
|
1365
|
+
ws.on("error", () => {
|
|
1366
|
+
if (pluginWs === ws) pluginWs = null;
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
bridgeWss.on("listening", () => {
|
|
1370
|
+
console.error(`[atomix-mcp] Figma bridge listening on ws://${FIGMA_BRIDGE_HOST}:${FIGMA_BRIDGE_PORT} (local only)`);
|
|
1371
|
+
if (FIGMA_BRIDGE_TOKEN) {
|
|
1372
|
+
console.error("[atomix-mcp] Figma bridge token required (FIGMA_BRIDGE_TOKEN)");
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
bridgeWss.on("error", (err) => {
|
|
1376
|
+
console.error("[atomix-mcp] Figma bridge server error:", err);
|
|
1377
|
+
});
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
console.error("[atomix-mcp] Failed to start Figma bridge:", err);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function closeFigmaBridge() {
|
|
1383
|
+
if (pluginWs) {
|
|
1384
|
+
try {
|
|
1385
|
+
pluginWs.close();
|
|
1386
|
+
} catch (_) {
|
|
1387
|
+
}
|
|
1388
|
+
pluginWs = null;
|
|
1389
|
+
}
|
|
1390
|
+
if (bridgeWss) {
|
|
1391
|
+
try {
|
|
1392
|
+
bridgeWss.close();
|
|
1393
|
+
} catch (_) {
|
|
1394
|
+
}
|
|
1395
|
+
bridgeWss = null;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function isBridgeReachable() {
|
|
1399
|
+
return Promise.resolve(!!(pluginWs && pluginWs.readyState === WebSocket.OPEN));
|
|
1400
|
+
}
|
|
1401
|
+
function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS) {
|
|
1402
|
+
const normalized = normalizeBridgeMethod(method);
|
|
1403
|
+
if (!isAllowedMethod(normalized)) {
|
|
1404
|
+
return Promise.reject(new Error(`Bridge method not allowed: ${method}`));
|
|
1405
|
+
}
|
|
1406
|
+
const ws = pluginWs;
|
|
1407
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1408
|
+
return Promise.reject(
|
|
1409
|
+
new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and click Connect to Cursor.")
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1413
|
+
return new Promise((resolve4, reject) => {
|
|
1414
|
+
const timeout = setTimeout(() => {
|
|
1415
|
+
if (pendingBridgeRequests.delete(id)) {
|
|
1416
|
+
reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
|
|
1417
|
+
}
|
|
1418
|
+
}, timeoutMs);
|
|
1419
|
+
pendingBridgeRequests.set(id, { resolve: resolve4, reject, timeout });
|
|
1420
|
+
try {
|
|
1421
|
+
ws.send(JSON.stringify({ id, method: normalized, params }));
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
pendingBridgeRequests.delete(id);
|
|
1424
|
+
clearTimeout(timeout);
|
|
1425
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
function figmaColorNameWithGroup(key) {
|
|
1430
|
+
if (key.includes("/")) {
|
|
1431
|
+
const [group2, ...rest] = key.split("/");
|
|
1432
|
+
const name2 = rest.join("/").trim();
|
|
1433
|
+
if (!name2) return key;
|
|
1434
|
+
const groupDisplay2 = group2.charAt(0).toUpperCase() + group2.slice(1).toLowerCase();
|
|
1435
|
+
return `${groupDisplay2} / ${name2}`;
|
|
1436
|
+
}
|
|
1437
|
+
const firstDash = key.indexOf("-");
|
|
1438
|
+
if (firstDash <= 0) return key;
|
|
1439
|
+
const group = key.slice(0, firstDash);
|
|
1440
|
+
const name = key.slice(firstDash + 1);
|
|
1441
|
+
const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
|
|
1442
|
+
return `${groupDisplay} / ${name}`;
|
|
1443
|
+
}
|
|
1444
|
+
function tokenValueToNumber(s) {
|
|
1445
|
+
if (typeof s !== "string" || !s.trim()) return 0;
|
|
1446
|
+
const t = s.trim();
|
|
1447
|
+
if (t.endsWith("rem")) {
|
|
1448
|
+
const n2 = parseFloat(t.replace(/rem$/, ""));
|
|
1449
|
+
return Number.isFinite(n2) ? Math.round(n2 * 16) : 0;
|
|
1450
|
+
}
|
|
1451
|
+
if (t.endsWith("px")) {
|
|
1452
|
+
const n2 = parseFloat(t.replace(/px$/, ""));
|
|
1453
|
+
return Number.isFinite(n2) ? Math.round(n2) : 0;
|
|
1454
|
+
}
|
|
1455
|
+
const n = parseFloat(t);
|
|
1456
|
+
return Number.isFinite(n) ? n : 0;
|
|
1457
|
+
}
|
|
1458
|
+
function parseBoxShadowToFigmaEffect(shadowStr) {
|
|
1459
|
+
const s = shadowStr.trim();
|
|
1460
|
+
if (!s || s.toLowerCase() === "none") return null;
|
|
1461
|
+
const parsePx = (x) => typeof x === "string" ? parseFloat(x.replace(/px$/i, "")) : NaN;
|
|
1462
|
+
const colorMatch = s.match(/(rgba?\s*\([^)]+\)|#[0-9A-Fa-f]{3,8})\s*$/i);
|
|
1463
|
+
const colorStr = colorMatch ? colorMatch[1].trim() : void 0;
|
|
1464
|
+
const rest = (colorMatch ? s.slice(0, colorMatch.index) : s).trim();
|
|
1465
|
+
const parts = rest ? rest.split(/\s+/) : [];
|
|
1466
|
+
if (parts.length < 3) return null;
|
|
1467
|
+
const offsetX = parsePx(parts[0]);
|
|
1468
|
+
const offsetY = parsePx(parts[1]);
|
|
1469
|
+
const blur = parsePx(parts[2]);
|
|
1470
|
+
let spread = 0;
|
|
1471
|
+
if (parts.length >= 4) spread = parsePx(parts[3]);
|
|
1472
|
+
let r = 0, g = 0, b = 0, a = 0.1;
|
|
1473
|
+
if (colorStr) {
|
|
1474
|
+
const rgbaMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
1475
|
+
if (rgbaMatch) {
|
|
1476
|
+
r = Number(rgbaMatch[1]) / 255;
|
|
1477
|
+
g = Number(rgbaMatch[2]) / 255;
|
|
1478
|
+
b = Number(rgbaMatch[3]) / 255;
|
|
1479
|
+
a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1;
|
|
1480
|
+
} else {
|
|
1481
|
+
const hexMatch = colorStr.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/);
|
|
1482
|
+
if (hexMatch) {
|
|
1483
|
+
r = parseInt(hexMatch[1].slice(0, 2), 16) / 255;
|
|
1484
|
+
g = parseInt(hexMatch[1].slice(2, 4), 16) / 255;
|
|
1485
|
+
b = parseInt(hexMatch[1].slice(4, 6), 16) / 255;
|
|
1486
|
+
a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 0.1;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) return null;
|
|
1491
|
+
return {
|
|
1492
|
+
type: "DROP_SHADOW",
|
|
1493
|
+
offset: { x: offsetX, y: offsetY },
|
|
1494
|
+
radius: Math.max(0, blur),
|
|
1495
|
+
spread: Number.isFinite(spread) ? spread : 0,
|
|
1496
|
+
color: { r, g, b, a },
|
|
1497
|
+
visible: true,
|
|
1498
|
+
blendMode: "NORMAL"
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function parseBoxShadowToFigmaEffects(shadowStr) {
|
|
1502
|
+
const s = (shadowStr || "").trim();
|
|
1503
|
+
if (!s || s.toLowerCase() === "none") return [];
|
|
1504
|
+
const out = [];
|
|
1505
|
+
const segments = s.split(/\s*,\s*/);
|
|
1506
|
+
for (const seg of segments) {
|
|
1507
|
+
const effect = parseBoxShadowToFigmaEffect(seg.trim());
|
|
1508
|
+
if (effect) out.push(effect);
|
|
1509
|
+
}
|
|
1510
|
+
return out;
|
|
1511
|
+
}
|
|
1512
|
+
function buildFigmaPayloadsFromDS(data) {
|
|
1513
|
+
const tokens = data.tokens;
|
|
1514
|
+
const colors = tokens?.colors;
|
|
1515
|
+
const typography = tokens?.typography;
|
|
1516
|
+
const modes = [];
|
|
1517
|
+
const variables = [];
|
|
1518
|
+
const paintStyles = [];
|
|
1519
|
+
const hexRe = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
|
|
1520
|
+
const addedNames = /* @__PURE__ */ new Set();
|
|
1521
|
+
if (colors?.modes) {
|
|
1522
|
+
const light = colors.modes.light ?? {};
|
|
1523
|
+
const dark = colors.modes.dark ?? {};
|
|
1524
|
+
if (Object.keys(light).length > 0) modes.push("Light");
|
|
1525
|
+
if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
|
|
1526
|
+
if (modes.length === 0) modes.push("Light");
|
|
1527
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(dark)]);
|
|
1528
|
+
for (const key of allKeys) {
|
|
1529
|
+
const lightHex = light[key];
|
|
1530
|
+
const darkHex = dark[key];
|
|
1531
|
+
if (typeof lightHex === "string" && hexRe.test(lightHex)) {
|
|
1532
|
+
const figmaName = figmaColorNameWithGroup(key);
|
|
1533
|
+
if (addedNames.has(figmaName)) continue;
|
|
1534
|
+
addedNames.add(figmaName);
|
|
1535
|
+
const values = {};
|
|
1536
|
+
if (modes.includes("Light")) values.Light = lightHex;
|
|
1537
|
+
if (modes.includes("Dark")) values.Dark = typeof darkHex === "string" && hexRe.test(darkHex) ? darkHex : lightHex;
|
|
1538
|
+
variables.push({ name: figmaName, values });
|
|
1539
|
+
paintStyles.push({ name: figmaName, color: lightHex });
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (colors?.static?.brand && typeof colors.static.brand === "object") {
|
|
1544
|
+
for (const [key, hex] of Object.entries(colors.static.brand)) {
|
|
1545
|
+
if (typeof hex !== "string" || !hexRe.test(hex)) continue;
|
|
1546
|
+
const figmaName = figmaColorNameWithGroup(`brand/${key}`);
|
|
1547
|
+
if (addedNames.has(figmaName)) continue;
|
|
1548
|
+
addedNames.add(figmaName);
|
|
1549
|
+
paintStyles.push({ name: figmaName, color: hex });
|
|
1550
|
+
if (modes.length === 0) modes.push("Light");
|
|
1551
|
+
const values = {};
|
|
1552
|
+
for (const m of modes) values[m] = hex;
|
|
1553
|
+
variables.push({ name: figmaName, values });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (variables.length === 0 && modes.length === 0) modes.push("Light");
|
|
1557
|
+
const collectionName = data.meta?.name ? `${data.meta.name} Colors` : "Atomix Colors";
|
|
1558
|
+
const textStyles = [];
|
|
1559
|
+
const sizeToPx = (val, basePx = 16) => {
|
|
1560
|
+
if (typeof val === "number") return Math.round(val);
|
|
1561
|
+
const s = String(val).trim();
|
|
1562
|
+
const pxMatch = s.match(/^([\d.]+)\s*px$/i);
|
|
1563
|
+
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
1564
|
+
const remMatch = s.match(/^([\d.]+)\s*rem$/i);
|
|
1565
|
+
if (remMatch) return Math.round(parseFloat(remMatch[1]) * basePx);
|
|
1566
|
+
const n = parseFloat(s);
|
|
1567
|
+
if (Number.isFinite(n)) return n <= 0 ? basePx : n < 50 ? Math.round(n * basePx) : Math.round(n);
|
|
1568
|
+
return basePx;
|
|
1569
|
+
};
|
|
1570
|
+
const letterSpacingToPx = (val, fontSizePx) => {
|
|
1571
|
+
if (val === void 0 || val === null) return void 0;
|
|
1572
|
+
if (typeof val === "number") return Math.round(val);
|
|
1573
|
+
const s = String(val).trim();
|
|
1574
|
+
const pxMatch = s.match(/^([-\d.]+)\s*px$/i);
|
|
1575
|
+
if (pxMatch) return Math.round(parseFloat(pxMatch[1]));
|
|
1576
|
+
const emMatch = s.match(/^([-\d.]+)\s*em$/i);
|
|
1577
|
+
if (emMatch) return Math.round(parseFloat(emMatch[1]) * fontSizePx);
|
|
1578
|
+
const n = parseFloat(s);
|
|
1579
|
+
return Number.isFinite(n) ? Math.round(n) : void 0;
|
|
1580
|
+
};
|
|
1581
|
+
const firstFont = (obj) => {
|
|
1582
|
+
if (typeof obj === "string") {
|
|
1583
|
+
const match = obj.match(/['"]?([^'",\s]+)['"]?/);
|
|
1584
|
+
return match ? match[1] : "Inter";
|
|
1585
|
+
}
|
|
1586
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
1587
|
+
const v = obj.body ?? obj.heading ?? obj.display ?? Object.values(obj)[0];
|
|
1588
|
+
return firstFont(v);
|
|
1589
|
+
}
|
|
1590
|
+
return "Inter";
|
|
1591
|
+
};
|
|
1592
|
+
const fontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
|
|
1593
|
+
const fontSizeMap = typography?.fontSize;
|
|
1594
|
+
const fontWeightMap = typography?.fontWeight;
|
|
1595
|
+
const lineHeightMap = typography?.lineHeight;
|
|
1596
|
+
const letterSpacingMap = typography?.letterSpacing;
|
|
1597
|
+
const textTransformMap = typography?.textTransform;
|
|
1598
|
+
const textDecorationMap = typography?.textDecoration;
|
|
1599
|
+
if (fontSizeMap && typeof fontSizeMap === "object" && Object.keys(fontSizeMap).length > 0) {
|
|
1600
|
+
for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
|
|
1601
|
+
const fontSize = sizeToPx(sizeVal);
|
|
1602
|
+
if (fontSize <= 0) continue;
|
|
1603
|
+
const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
|
|
1604
|
+
const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
|
|
1605
|
+
const fontWeight = weight != null ? String(weight) : "400";
|
|
1606
|
+
const letterSpacingPx = letterSpacingToPx(
|
|
1607
|
+
letterSpacingMap && typeof letterSpacingMap === "object" ? letterSpacingMap[key] : void 0,
|
|
1608
|
+
fontSize
|
|
1609
|
+
);
|
|
1610
|
+
const textTransform = textTransformMap && typeof textTransformMap === "object" ? textTransformMap[key] : void 0;
|
|
1611
|
+
const textDecoration = textDecorationMap && typeof textDecorationMap === "object" ? textDecorationMap[key] : void 0;
|
|
1612
|
+
const namePart = key.replace(/-/g, " / ");
|
|
1613
|
+
const style = {
|
|
1614
|
+
name: namePart.startsWith("Typography") ? namePart : `Typography / ${namePart}`,
|
|
1615
|
+
fontFamily,
|
|
1616
|
+
fontWeight,
|
|
1617
|
+
fontSize,
|
|
1618
|
+
lineHeightUnit: "PERCENT",
|
|
1619
|
+
letterSpacingUnit: "PIXELS",
|
|
1620
|
+
...letterSpacingPx !== void 0 && letterSpacingPx !== 0 ? { letterSpacingValue: letterSpacingPx } : {}
|
|
1621
|
+
};
|
|
1622
|
+
if (lh != null && typeof lh === "number" && lh > 0) {
|
|
1623
|
+
style.lineHeightValue = lh >= 10 ? Math.round(lh / fontSize * 100) : Math.round(lh * 100);
|
|
1624
|
+
} else {
|
|
1625
|
+
style.lineHeightValue = 150;
|
|
1626
|
+
}
|
|
1627
|
+
if (textTransform === "uppercase") style.textCase = "UPPER";
|
|
1628
|
+
else if (textTransform === "lowercase") style.textCase = "LOWER";
|
|
1629
|
+
else if (textTransform === "capitalize") style.textCase = "TITLE";
|
|
1630
|
+
else style.textCase = "ORIGINAL";
|
|
1631
|
+
if (textDecoration === "underline") style.textDecoration = "UNDERLINE";
|
|
1632
|
+
else style.textDecoration = "NONE";
|
|
1633
|
+
textStyles.push(style);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const textStylesMap = typography?.textStyles;
|
|
1637
|
+
if (textStyles.length === 0 && textStylesMap && typeof textStylesMap === "object") {
|
|
1638
|
+
for (const [styleName, style] of Object.entries(textStylesMap)) {
|
|
1639
|
+
if (!style || typeof style !== "object") continue;
|
|
1640
|
+
const fontSize = sizeToPx(style.fontSize ?? "1rem");
|
|
1641
|
+
const lhStr = style.lineHeight;
|
|
1642
|
+
const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
|
|
1643
|
+
const payload = {
|
|
1644
|
+
name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
|
|
1645
|
+
fontFamily,
|
|
1646
|
+
fontWeight: String(style.fontWeight ?? "400"),
|
|
1647
|
+
fontSize,
|
|
1648
|
+
lineHeightUnit: "PERCENT",
|
|
1649
|
+
lineHeightValue: Math.round((Number.isFinite(lineHeightUnitless) ? lineHeightUnitless : 1.5) * 100),
|
|
1650
|
+
letterSpacingUnit: "PIXELS",
|
|
1651
|
+
textCase: "ORIGINAL",
|
|
1652
|
+
textDecoration: "NONE"
|
|
1653
|
+
};
|
|
1654
|
+
textStyles.push(payload);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const numberVariables = [];
|
|
1658
|
+
const dsName = data.meta?.name ?? "Atomix";
|
|
1659
|
+
const primitivesCollectionName = `${dsName} Primitives`;
|
|
1660
|
+
const spacing = tokens?.spacing;
|
|
1661
|
+
if (spacing?.scale && typeof spacing.scale === "object") {
|
|
1662
|
+
for (const [key, val] of Object.entries(spacing.scale)) {
|
|
1663
|
+
const n = tokenValueToNumber(val);
|
|
1664
|
+
if (n >= 0) numberVariables.push({ name: `Spacing / ${key}`, value: n });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const radius = tokens?.radius;
|
|
1668
|
+
if (radius?.scale && typeof radius.scale === "object") {
|
|
1669
|
+
for (const [key, val] of Object.entries(radius.scale)) {
|
|
1670
|
+
const n = tokenValueToNumber(val);
|
|
1671
|
+
if (n >= 0) numberVariables.push({ name: `Radius / ${key}`, value: n });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const borders = tokens?.borders;
|
|
1675
|
+
if (borders?.width && typeof borders.width === "object") {
|
|
1676
|
+
for (const [key, val] of Object.entries(borders.width)) {
|
|
1677
|
+
const n = tokenValueToNumber(val);
|
|
1678
|
+
if (n >= 0) numberVariables.push({ name: `Borders / ${key}`, value: n });
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const sizing = tokens?.sizing;
|
|
1682
|
+
if (sizing?.height && typeof sizing.height === "object") {
|
|
1683
|
+
for (const [key, val] of Object.entries(sizing.height)) {
|
|
1684
|
+
const n = tokenValueToNumber(val);
|
|
1685
|
+
if (n >= 0) numberVariables.push({ name: `Height / ${key}`, value: n });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (sizing?.icon && typeof sizing.icon === "object") {
|
|
1689
|
+
for (const [key, val] of Object.entries(sizing.icon)) {
|
|
1690
|
+
const n = tokenValueToNumber(val);
|
|
1691
|
+
if (n >= 0) numberVariables.push({ name: `Icon / ${key}`, value: n });
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const layout = tokens?.layout;
|
|
1695
|
+
if (layout?.breakpoint && typeof layout.breakpoint === "object") {
|
|
1696
|
+
for (const [key, val] of Object.entries(layout.breakpoint)) {
|
|
1697
|
+
const n = tokenValueToNumber(val);
|
|
1698
|
+
if (n >= 0) numberVariables.push({ name: `Breakpoint / ${key}`, value: n });
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const effectStyles = [];
|
|
1702
|
+
const shadows = tokens?.shadows;
|
|
1703
|
+
if (shadows?.elevation && typeof shadows.elevation === "object") {
|
|
1704
|
+
for (const [key, val] of Object.entries(shadows.elevation)) {
|
|
1705
|
+
if (typeof val !== "string") continue;
|
|
1706
|
+
const effects = parseBoxShadowToFigmaEffects(val);
|
|
1707
|
+
if (effects.length > 0) {
|
|
1708
|
+
effectStyles.push({
|
|
1709
|
+
name: `Shadow / ${key}`,
|
|
1710
|
+
effects
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (shadows?.focus && typeof shadows.focus === "string") {
|
|
1716
|
+
const effects = parseBoxShadowToFigmaEffects(shadows.focus);
|
|
1717
|
+
if (effects.length > 0) {
|
|
1718
|
+
effectStyles.push({ name: "Shadow / focus", effects });
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return {
|
|
1722
|
+
colorVariables: { collectionName, modes, variables },
|
|
1723
|
+
paintStyles,
|
|
1724
|
+
textStyles,
|
|
1725
|
+
numberVariables: { collectionName: primitivesCollectionName, variables: numberVariables },
|
|
1726
|
+
effectStyles
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function getExpectedFigmaNamesFromDS(data) {
|
|
1730
|
+
const payloads = buildFigmaPayloadsFromDS(data);
|
|
1731
|
+
return {
|
|
1732
|
+
colorVariableNames: payloads.colorVariables.variables.map((v) => v.name),
|
|
1733
|
+
paintStyleNames: payloads.paintStyles.map((s) => s.name),
|
|
1734
|
+
textStyleNames: payloads.textStyles.map((s) => s.name),
|
|
1735
|
+
effectStyleNames: payloads.effectStyles.map((s) => s.name),
|
|
1736
|
+
numberVariableNames: payloads.numberVariables.variables.map((v) => v.name)
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1289
1739
|
function parseArgs() {
|
|
1290
1740
|
const args = process.argv.slice(2);
|
|
1291
1741
|
let dsId2 = null;
|
|
1292
1742
|
let apiKey2 = null;
|
|
1743
|
+
let accessToken2 = null;
|
|
1293
1744
|
let apiBase2 = null;
|
|
1294
1745
|
for (let i = 0; i < args.length; i++) {
|
|
1295
1746
|
if (args[i] === "--ds-id" && args[i + 1]) {
|
|
@@ -1298,19 +1749,42 @@ function parseArgs() {
|
|
|
1298
1749
|
} else if (args[i] === "--api-key" && args[i + 1]) {
|
|
1299
1750
|
apiKey2 = args[i + 1];
|
|
1300
1751
|
i++;
|
|
1752
|
+
} else if (args[i] === "--atomix-token" && args[i + 1]) {
|
|
1753
|
+
accessToken2 = args[i + 1];
|
|
1754
|
+
i++;
|
|
1301
1755
|
} else if (args[i] === "--api-base" && args[i + 1]) {
|
|
1302
1756
|
apiBase2 = args[i + 1];
|
|
1303
1757
|
i++;
|
|
1304
1758
|
}
|
|
1305
1759
|
}
|
|
1306
|
-
return { dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2 };
|
|
1760
|
+
return { dsId: dsId2, apiKey: apiKey2, accessToken: accessToken2, apiBase: apiBase2 };
|
|
1307
1761
|
}
|
|
1308
1762
|
var cliArgs = parseArgs();
|
|
1309
|
-
var { dsId, apiKey } = cliArgs;
|
|
1763
|
+
var { dsId, apiKey, accessToken } = cliArgs;
|
|
1310
1764
|
var apiBase = cliArgs.apiBase || "https://atomixstudio.eu";
|
|
1311
1765
|
var cachedData = null;
|
|
1312
1766
|
var cachedETag = null;
|
|
1767
|
+
var cachedMcpTier = null;
|
|
1768
|
+
var authFailedNoTools = false;
|
|
1769
|
+
function hasValidAuthConfig() {
|
|
1770
|
+
return !!(dsId && accessToken);
|
|
1771
|
+
}
|
|
1772
|
+
var AUTH_REQUIRED_MESSAGE = "Atomix MCP requires authentication. Add both --ds-id and --atomix-token to your MCP config (Settings \u2192 MCP), then restart Cursor. Get your token from Atomix Studio: Export modal or Settings \u2192 Regenerate Atomix access token.";
|
|
1313
1773
|
var lastChangeSummary = null;
|
|
1774
|
+
var FIGMA_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1775
|
+
"syncToFigma",
|
|
1776
|
+
"getFigmaVariablesAndStyles",
|
|
1777
|
+
"createDesignPlaceholder",
|
|
1778
|
+
"resolveFigmaIdsForTokens",
|
|
1779
|
+
"designCreateFrame",
|
|
1780
|
+
"designCreateText",
|
|
1781
|
+
"designCreateRectangle",
|
|
1782
|
+
"designSetAutoLayout",
|
|
1783
|
+
"designSetLayoutConstraints",
|
|
1784
|
+
"designAppendChild",
|
|
1785
|
+
"getDesignScreenshot",
|
|
1786
|
+
"finalizeDesignFrame"
|
|
1787
|
+
]);
|
|
1314
1788
|
var lastSyncAffectedTokens = null;
|
|
1315
1789
|
function getLastChangeSummary() {
|
|
1316
1790
|
return lastChangeSummary;
|
|
@@ -1326,10 +1800,11 @@ ${changes.summary}`);
|
|
|
1326
1800
|
}
|
|
1327
1801
|
}
|
|
1328
1802
|
async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
1329
|
-
if (!dsId) throw new Error("Missing --ds-id. Usage: npx @atomixstudio/mcp --ds-id <id>");
|
|
1803
|
+
if (!dsId) throw new Error("Missing --ds-id. Usage: npx @atomixstudio/mcp --ds-id <id> --atomix-token <token>");
|
|
1804
|
+
if (!accessToken) throw new Error("Missing --atomix-token. Get your token from the Export modal or Settings.");
|
|
1330
1805
|
const result = await fetchDesignSystem({
|
|
1331
1806
|
dsId,
|
|
1332
|
-
|
|
1807
|
+
accessToken,
|
|
1333
1808
|
apiBase: apiBase ?? void 0,
|
|
1334
1809
|
etag: forceRefresh ? void 0 : cachedETag ?? void 0,
|
|
1335
1810
|
forceRefresh
|
|
@@ -1338,6 +1813,7 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
|
1338
1813
|
if (result.status === 304 || !result.data) throw new Error("No design system data (304 or null)");
|
|
1339
1814
|
cachedData = result.data;
|
|
1340
1815
|
cachedETag = result.etag;
|
|
1816
|
+
cachedMcpTier = result.data.meta.mcpTier ?? null;
|
|
1341
1817
|
await updateChangeSummary(result.data);
|
|
1342
1818
|
return result.data;
|
|
1343
1819
|
}
|
|
@@ -1345,7 +1821,7 @@ var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows",
|
|
|
1345
1821
|
var server = new Server(
|
|
1346
1822
|
{
|
|
1347
1823
|
name: "atomix-mcp-user",
|
|
1348
|
-
version: "1.0.
|
|
1824
|
+
version: "1.0.19"
|
|
1349
1825
|
},
|
|
1350
1826
|
{
|
|
1351
1827
|
capabilities: {
|
|
@@ -1356,165 +1832,494 @@ var server = new Server(
|
|
|
1356
1832
|
}
|
|
1357
1833
|
);
|
|
1358
1834
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
inputSchema: {
|
|
1412
|
-
type: "object",
|
|
1413
|
-
properties: {
|
|
1414
|
-
value: {
|
|
1415
|
-
type: "string",
|
|
1416
|
-
description: "CSS value to validate (e.g., '#ff0000', '16px', 'rgb(0,112,97)')"
|
|
1417
|
-
},
|
|
1418
|
-
context: {
|
|
1419
|
-
type: "string",
|
|
1420
|
-
enum: ["color", "spacing", "radius", "shadow", "typography", "any"],
|
|
1421
|
-
description: "Context of the value to help find the right token"
|
|
1422
|
-
}
|
|
1423
|
-
},
|
|
1424
|
-
required: ["value"]
|
|
1425
|
-
}
|
|
1426
|
-
},
|
|
1427
|
-
{
|
|
1428
|
-
name: "getAIToolRules",
|
|
1429
|
-
description: "Generate design system rules for AI coding tools (Cursor, Copilot, Windsurf, etc.).",
|
|
1430
|
-
inputSchema: {
|
|
1431
|
-
type: "object",
|
|
1432
|
-
properties: {
|
|
1433
|
-
tool: {
|
|
1434
|
-
type: "string",
|
|
1435
|
-
enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic", "all"],
|
|
1436
|
-
description: "AI tool to generate rules for. Use 'all' to get rules for all tools."
|
|
1437
|
-
}
|
|
1835
|
+
if (!hasValidAuthConfig()) {
|
|
1836
|
+
authFailedNoTools = true;
|
|
1837
|
+
console.error("[Atomix MCP] Missing --ds-id or --atomix-token. Add both to your MCP config.");
|
|
1838
|
+
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
1839
|
+
}
|
|
1840
|
+
if (cachedMcpTier === null) {
|
|
1841
|
+
try {
|
|
1842
|
+
await fetchDesignSystemForMCP(true);
|
|
1843
|
+
if (cachedMcpTier === "pro") {
|
|
1844
|
+
console.error("[Atomix MCP] Resolved tier = pro. Figma sync/design tools are available.");
|
|
1845
|
+
} else if (cachedMcpTier === "free") {
|
|
1846
|
+
console.error(
|
|
1847
|
+
"[Atomix MCP] Resolved tier = free. Figma sync/design tools are hidden. Pro tools appear when the DS owner has Pro and the pro_figma_export flag is enabled."
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1852
|
+
authFailedNoTools = true;
|
|
1853
|
+
console.error(
|
|
1854
|
+
"[Atomix MCP] Design system not loaded: ds-id or token invalid or API error. No tools will be shown.",
|
|
1855
|
+
msg.includes("401") ? " Token invalid or expired. Regenerate in Atomix Studio (Settings \u2192 Regenerate Atomix access token), update your MCP config, then restart Cursor." : msg.includes("403") ? " You do not have access to this design system (owner or invited guest)." : msg.includes("404") ? " Design system not found (invalid ds-id)." : msg
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (authFailedNoTools) {
|
|
1860
|
+
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
1861
|
+
}
|
|
1862
|
+
const toolsList = [
|
|
1863
|
+
{
|
|
1864
|
+
name: "getToken",
|
|
1865
|
+
description: "Get a specific design token by its path. Returns the value and CSS variable name.",
|
|
1866
|
+
inputSchema: {
|
|
1867
|
+
type: "object",
|
|
1868
|
+
properties: {
|
|
1869
|
+
path: {
|
|
1870
|
+
type: "string",
|
|
1871
|
+
description: "Token path in dot notation (e.g., 'colors.brand.primary', 'spacing.scale.md')"
|
|
1872
|
+
}
|
|
1873
|
+
},
|
|
1874
|
+
required: ["path"]
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
name: "listTokens",
|
|
1879
|
+
description: "List all tokens in a category (colors, typography, spacing, sizing, shadows, radius, borders, motion, zIndex).",
|
|
1880
|
+
inputSchema: {
|
|
1881
|
+
type: "object",
|
|
1882
|
+
properties: {
|
|
1883
|
+
category: {
|
|
1884
|
+
type: "string",
|
|
1885
|
+
enum: TOKEN_CATEGORIES,
|
|
1886
|
+
description: "Token category to list"
|
|
1438
1887
|
},
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1888
|
+
subcategory: {
|
|
1889
|
+
type: "string",
|
|
1890
|
+
description: "Optional subcategory (e.g., 'brand' for colors, 'scale' for spacing)"
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
required: ["category"]
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
name: "searchTokens",
|
|
1898
|
+
description: "Search for tokens by name or value.",
|
|
1899
|
+
inputSchema: {
|
|
1900
|
+
type: "object",
|
|
1901
|
+
properties: {
|
|
1902
|
+
query: {
|
|
1903
|
+
type: "string",
|
|
1904
|
+
description: "Search query (matches token paths or values)"
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
required: ["query"]
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
name: "validateUsage",
|
|
1912
|
+
description: "Check if a CSS value follows the design system. Detects hardcoded values that should use tokens.",
|
|
1913
|
+
inputSchema: {
|
|
1914
|
+
type: "object",
|
|
1915
|
+
properties: {
|
|
1916
|
+
value: {
|
|
1917
|
+
type: "string",
|
|
1918
|
+
description: "CSS value to validate (e.g., '#ff0000', '16px', 'rgb(0,112,97)')"
|
|
1453
1919
|
},
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1920
|
+
context: {
|
|
1921
|
+
type: "string",
|
|
1922
|
+
enum: ["color", "spacing", "radius", "shadow", "typography", "any"],
|
|
1923
|
+
description: "Context of the value to help find the right token"
|
|
1924
|
+
}
|
|
1925
|
+
},
|
|
1926
|
+
required: ["value"]
|
|
1927
|
+
}
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
name: "getAIToolRules",
|
|
1931
|
+
description: "Generate design system rules for AI coding tools (Cursor, Copilot, Windsurf, etc.).",
|
|
1932
|
+
inputSchema: {
|
|
1933
|
+
type: "object",
|
|
1934
|
+
properties: {
|
|
1935
|
+
tool: {
|
|
1936
|
+
type: "string",
|
|
1937
|
+
enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic", "all"],
|
|
1938
|
+
description: "AI tool to generate rules for. Use 'all' to get rules for all tools."
|
|
1939
|
+
}
|
|
1940
|
+
},
|
|
1941
|
+
required: ["tool"]
|
|
1942
|
+
}
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
name: "exportMCPConfig",
|
|
1946
|
+
description: "Generate MCP configuration file for AI tools.",
|
|
1947
|
+
inputSchema: {
|
|
1948
|
+
type: "object",
|
|
1949
|
+
properties: {
|
|
1950
|
+
tool: {
|
|
1951
|
+
type: "string",
|
|
1952
|
+
enum: ["cursor", "claude-desktop", "windsurf", "continue", "vscode", "all"],
|
|
1953
|
+
description: "AI tool to generate MCP config for."
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
required: ["tool"]
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
name: "getSetupInstructions",
|
|
1961
|
+
description: "Get detailed setup instructions for a specific AI tool.",
|
|
1962
|
+
inputSchema: {
|
|
1963
|
+
type: "object",
|
|
1964
|
+
properties: {
|
|
1965
|
+
tool: {
|
|
1966
|
+
type: "string",
|
|
1967
|
+
enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "claude-desktop", "generic"],
|
|
1968
|
+
description: "AI tool to get setup instructions for."
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
required: ["tool"]
|
|
1972
|
+
}
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
name: "syncAll",
|
|
1976
|
+
description: "Sync tokens, AI rules, skills files (SKILL.md, design-in-figma.md), and atomix-dependencies.json. One tool for full project sync. Use /--sync prompt or call when the user wants to sync. Optional: output (default ./tokens.css), format (default css), skipTokens (if true, only writes skills and manifest).",
|
|
1977
|
+
inputSchema: {
|
|
1978
|
+
type: "object",
|
|
1979
|
+
properties: {
|
|
1980
|
+
output: {
|
|
1981
|
+
type: "string",
|
|
1982
|
+
description: "Token file path (e.g. ./tokens.css). Default: ./tokens.css. Ignored if skipTokens is true."
|
|
1468
1983
|
},
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
name: "syncTokens",
|
|
1474
|
-
description: "Sync design tokens to a local file. Safe, never breaks UI - adds new tokens, updates existing values, marks deprecated tokens. Use /refactor to migrate deprecated tokens. WARNING: The output file is completely rewritten - only CSS custom properties (variables) are preserved. Custom CSS rules will be lost. Keep custom CSS in a separate file.",
|
|
1475
|
-
inputSchema: {
|
|
1476
|
-
type: "object",
|
|
1477
|
-
properties: {
|
|
1478
|
-
output: {
|
|
1479
|
-
type: "string",
|
|
1480
|
-
description: "Output file path relative to project root (e.g., './src/tokens.css', './DesignTokens.swift')"
|
|
1481
|
-
},
|
|
1482
|
-
format: {
|
|
1483
|
-
type: "string",
|
|
1484
|
-
enum: ["css", "scss", "less", "json", "ts", "js", "swift", "kotlin", "dart"],
|
|
1485
|
-
description: "Output format. WEB: css, scss, less, json, ts, js. NATIVE: swift (iOS), kotlin (Android), dart (Flutter). Default: css"
|
|
1486
|
-
}
|
|
1984
|
+
format: {
|
|
1985
|
+
type: "string",
|
|
1986
|
+
enum: ["css", "scss", "less", "json", "ts", "js", "swift", "kotlin", "dart"],
|
|
1987
|
+
description: "Token output format. Default: css. Ignored if skipTokens is true."
|
|
1487
1988
|
},
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1989
|
+
skipTokens: {
|
|
1990
|
+
type: "boolean",
|
|
1991
|
+
description: "If true, skip token file and rules sync; only write skills and dependencies manifest. Default: false."
|
|
1992
|
+
}
|
|
1993
|
+
},
|
|
1994
|
+
required: []
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
name: "getDependencies",
|
|
1999
|
+
description: "Get suggested dependencies for this design system (icon package, fonts, SKILL.md, token files). Use with /--get-started prompt. Optional platform and stack for tailored suggestions.",
|
|
2000
|
+
inputSchema: {
|
|
2001
|
+
type: "object",
|
|
2002
|
+
properties: {
|
|
2003
|
+
platform: {
|
|
2004
|
+
type: "string",
|
|
2005
|
+
enum: ["web", "ios", "android"],
|
|
2006
|
+
description: "Target platform (web, ios, android). Optional."
|
|
1506
2007
|
},
|
|
1507
|
-
|
|
2008
|
+
stack: {
|
|
2009
|
+
type: "string",
|
|
2010
|
+
description: "Stack or framework (e.g. react, vue, next, swift, kotlin). Optional."
|
|
2011
|
+
}
|
|
2012
|
+
},
|
|
2013
|
+
required: []
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
{
|
|
2017
|
+
name: "syncToFigma",
|
|
2018
|
+
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'.",
|
|
2019
|
+
inputSchema: {
|
|
2020
|
+
type: "object",
|
|
2021
|
+
properties: {},
|
|
2022
|
+
required: []
|
|
2023
|
+
}
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
name: "getFigmaVariablesAndStyles",
|
|
2027
|
+
description: "Get all local variable collections (with variable names and ids) and local paint, text, and effect styles from the open Figma file. Call after syncToFigma when designing in Figma; use with resolveFigmaIdsForTokens to get ids for granular design commands. Requires Atomix Figma plugin connected.",
|
|
2028
|
+
inputSchema: {
|
|
2029
|
+
type: "object",
|
|
2030
|
+
properties: {},
|
|
2031
|
+
required: []
|
|
2032
|
+
}
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
name: "createDesignPlaceholder",
|
|
2036
|
+
description: "Create a placeholder frame in Figma with name 'Atomix: Preparing the design..', temporary background, and 'Design in progress...' text. Call this first when designing in Figma so the user sees the placeholder. Returns frameId; use that as parentId for designCreateFrame and other design commands. Optional width and height (default 400); use dimensions inferred from the request (e.g. mobile 390x844, card 400x300).",
|
|
2037
|
+
inputSchema: {
|
|
2038
|
+
type: "object",
|
|
2039
|
+
properties: {
|
|
2040
|
+
width: { type: "number", description: "Frame width in px (optional, default 400)" },
|
|
2041
|
+
height: { type: "number", description: "Frame height in px (optional, default 400)" }
|
|
1508
2042
|
}
|
|
1509
2043
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
name: "resolveFigmaIdsForTokens",
|
|
2047
|
+
description: "Resolve design system token names to Figma variable and style ids in the open file. Call after syncToFigma and getFigmaVariablesAndStyles. Returns a map keyed by Figma name (e.g. 'Spacing / lg', 'Background / surface') to { variableId?, paintStyleId?, textStyleId?, effectStyleId? }. Use these ids in designCreateFrame, designCreateText, designSetAutoLayout, etc. No arguments.",
|
|
2048
|
+
inputSchema: {
|
|
2049
|
+
type: "object",
|
|
2050
|
+
properties: {},
|
|
2051
|
+
required: []
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
name: "designCreateFrame",
|
|
2056
|
+
description: "Create a frame in Figma under the given parent. Use fillVariableId or fillPaintStyleId from resolveFigmaIdsForTokens for background. Returns nodeId.",
|
|
2057
|
+
inputSchema: {
|
|
2058
|
+
type: "object",
|
|
2059
|
+
properties: {
|
|
2060
|
+
parentId: { type: "string", description: "Parent frame/node id (e.g. from createDesignPlaceholder)" },
|
|
2061
|
+
name: { type: "string", description: "Layer name" },
|
|
2062
|
+
width: { type: "number", description: "Width in px (optional)" },
|
|
2063
|
+
height: { type: "number", description: "Height in px (optional)" },
|
|
2064
|
+
fillVariableId: { type: "string", description: "Color variable id from resolveFigmaIdsForTokens" },
|
|
2065
|
+
fillPaintStyleId: { type: "string", description: "Paint style id from resolveFigmaIdsForTokens" }
|
|
2066
|
+
},
|
|
2067
|
+
required: ["parentId", "name"]
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
name: "designCreateText",
|
|
2072
|
+
description: "Create a text node in Figma. Bind to a text style via textStyleId from resolveFigmaIdsForTokens.",
|
|
2073
|
+
inputSchema: {
|
|
2074
|
+
type: "object",
|
|
2075
|
+
properties: {
|
|
2076
|
+
parentId: { type: "string", description: "Parent frame id" },
|
|
2077
|
+
characters: { type: "string", description: "Text content" },
|
|
2078
|
+
textStyleId: { type: "string", description: "Text style id from resolveFigmaIdsForTokens (required)" },
|
|
2079
|
+
name: { type: "string", description: "Layer name (optional)" }
|
|
2080
|
+
},
|
|
2081
|
+
required: ["parentId", "characters", "textStyleId"]
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
name: "designCreateRectangle",
|
|
2086
|
+
description: "Create a rectangle in Figma. Use fillVariableId or fillPaintStyleId from resolveFigmaIdsForTokens.",
|
|
2087
|
+
inputSchema: {
|
|
2088
|
+
type: "object",
|
|
2089
|
+
properties: {
|
|
2090
|
+
parentId: { type: "string", description: "Parent frame id" },
|
|
2091
|
+
width: { type: "number", description: "Width in px" },
|
|
2092
|
+
height: { type: "number", description: "Height in px" },
|
|
2093
|
+
fillVariableId: { type: "string", description: "Color variable id" },
|
|
2094
|
+
fillPaintStyleId: { type: "string", description: "Paint style id" },
|
|
2095
|
+
name: { type: "string", description: "Layer name (optional)" }
|
|
2096
|
+
},
|
|
2097
|
+
required: ["parentId", "width", "height"]
|
|
2098
|
+
}
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
name: "designSetAutoLayout",
|
|
2102
|
+
description: "Set auto-layout on a frame. Use variable ids from resolveFigmaIdsForTokens for padding and itemSpacing (e.g. Spacing / lg).",
|
|
2103
|
+
inputSchema: {
|
|
2104
|
+
type: "object",
|
|
2105
|
+
properties: {
|
|
2106
|
+
nodeId: { type: "string", description: "Frame node id" },
|
|
2107
|
+
direction: { type: "string", description: "HORIZONTAL or VERTICAL" },
|
|
2108
|
+
paddingVariableId: { type: "string", description: "Number variable id for all padding" },
|
|
2109
|
+
paddingTopVariableId: { type: "string" },
|
|
2110
|
+
paddingRightVariableId: { type: "string" },
|
|
2111
|
+
paddingBottomVariableId: { type: "string" },
|
|
2112
|
+
paddingLeftVariableId: { type: "string" },
|
|
2113
|
+
itemSpacingVariableId: { type: "string", description: "Number variable id for gap between children" },
|
|
2114
|
+
primaryAxisAlignItems: { type: "string", description: "MIN, CENTER, MAX, SPACE_BETWEEN" },
|
|
2115
|
+
counterAxisAlignItems: { type: "string", description: "MIN, CENTER, MAX, BASELINE" },
|
|
2116
|
+
layoutSizingHorizontal: { type: "string", description: "HUG or FILL" },
|
|
2117
|
+
layoutSizingVertical: { type: "string", description: "HUG or FILL" }
|
|
2118
|
+
},
|
|
2119
|
+
required: ["nodeId", "direction"]
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
name: "designSetLayoutConstraints",
|
|
2124
|
+
description: "Set min/max width and height on a frame (e.g. breakpoint variables). Use number variable ids from resolveFigmaIdsForTokens.",
|
|
2125
|
+
inputSchema: {
|
|
2126
|
+
type: "object",
|
|
2127
|
+
properties: {
|
|
2128
|
+
nodeId: { type: "string", description: "Frame node id" },
|
|
2129
|
+
minWidthVariableId: { type: "string" },
|
|
2130
|
+
maxWidthVariableId: { type: "string" },
|
|
2131
|
+
minHeightVariableId: { type: "string" },
|
|
2132
|
+
maxHeightVariableId: { type: "string" }
|
|
2133
|
+
},
|
|
2134
|
+
required: ["nodeId"]
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
name: "designAppendChild",
|
|
2139
|
+
description: "Move a node under a new parent (reparent). Use to reorder or nest nodes.",
|
|
2140
|
+
inputSchema: {
|
|
2141
|
+
type: "object",
|
|
2142
|
+
properties: {
|
|
2143
|
+
parentId: { type: "string", description: "New parent frame id" },
|
|
2144
|
+
childId: { type: "string", description: "Node id to move" }
|
|
2145
|
+
},
|
|
2146
|
+
required: ["parentId", "childId"]
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
name: "getDesignScreenshot",
|
|
2151
|
+
description: "Export a frame as PNG and return it as base64 so you can verify layout, content fill, and hug. Call after each design pass to check your work against the user's intent. Use the returned image to decide if another pass is needed.",
|
|
2152
|
+
inputSchema: {
|
|
2153
|
+
type: "object",
|
|
2154
|
+
properties: {
|
|
2155
|
+
frameId: { type: "string", description: "Frame node id (e.g. from createDesignPlaceholder)" },
|
|
2156
|
+
scale: { type: "number", description: "Export scale 1\u20134 (optional, default 1)" }
|
|
2157
|
+
},
|
|
2158
|
+
required: ["frameId"]
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
name: "finalizeDesignFrame",
|
|
2163
|
+
description: "Rename the design frame and remove the placeholder background. Call after the final design pass. Set name to a short description of the design plus ' \u2705'. Use fillVariableId or fillPaintStyleId from resolveFigmaIdsForTokens (e.g. Background / surface) so the gray placeholder is replaced.",
|
|
2164
|
+
inputSchema: {
|
|
2165
|
+
type: "object",
|
|
2166
|
+
properties: {
|
|
2167
|
+
frameId: { type: "string", description: "Frame node id to finalize" },
|
|
2168
|
+
name: { type: "string", description: "New frame name (e.g. 'Login card \u2705')" },
|
|
2169
|
+
fillVariableId: { type: "string", description: "Variable id for frame fill (removes placeholder bg)" },
|
|
2170
|
+
fillPaintStyleId: { type: "string", description: "Paint style id for frame fill" }
|
|
2171
|
+
},
|
|
2172
|
+
required: ["frameId", "name"]
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
];
|
|
2176
|
+
const tools = cachedMcpTier === "pro" ? toolsList : toolsList.filter((t) => !FIGMA_TOOL_NAMES.has(t.name));
|
|
2177
|
+
return { tools };
|
|
1512
2178
|
});
|
|
1513
2179
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1514
2180
|
const { name, arguments: args } = request.params;
|
|
2181
|
+
if (!hasValidAuthConfig() || authFailedNoTools) {
|
|
2182
|
+
return {
|
|
2183
|
+
content: [{
|
|
2184
|
+
type: "text",
|
|
2185
|
+
text: "MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart Cursor. No tools are available until then."
|
|
2186
|
+
}],
|
|
2187
|
+
isError: true
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
1515
2190
|
try {
|
|
1516
|
-
const shouldForceRefresh = name === "
|
|
2191
|
+
const shouldForceRefresh = name === "syncAll";
|
|
1517
2192
|
const data = await fetchDesignSystemForMCP(shouldForceRefresh);
|
|
2193
|
+
if (FIGMA_TOOL_NAMES.has(name) && cachedMcpTier !== "pro") {
|
|
2194
|
+
return {
|
|
2195
|
+
content: [
|
|
2196
|
+
{
|
|
2197
|
+
type: "text",
|
|
2198
|
+
text: "This design system does not have Pro Figma access. Figma sync and design tools are available when the design system owner has a Pro subscription."
|
|
2199
|
+
}
|
|
2200
|
+
],
|
|
2201
|
+
isError: true
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat) {
|
|
2205
|
+
const output = tokenOutput;
|
|
2206
|
+
const format = tokenFormat;
|
|
2207
|
+
const outputPath = path2.resolve(process.cwd(), output);
|
|
2208
|
+
const fileExists = fs2.existsSync(outputPath);
|
|
2209
|
+
const deprecatedTokens = /* @__PURE__ */ new Map();
|
|
2210
|
+
const existingTokens = /* @__PURE__ */ new Map();
|
|
2211
|
+
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
2212
|
+
const oldContent = fs2.readFileSync(outputPath, "utf-8");
|
|
2213
|
+
const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
|
|
2214
|
+
let match;
|
|
2215
|
+
while ((match = oldVarPattern.exec(oldContent)) !== null) {
|
|
2216
|
+
const varName = match[1];
|
|
2217
|
+
const varValue = match[2].trim();
|
|
2218
|
+
existingTokens.set(varName, varValue);
|
|
2219
|
+
if (!(varName in designSystemData.cssVariables)) deprecatedTokens.set(varName, varValue);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
const mergedCssVariables = { ...designSystemData.cssVariables };
|
|
2223
|
+
const darkModeColors = designSystemData.tokens?.colors?.modes;
|
|
2224
|
+
let newContent;
|
|
2225
|
+
switch (format) {
|
|
2226
|
+
case "css":
|
|
2227
|
+
newContent = generateCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2228
|
+
break;
|
|
2229
|
+
case "scss":
|
|
2230
|
+
newContent = generateSCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2231
|
+
break;
|
|
2232
|
+
case "less":
|
|
2233
|
+
newContent = generateLessOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2234
|
+
break;
|
|
2235
|
+
case "json":
|
|
2236
|
+
newContent = generateJSONOutput(designSystemData.tokens);
|
|
2237
|
+
break;
|
|
2238
|
+
case "js":
|
|
2239
|
+
newContent = generateJSOutput(designSystemData.tokens);
|
|
2240
|
+
break;
|
|
2241
|
+
case "ts":
|
|
2242
|
+
newContent = generateTSOutput(designSystemData.tokens);
|
|
2243
|
+
break;
|
|
2244
|
+
case "swift":
|
|
2245
|
+
newContent = generateSwiftOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2246
|
+
break;
|
|
2247
|
+
case "kotlin":
|
|
2248
|
+
newContent = generateKotlinOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2249
|
+
break;
|
|
2250
|
+
case "dart":
|
|
2251
|
+
newContent = generateDartOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2252
|
+
break;
|
|
2253
|
+
default:
|
|
2254
|
+
newContent = generateCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
2255
|
+
}
|
|
2256
|
+
const tokenCount = Object.keys(mergedCssVariables).length;
|
|
2257
|
+
const dsTokenCount = Object.keys(designSystemData.cssVariables).length;
|
|
2258
|
+
const deprecatedCount = deprecatedTokens.size;
|
|
2259
|
+
let changes = [];
|
|
2260
|
+
let diff;
|
|
2261
|
+
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
2262
|
+
const oldContent = fs2.readFileSync(outputPath, "utf-8");
|
|
2263
|
+
diff = diffTokens(oldContent, mergedCssVariables, format, darkModeColors?.dark);
|
|
2264
|
+
const lightChanges = diff.added.length + diff.modified.length;
|
|
2265
|
+
const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
|
|
2266
|
+
const totalChanges = lightChanges + darkChanges + deprecatedCount;
|
|
2267
|
+
if (totalChanges === 0) {
|
|
2268
|
+
const lastUpdated = designSystemData.meta.exportedAt ? new Date(designSystemData.meta.exportedAt).toLocaleString() : "N/A";
|
|
2269
|
+
return {
|
|
2270
|
+
responseText: `\u2713 Already up to date!
|
|
2271
|
+
|
|
2272
|
+
File: ${output}
|
|
2273
|
+
Tokens: ${tokenCount}
|
|
2274
|
+
Version: ${designSystemData.meta.version}
|
|
2275
|
+
Last updated: ${lastUpdated}`,
|
|
2276
|
+
rulesResults: []
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
changes = [...diff.modified, ...diff.modifiedDark];
|
|
2280
|
+
const removedTokensWithValues = [];
|
|
2281
|
+
for (const [token, value] of deprecatedTokens.entries()) removedTokensWithValues.push({ token, lastValue: value });
|
|
2282
|
+
lastSyncAffectedTokens = {
|
|
2283
|
+
modified: [...diff.modified, ...diff.modifiedDark].map((m) => ({ token: m.key, oldValue: m.old, newValue: m.new })),
|
|
2284
|
+
removed: removedTokensWithValues,
|
|
2285
|
+
added: [...diff.added, ...diff.addedDark],
|
|
2286
|
+
format,
|
|
2287
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
const outputDir = path2.dirname(outputPath);
|
|
2291
|
+
if (!fs2.existsSync(outputDir)) fs2.mkdirSync(outputDir, { recursive: true });
|
|
2292
|
+
fs2.writeFileSync(outputPath, newContent);
|
|
2293
|
+
let rulesResults = [];
|
|
2294
|
+
try {
|
|
2295
|
+
rulesResults = await syncRulesFiles({
|
|
2296
|
+
dsId,
|
|
2297
|
+
apiKey: apiKey ?? void 0,
|
|
2298
|
+
apiBase: apiBase ?? void 0,
|
|
2299
|
+
rulesDir: process.cwd()
|
|
2300
|
+
});
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
console.error(`[syncAll] Failed to sync rules: ${error}`);
|
|
2303
|
+
}
|
|
2304
|
+
const governanceChanges = cachedData ? detectGovernanceChangesByFoundation(cachedData, designSystemData) : [];
|
|
2305
|
+
const response = formatSyncResponse({
|
|
2306
|
+
data: designSystemData,
|
|
2307
|
+
output,
|
|
2308
|
+
format,
|
|
2309
|
+
dsTokenCount,
|
|
2310
|
+
deprecatedCount,
|
|
2311
|
+
deprecatedTokens,
|
|
2312
|
+
diff,
|
|
2313
|
+
changes,
|
|
2314
|
+
fileExists,
|
|
2315
|
+
rulesResults,
|
|
2316
|
+
governanceChanges,
|
|
2317
|
+
changeSummary: getLastChangeSummary(),
|
|
2318
|
+
hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
|
|
2319
|
+
deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
|
|
2320
|
+
});
|
|
2321
|
+
return { responseText: response, rulesResults };
|
|
2322
|
+
}
|
|
1518
2323
|
switch (name) {
|
|
1519
2324
|
case "getToken": {
|
|
1520
2325
|
const path4 = args?.path;
|
|
@@ -1688,7 +2493,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1688
2493
|
const serverName = data.meta.name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
1689
2494
|
const npxArgs = ["@atomixstudio/mcp@latest"];
|
|
1690
2495
|
if (dsId) npxArgs.push("--ds-id", dsId);
|
|
1691
|
-
if (
|
|
2496
|
+
if (accessToken) npxArgs.push("--atomix-token", accessToken);
|
|
1692
2497
|
const config = {
|
|
1693
2498
|
mcpServers: {
|
|
1694
2499
|
[serverName]: {
|
|
@@ -1879,153 +2684,74 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1879
2684
|
}]
|
|
1880
2685
|
};
|
|
1881
2686
|
}
|
|
1882
|
-
case "
|
|
1883
|
-
const
|
|
2687
|
+
case "syncAll": {
|
|
2688
|
+
const skipTokens = args?.skipTokens === true;
|
|
2689
|
+
const output = args?.output || "./tokens.css";
|
|
1884
2690
|
const format = args?.format || "css";
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const outputPath = path2.resolve(process.cwd(), output);
|
|
1894
|
-
const fileExists = fs2.existsSync(outputPath);
|
|
1895
|
-
const deprecatedTokens = /* @__PURE__ */ new Map();
|
|
1896
|
-
const existingTokens = /* @__PURE__ */ new Map();
|
|
1897
|
-
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
1898
|
-
const oldContent = fs2.readFileSync(outputPath, "utf-8");
|
|
1899
|
-
const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
|
|
1900
|
-
let match;
|
|
1901
|
-
while ((match = oldVarPattern.exec(oldContent)) !== null) {
|
|
1902
|
-
const varName = match[1];
|
|
1903
|
-
const varValue = match[2].trim();
|
|
1904
|
-
existingTokens.set(varName, varValue);
|
|
1905
|
-
if (!(varName in data.cssVariables)) {
|
|
1906
|
-
deprecatedTokens.set(varName, varValue);
|
|
1907
|
-
}
|
|
2691
|
+
const parts = ["\u2713 syncAll complete."];
|
|
2692
|
+
let tokenResponseText = "";
|
|
2693
|
+
if (!skipTokens) {
|
|
2694
|
+
const { responseText, rulesResults } = await performTokenSyncAndRules(data, output, format);
|
|
2695
|
+
tokenResponseText = responseText;
|
|
2696
|
+
parts.push(`Tokens: ${output} (${format})`);
|
|
2697
|
+
if (rulesResults.length > 0) {
|
|
2698
|
+
parts.push(`Rules: ${rulesResults.map((r) => r.path).join(", ")}`);
|
|
1908
2699
|
}
|
|
1909
2700
|
}
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
newContent = generateJSOutput(data.tokens);
|
|
1928
|
-
break;
|
|
1929
|
-
case "ts":
|
|
1930
|
-
newContent = generateTSOutput(data.tokens);
|
|
1931
|
-
break;
|
|
1932
|
-
case "swift":
|
|
1933
|
-
newContent = generateSwiftOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
1934
|
-
break;
|
|
1935
|
-
case "kotlin":
|
|
1936
|
-
newContent = generateKotlinOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
1937
|
-
break;
|
|
1938
|
-
case "dart":
|
|
1939
|
-
newContent = generateDartOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
1940
|
-
break;
|
|
1941
|
-
default:
|
|
1942
|
-
newContent = generateCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
1943
|
-
}
|
|
1944
|
-
const tokenCount = Object.keys(mergedCssVariables).length;
|
|
1945
|
-
const dsTokenCount = Object.keys(data.cssVariables).length;
|
|
1946
|
-
const deprecatedCount = deprecatedTokens.size;
|
|
1947
|
-
let diffSummary = "";
|
|
1948
|
-
let changes = [];
|
|
1949
|
-
let diff;
|
|
1950
|
-
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
1951
|
-
const oldContent = fs2.readFileSync(outputPath, "utf-8");
|
|
1952
|
-
diff = diffTokens(oldContent, mergedCssVariables, format, darkModeColors?.dark);
|
|
1953
|
-
const lightChanges = diff.added.length + diff.modified.length;
|
|
1954
|
-
const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
|
|
1955
|
-
const totalChanges = lightChanges + darkChanges + deprecatedCount;
|
|
1956
|
-
if (totalChanges === 0) {
|
|
1957
|
-
const lastUpdated = data.meta.exportedAt ? new Date(data.meta.exportedAt).toLocaleString() : "N/A";
|
|
1958
|
-
return {
|
|
1959
|
-
content: [{
|
|
1960
|
-
type: "text",
|
|
1961
|
-
text: `\u2713 Already up to date!
|
|
1962
|
-
|
|
1963
|
-
File: ${output}
|
|
1964
|
-
Tokens: ${tokenCount}
|
|
1965
|
-
Version: ${data.meta.version}
|
|
1966
|
-
Last updated: ${lastUpdated}`
|
|
1967
|
-
}]
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
changes = [...diff.modified, ...diff.modifiedDark];
|
|
1971
|
-
const lightSummary = lightChanges > 0 ? `Light: ${diff.modified.length} modified, ${diff.added.length} added` : "";
|
|
1972
|
-
const darkSummary = darkChanges > 0 ? `Dark: ${diff.modifiedDark.length} modified, ${diff.addedDark.length} added` : "";
|
|
1973
|
-
const deprecatedSummary = deprecatedCount > 0 ? `${deprecatedCount} deprecated (use /refactor)` : "";
|
|
1974
|
-
diffSummary = [lightSummary, darkSummary, deprecatedSummary].filter(Boolean).join(" | ");
|
|
1975
|
-
const removedTokensWithValues = [];
|
|
1976
|
-
for (const [token, value] of deprecatedTokens.entries()) {
|
|
1977
|
-
removedTokensWithValues.push({ token, lastValue: value });
|
|
2701
|
+
const skillsDir = path2.resolve(process.cwd(), ".cursor/skills/atomix-ds");
|
|
2702
|
+
if (!fs2.existsSync(skillsDir)) fs2.mkdirSync(skillsDir, { recursive: true });
|
|
2703
|
+
const dsVersion = String(data.meta.version ?? "1.0.0");
|
|
2704
|
+
const dsExportedAt = data.meta.exportedAt;
|
|
2705
|
+
const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
|
|
2706
|
+
const figmaWithVersion = injectSkillVersion(FIGMA_DESIGN_SKILL_MD, dsVersion, dsExportedAt);
|
|
2707
|
+
fs2.writeFileSync(path2.join(skillsDir, "SKILL.md"), genericWithVersion);
|
|
2708
|
+
fs2.writeFileSync(path2.join(skillsDir, "design-in-figma.md"), figmaWithVersion);
|
|
2709
|
+
parts.push("Skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/design-in-figma.md (synced at DS v" + dsVersion + ")");
|
|
2710
|
+
const tokens = data.tokens;
|
|
2711
|
+
const typography = tokens?.typography;
|
|
2712
|
+
const fontFamily = typography?.fontFamily;
|
|
2713
|
+
const fontNames = [];
|
|
2714
|
+
if (fontFamily) {
|
|
2715
|
+
for (const key of ["display", "heading", "body"]) {
|
|
2716
|
+
const v = fontFamily[key];
|
|
2717
|
+
if (typeof v === "string" && v && !fontNames.includes(v)) fontNames.push(v);
|
|
1978
2718
|
}
|
|
1979
|
-
lastSyncAffectedTokens = {
|
|
1980
|
-
modified: [...diff.modified, ...diff.modifiedDark].map((m) => ({
|
|
1981
|
-
token: m.key,
|
|
1982
|
-
oldValue: m.old,
|
|
1983
|
-
newValue: m.new
|
|
1984
|
-
})),
|
|
1985
|
-
removed: removedTokensWithValues,
|
|
1986
|
-
added: [...diff.added, ...diff.addedDark],
|
|
1987
|
-
format,
|
|
1988
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1989
|
-
};
|
|
1990
|
-
}
|
|
1991
|
-
const outputDir = path2.dirname(outputPath);
|
|
1992
|
-
if (!fs2.existsSync(outputDir)) {
|
|
1993
|
-
fs2.mkdirSync(outputDir, { recursive: true });
|
|
1994
2719
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2720
|
+
const icons = tokens?.icons;
|
|
2721
|
+
const ICON_PACKAGES = {
|
|
2722
|
+
lucide: { web: "lucide-react", native: "lucide-react-native" },
|
|
2723
|
+
heroicons: { web: "@heroicons/react", native: "heroicons-react-native" },
|
|
2724
|
+
phosphor: { web: "phosphor-react", native: "phosphor-react-native" }
|
|
2725
|
+
};
|
|
2726
|
+
const lib = icons?.library || "lucide";
|
|
2727
|
+
const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
|
|
2728
|
+
const manifest = {
|
|
2729
|
+
designSystem: { name: data.meta.name, version: data.meta.version },
|
|
2730
|
+
tokenFile: skipTokens ? void 0 : output,
|
|
2731
|
+
iconLibrary: {
|
|
2732
|
+
package: iconPkgs.web,
|
|
2733
|
+
nativePackage: iconPkgs.native,
|
|
2734
|
+
strokeWidthToken: icons?.strokeWidth != null ? "icons.strokeWidth" : void 0,
|
|
2735
|
+
strokeWidthValue: icons?.strokeWidth
|
|
2736
|
+
},
|
|
2737
|
+
fonts: { families: fontNames },
|
|
2738
|
+
skills: {
|
|
2739
|
+
skill: ".cursor/skills/atomix-ds/SKILL.md",
|
|
2740
|
+
skillFigmaDesign: ".cursor/skills/atomix-ds/design-in-figma.md",
|
|
2741
|
+
syncedAtVersion: data.meta.version ?? "1.0.0"
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
const manifestPath = path2.resolve(process.cwd(), "atomix-dependencies.json");
|
|
2745
|
+
fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2746
|
+
parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
|
|
2747
|
+
const summary = parts.join("\n");
|
|
2748
|
+
const fullText = tokenResponseText ? `${summary}
|
|
2749
|
+
|
|
2750
|
+
---
|
|
2751
|
+
|
|
2752
|
+
${tokenResponseText}` : summary;
|
|
2024
2753
|
return {
|
|
2025
|
-
content: [{
|
|
2026
|
-
type: "text",
|
|
2027
|
-
text: response
|
|
2028
|
-
}]
|
|
2754
|
+
content: [{ type: "text", text: fullText }]
|
|
2029
2755
|
};
|
|
2030
2756
|
}
|
|
2031
2757
|
case "getDependencies": {
|
|
@@ -2053,21 +2779,38 @@ Last updated: ${lastUpdated}`
|
|
|
2053
2779
|
iconLibrary: {
|
|
2054
2780
|
package: iconPkgs.web,
|
|
2055
2781
|
nativePackage: iconPkgs.native,
|
|
2056
|
-
|
|
2782
|
+
strokeWidthToken: icons?.strokeWidth != null ? "icons.strokeWidth" : void 0,
|
|
2783
|
+
strokeWidthValue: icons?.strokeWidth,
|
|
2784
|
+
performanceHint: "Use individual SVG imports for tree-shaking. Apply the design system's icon tokens: sizing via getToken('sizing.icon.sm') or listTokens('sizing'), and stroke width via getToken('icons.strokeWidth') when the DS defines it. Do not use hardcoded sizes or stroke widths."
|
|
2057
2785
|
},
|
|
2058
2786
|
fonts: {
|
|
2059
2787
|
families: fontNames,
|
|
2060
|
-
performanceHint: "
|
|
2788
|
+
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 typeset: CSS rules (e.g. .typeset-display, .typeset-heading, .typeset-body) that use var(--atmx-typography-*) for font-family, font-size, font-weight, line-height, letter-spacing from getToken/listTokens(typography). Do not create a file that only contains a font import."
|
|
2061
2789
|
},
|
|
2062
2790
|
skill: {
|
|
2063
2791
|
path: ".cursor/skills/atomix-ds/SKILL.md",
|
|
2064
2792
|
content: GENERIC_SKILL_MD
|
|
2065
2793
|
},
|
|
2794
|
+
skillFigmaDesign: {
|
|
2795
|
+
path: ".cursor/skills/atomix-ds/design-in-figma.md",
|
|
2796
|
+
content: FIGMA_DESIGN_SKILL_MD
|
|
2797
|
+
},
|
|
2066
2798
|
tokenFiles: {
|
|
2067
2799
|
files: ["tokens.css", "tokens.json"],
|
|
2068
|
-
copyInstructions: "Call the
|
|
2800
|
+
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."
|
|
2069
2801
|
},
|
|
2070
|
-
|
|
2802
|
+
showcase: platform === "web" || !platform ? {
|
|
2803
|
+
path: "atomix-setup-showcase.html",
|
|
2804
|
+
template: SHOWCASE_HTML_TEMPLATE,
|
|
2805
|
+
substitutionInstructions: "Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export."
|
|
2806
|
+
} : void 0,
|
|
2807
|
+
meta: {
|
|
2808
|
+
dsName: data.meta.name,
|
|
2809
|
+
platform: platform ?? void 0,
|
|
2810
|
+
stack: stack ?? void 0,
|
|
2811
|
+
designSystemVersion: data.meta.version ?? "1.0.0",
|
|
2812
|
+
designSystemExportedAt: data.meta.exportedAt
|
|
2813
|
+
}
|
|
2071
2814
|
};
|
|
2072
2815
|
return {
|
|
2073
2816
|
content: [{
|
|
@@ -2076,13 +2819,363 @@ Last updated: ${lastUpdated}`
|
|
|
2076
2819
|
}]
|
|
2077
2820
|
};
|
|
2078
2821
|
}
|
|
2822
|
+
case "syncToFigma": {
|
|
2823
|
+
const payloads = buildFigmaPayloadsFromDS(data);
|
|
2824
|
+
const out = {};
|
|
2825
|
+
const agentStartBridge = `The Figma bridge runs with this MCP server. Ensure Cursor has this MCP configured and running, then in Figma run the Atomix plugin and click Connect to Cursor. Then call **syncToFigma** again.`;
|
|
2826
|
+
const userSteps = [
|
|
2827
|
+
FIGMA_CONNECTION_INSTRUCTIONS.startBridge,
|
|
2828
|
+
FIGMA_CONNECTION_INSTRUCTIONS.installAndRun,
|
|
2829
|
+
FIGMA_CONNECTION_INSTRUCTIONS.connect,
|
|
2830
|
+
"Then run Sync to Figma again."
|
|
2831
|
+
].join(" ");
|
|
2832
|
+
try {
|
|
2833
|
+
const reachable = await isBridgeReachable();
|
|
2834
|
+
if (!reachable) {
|
|
2835
|
+
out.bridgeNotRunning = true;
|
|
2836
|
+
out.agentInstruction = agentStartBridge;
|
|
2837
|
+
out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
|
|
2838
|
+
out.figmaPayload = {
|
|
2839
|
+
collectionName: payloads.colorVariables.collectionName,
|
|
2840
|
+
modes: payloads.colorVariables.modes,
|
|
2841
|
+
variables: payloads.colorVariables.variables,
|
|
2842
|
+
paintStyles: payloads.paintStyles,
|
|
2843
|
+
textStyles: payloads.textStyles,
|
|
2844
|
+
numberVariables: payloads.numberVariables,
|
|
2845
|
+
effectStyles: payloads.effectStyles
|
|
2846
|
+
};
|
|
2847
|
+
return {
|
|
2848
|
+
content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
if (payloads.colorVariables.variables.length > 0) {
|
|
2852
|
+
out.colorVariables = await sendBridgeRequest("create_color_variables", {
|
|
2853
|
+
collectionName: payloads.colorVariables.collectionName,
|
|
2854
|
+
modes: payloads.colorVariables.modes,
|
|
2855
|
+
variables: payloads.colorVariables.variables
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
if (payloads.paintStyles.length > 0) {
|
|
2859
|
+
out.paintStyles = await sendBridgeRequest("create_paint_styles", { styles: payloads.paintStyles });
|
|
2860
|
+
}
|
|
2861
|
+
if (payloads.numberVariables.variables.length > 0) {
|
|
2862
|
+
try {
|
|
2863
|
+
out.numberVariables = await sendBridgeRequest("create_number_variables", {
|
|
2864
|
+
collectionName: payloads.numberVariables.collectionName,
|
|
2865
|
+
variables: payloads.numberVariables.variables
|
|
2866
|
+
});
|
|
2867
|
+
} catch (e) {
|
|
2868
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2869
|
+
out.numberVariables = { error: msg };
|
|
2870
|
+
if (msg.includes("Method not allowed") && msg.includes("create_number_variables")) {
|
|
2871
|
+
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.";
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (payloads.textStyles.length > 0) {
|
|
2876
|
+
out.textStyles = await sendBridgeRequest("create_text_styles", { styles: payloads.textStyles });
|
|
2877
|
+
}
|
|
2878
|
+
if (payloads.effectStyles.length > 0) {
|
|
2879
|
+
out.effectStyles = await sendBridgeRequest("create_effect_styles", { styles: payloads.effectStyles });
|
|
2880
|
+
}
|
|
2881
|
+
out.figmaPayload = {
|
|
2882
|
+
collectionName: payloads.colorVariables.collectionName,
|
|
2883
|
+
modes: payloads.colorVariables.modes,
|
|
2884
|
+
variables: payloads.colorVariables.variables,
|
|
2885
|
+
paintStyles: payloads.paintStyles,
|
|
2886
|
+
textStyles: payloads.textStyles,
|
|
2887
|
+
numberVariables: payloads.numberVariables,
|
|
2888
|
+
effectStyles: payloads.effectStyles
|
|
2889
|
+
};
|
|
2890
|
+
} catch (e) {
|
|
2891
|
+
out.error = e instanceof Error ? e.message : String(e);
|
|
2892
|
+
out.figmaPayload = {
|
|
2893
|
+
collectionName: payloads.colorVariables.collectionName,
|
|
2894
|
+
modes: payloads.colorVariables.modes,
|
|
2895
|
+
variables: payloads.colorVariables.variables,
|
|
2896
|
+
paintStyles: payloads.paintStyles,
|
|
2897
|
+
textStyles: payloads.textStyles,
|
|
2898
|
+
numberVariables: payloads.numberVariables,
|
|
2899
|
+
effectStyles: payloads.effectStyles
|
|
2900
|
+
};
|
|
2901
|
+
const errMsg = out.error.toLowerCase();
|
|
2902
|
+
const connectionFailure = errMsg.includes("econnrefused") || errMsg.includes("bridge timeout") || errMsg.includes("websocket") || errMsg.includes("network");
|
|
2903
|
+
if (connectionFailure) {
|
|
2904
|
+
out.bridgeNotRunning = true;
|
|
2905
|
+
out.agentInstruction = agentStartBridge;
|
|
2906
|
+
out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
|
|
2907
|
+
} else if (errMsg.includes("plugin not connected") || errMsg.includes("figma plugin")) {
|
|
2908
|
+
out.userInstruction = `${FIGMA_CONNECTION_INSTRUCTIONS.installAndRun} ${FIGMA_CONNECTION_INSTRUCTIONS.connect}`;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const textStylesResult = out.textStyles;
|
|
2912
|
+
if (textStylesResult?.failed && textStylesResult.failures?.length) {
|
|
2913
|
+
const firstReason = textStylesResult.failures[0].reason;
|
|
2914
|
+
out.summary = `Text styles: ${textStylesResult.failed} could not be created. ${firstReason}`;
|
|
2915
|
+
}
|
|
2916
|
+
if (out.numberVariablesHint) {
|
|
2917
|
+
out.summary = [out.summary, out.numberVariablesHint].filter(Boolean).join(" ");
|
|
2918
|
+
}
|
|
2919
|
+
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.";
|
|
2920
|
+
return {
|
|
2921
|
+
content: [{
|
|
2922
|
+
type: "text",
|
|
2923
|
+
text: JSON.stringify(out, null, 2)
|
|
2924
|
+
}],
|
|
2925
|
+
...out.error ? { isError: true } : {}
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
case "getFigmaVariablesAndStyles": {
|
|
2929
|
+
try {
|
|
2930
|
+
const reachable = await isBridgeReachable();
|
|
2931
|
+
if (!reachable) {
|
|
2932
|
+
return {
|
|
2933
|
+
content: [{
|
|
2934
|
+
type: "text",
|
|
2935
|
+
text: JSON.stringify({
|
|
2936
|
+
error: "Figma bridge not reachable.",
|
|
2937
|
+
bridgeNotRunning: true,
|
|
2938
|
+
agentInstruction: "The Figma bridge runs with this MCP server. Ensure Cursor has this MCP running, then in Figma run the Atomix plugin and click Connect to Cursor. Then call getFigmaVariablesAndStyles again."
|
|
2939
|
+
}, null, 2)
|
|
2940
|
+
}],
|
|
2941
|
+
isError: true
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
const result = await sendBridgeRequest("get_figma_variables_and_styles");
|
|
2945
|
+
return {
|
|
2946
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2947
|
+
};
|
|
2948
|
+
} catch (e) {
|
|
2949
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2950
|
+
return {
|
|
2951
|
+
content: [{
|
|
2952
|
+
type: "text",
|
|
2953
|
+
text: JSON.stringify({ error: message, hint: "Ensure the bridge is running and the Atomix plugin is connected." }, null, 2)
|
|
2954
|
+
}],
|
|
2955
|
+
isError: true
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
case "createDesignPlaceholder": {
|
|
2960
|
+
try {
|
|
2961
|
+
const reachable = await isBridgeReachable();
|
|
2962
|
+
if (!reachable) {
|
|
2963
|
+
return {
|
|
2964
|
+
content: [{
|
|
2965
|
+
type: "text",
|
|
2966
|
+
text: JSON.stringify({
|
|
2967
|
+
error: "Figma bridge not reachable.",
|
|
2968
|
+
hint: "The bridge runs with this MCP server. Ensure Cursor has this MCP running, then run the Atomix plugin and click Connect to Cursor."
|
|
2969
|
+
}, null, 2)
|
|
2970
|
+
}],
|
|
2971
|
+
isError: true
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
const w = args?.width;
|
|
2975
|
+
const h = args?.height;
|
|
2976
|
+
const placeholderParams = {};
|
|
2977
|
+
if (typeof w === "number" && w > 0) placeholderParams.width = Math.round(w);
|
|
2978
|
+
if (typeof h === "number" && h > 0) placeholderParams.height = Math.round(h);
|
|
2979
|
+
const result = await sendBridgeRequest("create_design_placeholder", placeholderParams);
|
|
2980
|
+
return {
|
|
2981
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2982
|
+
};
|
|
2983
|
+
} catch (e) {
|
|
2984
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2985
|
+
return {
|
|
2986
|
+
content: [{
|
|
2987
|
+
type: "text",
|
|
2988
|
+
text: JSON.stringify({ error: message }, null, 2)
|
|
2989
|
+
}],
|
|
2990
|
+
isError: true
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
case "resolveFigmaIdsForTokens": {
|
|
2995
|
+
try {
|
|
2996
|
+
const reachable = await isBridgeReachable();
|
|
2997
|
+
if (!reachable) {
|
|
2998
|
+
return {
|
|
2999
|
+
content: [{
|
|
3000
|
+
type: "text",
|
|
3001
|
+
text: JSON.stringify({
|
|
3002
|
+
error: "Figma bridge not reachable.",
|
|
3003
|
+
hint: "Run syncToFigma first, then ensure the plugin is connected. Then call resolveFigmaIdsForTokens again."
|
|
3004
|
+
}, null, 2)
|
|
3005
|
+
}],
|
|
3006
|
+
isError: true
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
const raw = await sendBridgeRequest("get_figma_variables_and_styles");
|
|
3010
|
+
const response = raw;
|
|
3011
|
+
const variableByName = /* @__PURE__ */ new Map();
|
|
3012
|
+
if (response.variableCollections) {
|
|
3013
|
+
for (const coll of response.variableCollections) {
|
|
3014
|
+
for (const v of coll.variables || []) {
|
|
3015
|
+
variableByName.set(v.name, v.id);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
const paintByName = /* @__PURE__ */ new Map();
|
|
3020
|
+
for (const s of response.paintStyles || []) {
|
|
3021
|
+
paintByName.set(s.name, s.id);
|
|
3022
|
+
}
|
|
3023
|
+
const textByName = /* @__PURE__ */ new Map();
|
|
3024
|
+
for (const s of response.textStyles || []) {
|
|
3025
|
+
textByName.set(s.name, s.id);
|
|
3026
|
+
}
|
|
3027
|
+
const effectByName = /* @__PURE__ */ new Map();
|
|
3028
|
+
for (const s of response.effectStyles || []) {
|
|
3029
|
+
effectByName.set(s.name, s.id);
|
|
3030
|
+
}
|
|
3031
|
+
const expected = getExpectedFigmaNamesFromDS(data);
|
|
3032
|
+
const resolved = {};
|
|
3033
|
+
const allNames = /* @__PURE__ */ new Set([
|
|
3034
|
+
...expected.colorVariableNames,
|
|
3035
|
+
...expected.paintStyleNames,
|
|
3036
|
+
...expected.textStyleNames,
|
|
3037
|
+
...expected.effectStyleNames,
|
|
3038
|
+
...expected.numberVariableNames
|
|
3039
|
+
]);
|
|
3040
|
+
for (const name2 of allNames) {
|
|
3041
|
+
const entry = {};
|
|
3042
|
+
const vId = variableByName.get(name2);
|
|
3043
|
+
if (vId) entry.variableId = vId;
|
|
3044
|
+
const pId = paintByName.get(name2);
|
|
3045
|
+
if (pId) entry.paintStyleId = pId;
|
|
3046
|
+
const tId = textByName.get(name2);
|
|
3047
|
+
if (tId) entry.textStyleId = tId;
|
|
3048
|
+
const eId = effectByName.get(name2);
|
|
3049
|
+
if (eId) entry.effectStyleId = eId;
|
|
3050
|
+
if (vId || pId || tId || eId) resolved[name2] = entry;
|
|
3051
|
+
}
|
|
3052
|
+
return {
|
|
3053
|
+
content: [{ type: "text", text: JSON.stringify({ resolved }, null, 2) }]
|
|
3054
|
+
};
|
|
3055
|
+
} catch (e) {
|
|
3056
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3057
|
+
return {
|
|
3058
|
+
content: [{
|
|
3059
|
+
type: "text",
|
|
3060
|
+
text: JSON.stringify({ error: message }, null, 2)
|
|
3061
|
+
}],
|
|
3062
|
+
isError: true
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
case "getDesignScreenshot": {
|
|
3067
|
+
try {
|
|
3068
|
+
const reachable = await isBridgeReachable();
|
|
3069
|
+
if (!reachable) {
|
|
3070
|
+
return {
|
|
3071
|
+
content: [{
|
|
3072
|
+
type: "text",
|
|
3073
|
+
text: JSON.stringify({
|
|
3074
|
+
error: "Figma bridge not reachable.",
|
|
3075
|
+
hint: "Run the Atomix plugin in Figma and click Connect to Cursor, then retry."
|
|
3076
|
+
}, null, 2)
|
|
3077
|
+
}],
|
|
3078
|
+
isError: true
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
const frameId = args?.frameId;
|
|
3082
|
+
const scale = args?.scale;
|
|
3083
|
+
const params = { frameId };
|
|
3084
|
+
if (typeof scale === "number" && scale >= 1 && scale <= 4) params.scale = scale;
|
|
3085
|
+
const result = await sendBridgeRequest("get_design_screenshot", params);
|
|
3086
|
+
if (result.error) {
|
|
3087
|
+
return {
|
|
3088
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3089
|
+
isError: true
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
const content = [
|
|
3093
|
+
{ type: "text", text: `Screenshot captured (format: ${result.format ?? "PNG"}, scale: ${result.scale ?? 1}). Use the image below to verify layout, fill, and content hug.` }
|
|
3094
|
+
];
|
|
3095
|
+
if (result.imageBase64) {
|
|
3096
|
+
content.push({ type: "image", data: result.imageBase64, mimeType: "image/png" });
|
|
3097
|
+
}
|
|
3098
|
+
return { content };
|
|
3099
|
+
} catch (e) {
|
|
3100
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3101
|
+
return {
|
|
3102
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
|
|
3103
|
+
isError: true
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
case "finalizeDesignFrame": {
|
|
3108
|
+
try {
|
|
3109
|
+
const reachable = await isBridgeReachable();
|
|
3110
|
+
if (!reachable) {
|
|
3111
|
+
return {
|
|
3112
|
+
content: [{
|
|
3113
|
+
type: "text",
|
|
3114
|
+
text: JSON.stringify({
|
|
3115
|
+
error: "Figma bridge not reachable.",
|
|
3116
|
+
hint: "Run the Atomix plugin in Figma and click Connect to Cursor, then retry."
|
|
3117
|
+
}, null, 2)
|
|
3118
|
+
}],
|
|
3119
|
+
isError: true
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
const params = args && typeof args === "object" ? args : {};
|
|
3123
|
+
const result = await sendBridgeRequest("finalize_design_frame", params);
|
|
3124
|
+
return {
|
|
3125
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3126
|
+
};
|
|
3127
|
+
} catch (e) {
|
|
3128
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3129
|
+
return {
|
|
3130
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
|
|
3131
|
+
isError: true
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
case "designCreateFrame":
|
|
3136
|
+
case "designCreateText":
|
|
3137
|
+
case "designCreateRectangle":
|
|
3138
|
+
case "designSetAutoLayout":
|
|
3139
|
+
case "designSetLayoutConstraints":
|
|
3140
|
+
case "designAppendChild": {
|
|
3141
|
+
try {
|
|
3142
|
+
const reachable = await isBridgeReachable();
|
|
3143
|
+
if (!reachable) {
|
|
3144
|
+
return {
|
|
3145
|
+
content: [{
|
|
3146
|
+
type: "text",
|
|
3147
|
+
text: JSON.stringify({
|
|
3148
|
+
error: "Figma bridge not reachable.",
|
|
3149
|
+
hint: "Run the Atomix plugin in Figma and click Connect to Cursor, then retry."
|
|
3150
|
+
}, null, 2)
|
|
3151
|
+
}],
|
|
3152
|
+
isError: true
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
const method = normalizeBridgeMethod(name);
|
|
3156
|
+
const params = args && typeof args === "object" ? args : {};
|
|
3157
|
+
const result = await sendBridgeRequest(method, params);
|
|
3158
|
+
return {
|
|
3159
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3160
|
+
};
|
|
3161
|
+
} catch (e) {
|
|
3162
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3163
|
+
return {
|
|
3164
|
+
content: [{
|
|
3165
|
+
type: "text",
|
|
3166
|
+
text: JSON.stringify({ error: message }, null, 2)
|
|
3167
|
+
}],
|
|
3168
|
+
isError: true
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
2079
3172
|
default:
|
|
2080
3173
|
return {
|
|
2081
3174
|
content: [{
|
|
2082
3175
|
type: "text",
|
|
2083
3176
|
text: JSON.stringify({
|
|
2084
3177
|
error: `Unknown tool: ${name}`,
|
|
2085
|
-
availableTools: ["getToken", "listTokens", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "
|
|
3178
|
+
availableTools: ["getToken", "listTokens", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma", "getFigmaVariablesAndStyles", "createDesignPlaceholder", "resolveFigmaIdsForTokens", "designCreateFrame", "designCreateText", "designCreateRectangle", "designSetAutoLayout", "designSetLayoutConstraints", "designAppendChild", "getDesignScreenshot", "finalizeDesignFrame"]
|
|
2086
3179
|
}, null, 2)
|
|
2087
3180
|
}]
|
|
2088
3181
|
};
|
|
@@ -2099,7 +3192,7 @@ Last updated: ${lastUpdated}`
|
|
|
2099
3192
|
if (status === "404") {
|
|
2100
3193
|
suggestion = `Design system ID "${dsId}" not found. Verify the ID is correct or the design system has been published.`;
|
|
2101
3194
|
} else if (status === "401" || status === "403") {
|
|
2102
|
-
suggestion = "
|
|
3195
|
+
suggestion = "Add --atomix-token <your-token> from the Export modal or Settings \u2192 Regenerate.";
|
|
2103
3196
|
} else {
|
|
2104
3197
|
suggestion = `API request failed (${status}). Check your network connection and API base URL (${apiBase}).`;
|
|
2105
3198
|
}
|
|
@@ -2124,54 +3217,177 @@ Last updated: ${lastUpdated}`
|
|
|
2124
3217
|
};
|
|
2125
3218
|
}
|
|
2126
3219
|
});
|
|
3220
|
+
function injectSkillVersion(content, version, exportedAt) {
|
|
3221
|
+
const endOfFrontmatter = content.indexOf("\n---\n", 3);
|
|
3222
|
+
if (endOfFrontmatter === -1) return content;
|
|
3223
|
+
const before = content.slice(0, endOfFrontmatter);
|
|
3224
|
+
const after = content.slice(endOfFrontmatter);
|
|
3225
|
+
const versionLines = `atomixDsVersion: "${version}"
|
|
3226
|
+
atomixDsExportedAt: "${exportedAt ?? ""}"
|
|
3227
|
+
`;
|
|
3228
|
+
return before + "\n" + versionLines + after;
|
|
3229
|
+
}
|
|
2127
3230
|
var GENERIC_SKILL_MD = `---
|
|
2128
3231
|
name: atomix-ds
|
|
2129
|
-
description: Use
|
|
3232
|
+
description: Use the Atomix design system for UI, tokens, and styles. Fetch rules and tokens via MCP tools; never hardcode design values.
|
|
2130
3233
|
---
|
|
2131
3234
|
|
|
2132
3235
|
# Atomix Design System
|
|
2133
3236
|
|
|
3237
|
+
Use this skill when editing UI, design system files, or when the user asks to follow the project's design system. Your job is to **get the relevant data from the design system via MCP** and apply it\u2014not to guess or invent values.
|
|
3238
|
+
|
|
3239
|
+
## Goal
|
|
3240
|
+
|
|
3241
|
+
Complete the user's task using the design system as the single source of truth. Every color, spacing, typography, radius, shadow, or sizing value must come from the MCP tools (tokens or governance rules). Do not hardcode hex codes, pixel values, or font names.
|
|
3242
|
+
|
|
2134
3243
|
## When to use
|
|
2135
3244
|
|
|
2136
|
-
-
|
|
2137
|
-
- Working in design system or token files
|
|
2138
|
-
- User asks to follow the
|
|
3245
|
+
- Building or editing UI components, pages, or styles
|
|
3246
|
+
- Working in design system or token files (e.g. \`tokens.css\`, theme files)
|
|
3247
|
+
- User asks to "follow the design system", "use tokens", or "match the DS"
|
|
3248
|
+
- Validating or refactoring existing code, UI or visual design for token compliance
|
|
3249
|
+
|
|
3250
|
+
## How to get design system data
|
|
2139
3251
|
|
|
2140
|
-
|
|
3252
|
+
**1. Governance rules (how to use tokens in code)**
|
|
3253
|
+
Call **getAIToolRules** with the tool id for your current environment: \`cursor\`, \`windsurf\`, \`copilot\`, \`cline\`, \`continue\`, \`zed\`, or \`generic\`.
|
|
3254
|
+
Example: \`getAIToolRules({ tool: "cursor" })\`.
|
|
3255
|
+
Alternatively use the **/--rules** prompt or the resource \`atomix://rules/<tool>\`.
|
|
2141
3256
|
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
- **cline** (Cline)
|
|
2147
|
-
- **continue** (Continue)
|
|
2148
|
-
- **zed** (Zed)
|
|
2149
|
-
- **generic** (other tools)
|
|
3257
|
+
**2. Token values (what to use in code)**
|
|
3258
|
+
- **getToken(path)** \u2014 One token by path (e.g. \`colors.brand.primary\`, \`typography.fontSize.lg\`, \`sizing.icon.sm\`, \`icons.strokeWidth\`). Icon stroke width is at \`icons.strokeWidth\` when the design system defines it.
|
|
3259
|
+
- **listTokens(category)** \u2014 All tokens in a category: \`colors\`, \`typography\`, \`spacing\`, \`sizing\`, \`shadows\`, \`radius\`, \`borders\`, \`motion\`, \`zIndex\`. For icon config (e.g. stroke width), use \`getToken("icons.strokeWidth")\` since \`icons\` is not a list category.
|
|
3260
|
+
- **searchTokens(query)** \u2014 Find tokens by name or value.
|
|
2150
3261
|
|
|
2151
|
-
|
|
3262
|
+
**3. Validation**
|
|
3263
|
+
- **validateUsage(value, context)** \u2014 Check if a CSS/value should use a token instead (e.g. \`validateUsage("#007061", "color")\`).
|
|
2152
3264
|
|
|
2153
|
-
|
|
3265
|
+
**4. Syncing tokens to a file**
|
|
3266
|
+
- **syncAll({ output?, format?, skipTokens? })** \u2014 Syncs tokens to a file, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Default output \`./tokens.css\`, format \`css\`. Use \`skipTokens: true\` to only write skills and manifest.
|
|
2154
3267
|
|
|
2155
|
-
|
|
3268
|
+
Use the returned rules and token paths/values when generating or editing code. Prefer CSS variables (e.g. \`var(--atmx-*)\`) or the exact token references from the tools.
|
|
2156
3269
|
|
|
2157
|
-
|
|
3270
|
+
## Best practices
|
|
3271
|
+
|
|
3272
|
+
- **Fetch first:** Before writing UI or styles, call getAIToolRules and/or getToken/listTokens so you know the exact tokens and conventions.
|
|
3273
|
+
- **Icons:** Apply the design system's icon tokens when rendering icons: sizing via \`getToken("sizing.icon.sm")\` or \`listTokens("sizing")\`, and stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it; do not use hardcoded sizes or stroke widths.
|
|
3274
|
+
- **Typography:** Use typography tokens (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) from the DS for any text; build typesets (Display, Heading, body) from those tokens when creating global styles.
|
|
3275
|
+
- **No guessing:** If a value is not in the rules or token list, use searchTokens or listTokens to find the closest match rather than inventing a value.
|
|
3276
|
+
- **Version check:** If this skill file has frontmatter \`atomixDsVersion\`, compare it to the design system version from **getDependencies** (\`meta.designSystemVersion\`). If the design system is newer, suggest the user run **syncAll** to update skills and tokens.
|
|
3277
|
+
`;
|
|
3278
|
+
var FIGMA_DESIGN_SKILL_MD = `---
|
|
3279
|
+
name: atomix-design-in-figma
|
|
3280
|
+
description: Design in Figma using granular MCP commands. Three passes: wireframe, then tokens/rules, then audit. Use getDesignScreenshot to verify; finalizeDesignFrame to rename and remove placeholder.
|
|
3281
|
+
---
|
|
3282
|
+
|
|
3283
|
+
# Design in Figma (principal product designer)
|
|
3284
|
+
|
|
3285
|
+
Use this skill when the user asks to **design in Figma** (e.g. /--design-in-figma). Design by calling a **sequence of MCP tools**\u2014no script generation. All values come from the owner's Figma variables and styles (via **syncToFigma** and **resolveFigmaIdsForTokens**).
|
|
3286
|
+
|
|
3287
|
+
## Mandatory: no hardcoding
|
|
3288
|
+
|
|
3289
|
+
- **Only variable and style ids.** Use **resolveFigmaIdsForTokens** to get \`variableId\`, \`paintStyleId\`, \`textStyleId\`, \`effectStyleId\` keyed by Figma name (e.g. "Spacing / lg", "Background / surface"). Pass these ids into designCreateFrame, designCreateText, designSetAutoLayout, designSetLayoutConstraints. Never pass raw px, hex, or font sizes.
|
|
3290
|
+
|
|
3291
|
+
## Mandatory: user AI rules (colors, typography, buttons)
|
|
3292
|
+
|
|
3293
|
+
- **Colors:** Only variable/paint style ids from resolveFigmaIdsForTokens and getAIToolRules; no hex or raw values.
|
|
3294
|
+
- **Typography:** Only text style ids from resolved + rules; no raw font size/weight/family.
|
|
3295
|
+
- **Buttons:** Use token-based sizing and hierarchy from getAIToolRules; primary/secondary/ghost from resolved styles.
|
|
3296
|
+
|
|
3297
|
+
## Mandatory: set up variables first
|
|
3298
|
+
|
|
3299
|
+
- **Call syncToFigma first.** Then getAIToolRules, listTokens, and resolveFigmaIdsForTokens. Use the \`resolved\` map for every fill, text style, and spacing.
|
|
3300
|
+
|
|
3301
|
+
## Mandatory: auto-layout and breakpoints
|
|
3302
|
+
|
|
3303
|
+
- **Every frame with children:** Call **designSetAutoLayout** with \`nodeId\`, \`direction\`, \`paddingVariableId\`, \`itemSpacingVariableId\` from resolved. Use \`layoutSizingHorizontal\` / \`layoutSizingVertical\` (HUG or FILL) so content hugs or fills as intended.
|
|
3304
|
+
- **Breakpoints (non-mobile):** Use **designSetLayoutConstraints** with \`maxWidthVariableId\` from resolved for container frames.
|
|
3305
|
+
|
|
3306
|
+
## Three-pass flow (strict order)
|
|
3307
|
+
|
|
3308
|
+
**Setup (once):** syncToFigma \u2192 createDesignPlaceholder (save \`frameId\`) \u2192 getAIToolRules + listTokens \u2192 resolveFigmaIdsForTokens. Keep \`resolved\` and \`frameId\` for all passes.
|
|
3309
|
+
|
|
3310
|
+
**Pass 1 \u2014 Layout and contents (wireframe)**
|
|
3311
|
+
Build structure only: frames, text nodes, rectangles, auto-layout, layout constraints. Focus on hierarchy, spacing, and content placement; temporary or neutral fills are acceptable. Then call **getDesignScreenshot** with \`frameId\`. Check the image: layout correct? Content and frames hugging/filling as intended? If not, fix with design commands and take another screenshot until satisfied.
|
|
3312
|
+
|
|
3313
|
+
**Pass 2 \u2014 Apply design tokens and AI rules**
|
|
3314
|
+
Apply the owner's design system: set all fills to variable/paint ids from \`resolved\` (e.g. Background / surface, Brand / primary). Set all text to text style ids from \`resolved\`. Apply button and component tokens from getAIToolRules (colors, typography, buttons). Then call **getDesignScreenshot**. Check: do colors, typography, and buttons match the design system and rules? If not, fix and re-check.
|
|
3315
|
+
|
|
3316
|
+
**Pass 3 \u2014 Confirm no hardcoded values and follow AI rules**
|
|
3317
|
+
Audit: ensure no raw px, hex, or font values were introduced. Verify every fill, text style, and spacing uses an id from \`resolved\`. Check getAIToolRules again for colors, typography, buttons. Then call **getDesignScreenshot** one last time. If all good: call **finalizeDesignFrame** with \`frameId\`, \`name\` = short description of the design + " \u2705", and \`fillVariableId\` (or \`fillPaintStyleId\`) for the surface/background so the placeholder gray is removed. Then **summarise** what was built and any fixes made across passes.
|
|
3318
|
+
|
|
3319
|
+
Do not generate or run any JavaScript code. Use only the MCP tools listed above.
|
|
3320
|
+
`;
|
|
3321
|
+
var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
|
|
3322
|
+
<html lang="en">
|
|
3323
|
+
<head>
|
|
3324
|
+
<meta charset="UTF-8">
|
|
3325
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3326
|
+
<title>Setup complete \u2014 {{DS_NAME}}</title>
|
|
3327
|
+
{{FONT_LINK_TAG}}
|
|
3328
|
+
<link rel="stylesheet" href="{{TOKENS_CSS_PATH}}">
|
|
3329
|
+
<style>
|
|
3330
|
+
* { box-sizing: border-box; }
|
|
3331
|
+
body {
|
|
3332
|
+
margin: 0;
|
|
3333
|
+
font-family: {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
|
|
3334
|
+
background: {{BRAND_PRIMARY_VAR}};
|
|
3335
|
+
color: {{BRAND_PRIMARY_FOREGROUND_VAR}};
|
|
3336
|
+
min-height: 100vh;
|
|
3337
|
+
padding: 2rem;
|
|
3338
|
+
display: flex;
|
|
3339
|
+
align-items: center;
|
|
3340
|
+
justify-content: center;
|
|
3341
|
+
text-align: center;
|
|
3342
|
+
}
|
|
3343
|
+
.wrap { width: 375px; max-width: 100%; }
|
|
3344
|
+
.icon { width: 2rem; height: 2rem; margin: 0 auto 1rem; }
|
|
3345
|
+
h1 {
|
|
3346
|
+
font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
|
|
3347
|
+
font-size: clamp(1.5rem, 4vw, 2.25rem);
|
|
3348
|
+
font-weight: 700;
|
|
3349
|
+
margin: 0 0 0.75rem;
|
|
3350
|
+
line-height: 1.25;
|
|
3351
|
+
}
|
|
3352
|
+
.lead { margin: 0 0 1.5rem; font-size: 1rem; line-height: 1.5; opacity: 0.95; }
|
|
3353
|
+
.now { margin: 1.5rem 0 0; font-size: 0.875rem; line-height: 1.6; opacity: 0.95; text-align: left; }
|
|
3354
|
+
.now strong { display: block; margin-bottom: 0.5rem; }
|
|
3355
|
+
.now ul { margin: 0; padding-left: 1.25rem; }
|
|
3356
|
+
.tips { margin-top: 1.5rem; font-size: 0.875rem; line-height: 1.6; opacity: 0.9; }
|
|
3357
|
+
.tips a { color: inherit; text-decoration: underline; }
|
|
3358
|
+
</style>
|
|
3359
|
+
</head>
|
|
3360
|
+
<body>
|
|
3361
|
+
<div class="wrap">
|
|
3362
|
+
<div class="icon" aria-hidden="true">
|
|
3363
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="32" height="32"><path d="M20 6L9 17l-5-5"/></svg>
|
|
3364
|
+
</div>
|
|
3365
|
+
<h1>You're all set with {{DS_NAME}}</h1>
|
|
3366
|
+
<p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon. Token file is linked above.</p>
|
|
3367
|
+
<div class="now">
|
|
3368
|
+
<strong>What you can do now:</strong>
|
|
3369
|
+
<ul>
|
|
3370
|
+
<li>Ask your agent to build your designs using the design system tokens</li>
|
|
3371
|
+
<li>Build components and pages that use <code>var(--atmx-*)</code> for colors, spacing, and typography</li>
|
|
3372
|
+
<li>Run <code>/--rules</code> to load governance rules; run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
|
|
3373
|
+
</ul>
|
|
3374
|
+
</div>
|
|
3375
|
+
<p class="tips">Keep the source of truth at <a href="https://atomixstudio.eu" target="_blank" rel="noopener">atomixstudio.eu</a> \u2014 avoid editing token values in this repo.</p>
|
|
3376
|
+
</div>
|
|
3377
|
+
</body>
|
|
3378
|
+
</html>
|
|
2158
3379
|
`;
|
|
2159
3380
|
var AI_TOOLS = ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic"];
|
|
2160
3381
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
3382
|
+
if (!hasValidAuthConfig() || authFailedNoTools) {
|
|
3383
|
+
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
3384
|
+
}
|
|
2161
3385
|
try {
|
|
2162
3386
|
await fetchDesignSystemForMCP();
|
|
2163
3387
|
return { resources: [] };
|
|
2164
3388
|
} catch {
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
{
|
|
2168
|
-
uri: "atomix://setup",
|
|
2169
|
-
name: "Configure MCP",
|
|
2170
|
-
description: "Add --ds-id to your MCP config to load design system resources",
|
|
2171
|
-
mimeType: "text/markdown"
|
|
2172
|
-
}
|
|
2173
|
-
]
|
|
2174
|
-
};
|
|
3389
|
+
authFailedNoTools = true;
|
|
3390
|
+
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
2175
3391
|
}
|
|
2176
3392
|
});
|
|
2177
3393
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
@@ -2183,7 +3399,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
2183
3399
|
mimeType: "text/markdown",
|
|
2184
3400
|
text: `# Configure Atomix MCP
|
|
2185
3401
|
|
|
2186
|
-
Add \`--ds-id
|
|
3402
|
+
Add \`--ds-id\` and \`--atomix-token\` to your MCP server config (both required).
|
|
2187
3403
|
|
|
2188
3404
|
Example (\`.cursor/mcp.json\`):
|
|
2189
3405
|
\`\`\`json
|
|
@@ -2191,16 +3407,19 @@ Example (\`.cursor/mcp.json\`):
|
|
|
2191
3407
|
"mcpServers": {
|
|
2192
3408
|
"atomix": {
|
|
2193
3409
|
"command": "npx",
|
|
2194
|
-
"args": ["@atomixstudio/mcp@latest", "--ds-id", "<your-ds-id>"]
|
|
3410
|
+
"args": ["@atomixstudio/mcp@latest", "--ds-id", "<your-ds-id>", "--atomix-token", "<your-token>"]
|
|
2195
3411
|
}
|
|
2196
3412
|
}
|
|
2197
3413
|
}
|
|
2198
3414
|
\`\`\`
|
|
2199
3415
|
|
|
2200
|
-
Get your DS ID from
|
|
3416
|
+
Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Atomix access token.`
|
|
2201
3417
|
}]
|
|
2202
3418
|
};
|
|
2203
3419
|
}
|
|
3420
|
+
if (!hasValidAuthConfig() || authFailedNoTools) {
|
|
3421
|
+
throw new Error("MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart Cursor. See atomix://setup for instructions.");
|
|
3422
|
+
}
|
|
2204
3423
|
const data = await fetchDesignSystemForMCP();
|
|
2205
3424
|
const stats = getTokenStats(data);
|
|
2206
3425
|
if (uri === "atomix://hello") {
|
|
@@ -2237,32 +3456,49 @@ Get your DS ID from: https://atomixstudio.eu/ds/[your-ds-id]`
|
|
|
2237
3456
|
}
|
|
2238
3457
|
throw new Error(`Unknown resource: ${uri}`);
|
|
2239
3458
|
});
|
|
3459
|
+
var FIGMA_PROMPT_NAMES = /* @__PURE__ */ new Set(["--sync-to-figma", "--design-in-figma"]);
|
|
2240
3460
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
2241
|
-
|
|
3461
|
+
if (!hasValidAuthConfig()) {
|
|
3462
|
+
authFailedNoTools = true;
|
|
3463
|
+
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
3464
|
+
}
|
|
3465
|
+
if (cachedMcpTier === null && !authFailedNoTools) {
|
|
3466
|
+
try {
|
|
3467
|
+
await fetchDesignSystemForMCP(true);
|
|
3468
|
+
} catch {
|
|
3469
|
+
authFailedNoTools = true;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
if (authFailedNoTools) {
|
|
3473
|
+
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
3474
|
+
}
|
|
3475
|
+
const allPrompts = [
|
|
2242
3476
|
{ name: "--hello", description: "Get started with this design system - overview, tokens, and tools. Run this first!" },
|
|
2243
|
-
{
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
]
|
|
2250
|
-
},
|
|
2251
|
-
{
|
|
2252
|
-
name: "--rules",
|
|
2253
|
-
description: "Get the design system governance rules for your AI coding tool",
|
|
2254
|
-
arguments: [
|
|
2255
|
-
{ name: "tool", description: "AI tool (cursor, copilot, windsurf, cline, continue, zed, generic). Optional.", required: false }
|
|
2256
|
-
]
|
|
2257
|
-
},
|
|
2258
|
-
{ name: "--sync", description: "Sync tokens to a local file. Safe: adds new, updates existing, marks deprecated. Use /--refactor to migrate." },
|
|
2259
|
-
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." }
|
|
3477
|
+
{ name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
|
|
3478
|
+
{ name: "--rules", description: "Get the design system governance rules for your AI coding tool (default: cursor)." },
|
|
3479
|
+
{ name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
|
|
3480
|
+
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
|
|
3481
|
+
{ name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." },
|
|
3482
|
+
{ name: "--design-in-figma", description: "Design UI in Figma: 3 passes (wireframe \u2192 tokens/rules \u2192 audit), getDesignScreenshot after each pass, then finalizeDesignFrame (rename + \u2705, remove placeholder) and summarise." }
|
|
2260
3483
|
];
|
|
3484
|
+
const prompts = cachedMcpTier === "pro" ? allPrompts : allPrompts.filter((p) => !FIGMA_PROMPT_NAMES.has(p.name));
|
|
2261
3485
|
return { prompts };
|
|
2262
3486
|
});
|
|
2263
3487
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
2264
3488
|
const { name, arguments: args } = request.params;
|
|
2265
|
-
|
|
3489
|
+
if (!hasValidAuthConfig() || authFailedNoTools) {
|
|
3490
|
+
return {
|
|
3491
|
+
description: "MCP Server Configuration Required",
|
|
3492
|
+
messages: [{
|
|
3493
|
+
role: "user",
|
|
3494
|
+
content: {
|
|
3495
|
+
type: "text",
|
|
3496
|
+
text: "MCP access requires valid --ds-id and --atomix-token. Add both to your MCP config and restart Cursor. No tools or prompts are available until then."
|
|
3497
|
+
}
|
|
3498
|
+
}]
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
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" ? "design-in-figma" : name;
|
|
2266
3502
|
const shouldForceRefresh = canonicalName === "sync";
|
|
2267
3503
|
let data = null;
|
|
2268
3504
|
let stats = null;
|
|
@@ -2280,12 +3516,10 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
2280
3516
|
content: {
|
|
2281
3517
|
type: "text",
|
|
2282
3518
|
text: `The MCP server isn't configured. To use design system prompts, you need to configure the server with:
|
|
2283
|
-
- \`--ds-id\`: Your design system ID (
|
|
2284
|
-
- \`--
|
|
2285
|
-
|
|
2286
|
-
**Note:** Most design systems are public and don't require an API key. Only add \`--api-key\` if your design system is private.
|
|
3519
|
+
- \`--ds-id\`: Your design system ID (get it from https://atomixstudio.eu/ds/[your-ds-id])
|
|
3520
|
+
- \`--atomix-token\`: Your access token (get it from the Export modal or Settings \u2192 Regenerate Atomix access token)
|
|
2287
3521
|
|
|
2288
|
-
Configure the MCP server in your Cursor settings, then restart Cursor.`
|
|
3522
|
+
Both are required. Configure the MCP server in your Cursor settings, then restart Cursor.`
|
|
2289
3523
|
}
|
|
2290
3524
|
}
|
|
2291
3525
|
]
|
|
@@ -2316,12 +3550,12 @@ Configure the MCP server in your Cursor settings, then restart Cursor.`
|
|
|
2316
3550
|
} else if (status === "401" || status === "403") {
|
|
2317
3551
|
helpText += `**Possible causes:**
|
|
2318
3552
|
`;
|
|
2319
|
-
helpText += `-
|
|
3553
|
+
helpText += `- Missing or invalid access token
|
|
2320
3554
|
`;
|
|
2321
|
-
helpText += `-
|
|
3555
|
+
helpText += `- You don't have access to this design system (owner or invited guest only)
|
|
2322
3556
|
|
|
2323
3557
|
`;
|
|
2324
|
-
helpText += `**Solution:** Add \`--
|
|
3558
|
+
helpText += `**Solution:** Add \`--atomix-token <your-token>\` from the Export modal or Settings \u2192 Regenerate.`;
|
|
2325
3559
|
} else {
|
|
2326
3560
|
helpText += `**Possible causes:**
|
|
2327
3561
|
`;
|
|
@@ -2349,6 +3583,20 @@ Configure the MCP server in your Cursor settings, then restart Cursor.`
|
|
|
2349
3583
|
}
|
|
2350
3584
|
throw error;
|
|
2351
3585
|
}
|
|
3586
|
+
if (FIGMA_PROMPT_NAMES.has(name) && cachedMcpTier !== "pro") {
|
|
3587
|
+
return {
|
|
3588
|
+
description: "Pro Figma required",
|
|
3589
|
+
messages: [
|
|
3590
|
+
{
|
|
3591
|
+
role: "user",
|
|
3592
|
+
content: {
|
|
3593
|
+
type: "text",
|
|
3594
|
+
text: "This design system does not have Pro Figma access. Figma sync and design prompts are available when the design system owner has a Pro subscription."
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
]
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
2352
3600
|
const buildCategoryPrompt = (category, instructions) => {
|
|
2353
3601
|
const lines = [];
|
|
2354
3602
|
lines.push(`## Design System Information`);
|
|
@@ -2568,19 +3816,61 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
2568
3816
|
case "sync": {
|
|
2569
3817
|
const output = args?.output || "./tokens.css";
|
|
2570
3818
|
const format = args?.format || "css";
|
|
2571
|
-
|
|
2572
|
-
description:
|
|
3819
|
+
return {
|
|
3820
|
+
description: "Sync tokens, rules, skills, and dependencies manifest",
|
|
2573
3821
|
messages: [
|
|
2574
3822
|
{
|
|
2575
3823
|
role: "user",
|
|
2576
3824
|
content: {
|
|
2577
3825
|
type: "text",
|
|
2578
|
-
text: `Call the
|
|
3826
|
+
text: `Call the syncAll tool now. Use output="${output}" and format="${format}". This syncs tokens, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Execute immediately - do not search or ask questions.`
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
]
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
case "sync-to-figma": {
|
|
3833
|
+
return {
|
|
3834
|
+
description: "Push design system to Figma via MCP tool",
|
|
3835
|
+
messages: [
|
|
3836
|
+
{
|
|
3837
|
+
role: "user",
|
|
3838
|
+
content: {
|
|
3839
|
+
type: "text",
|
|
3840
|
+
text: `Call the MCP tool **syncToFigma** now (no arguments). It pushes the design system into the open Figma file via the built-in bridge and Atomix plugin. Do not use the Figma REST API or external scripts. If the response includes \`bridgeNotRunning\` and \`agentInstruction\`, ensure Cursor has this MCP server running, then in Figma run the Atomix plugin and click Connect to Cursor, then call syncToFigma again. Only if that fails, tell the user: (1) Ensure Cursor has this MCP configured and running. (2) In Figma, open and run the Atomix plugin, then tap **Connect to Cursor**. (3) Run Sync to Figma again.`
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
]
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
case "design-in-figma": {
|
|
3847
|
+
return {
|
|
3848
|
+
description: "Design in Figma using granular MCP commands: 3 passes (wireframe \u2192 tokens \u2192 audit), screenshot checks, then finalize and summarise",
|
|
3849
|
+
messages: [
|
|
3850
|
+
{
|
|
3851
|
+
role: "user",
|
|
3852
|
+
content: {
|
|
3853
|
+
type: "text",
|
|
3854
|
+
text: `You must **design in Figma** by calling a **sequence of MCP tools**\u2014do not generate or run any JavaScript code. Follow the design-in-Figma skill if present; otherwise follow this **three-pass flow**.
|
|
3855
|
+
|
|
3856
|
+
Prerequisites: Figma bridge runs inside this MCP server. In Figma, run the Atomix plugin and click "Connect to Cursor"; leave the plugin open.
|
|
3857
|
+
|
|
3858
|
+
**Setup (once):** syncToFigma \u2192 createDesignPlaceholder (save \`frameId\`) \u2192 getAIToolRules + listTokens \u2192 resolveFigmaIdsForTokens. Use the returned \`resolved\` map and \`frameId\` for all passes.
|
|
3859
|
+
|
|
3860
|
+
**Pass 1 \u2014 Layout and contents (wireframe)**
|
|
3861
|
+
Build the structure: designCreateFrame, designCreateText, designCreateRectangle, designSetAutoLayout (use paddingVariableId, itemSpacingVariableId, and layoutSizingHorizontal/layoutSizingVertical HUG or FILL from resolved), designSetLayoutConstraints, designAppendChild. Focus on hierarchy, spacing, and content placement. Then call **getDesignScreenshot** with \`frameId\`. Check the returned image: is layout correct? Do elements hug or fill as intended? If not, fix and take another screenshot until satisfied.
|
|
3862
|
+
|
|
3863
|
+
**Pass 2 \u2014 Apply design tokens and AI rules**
|
|
3864
|
+
Apply the owner's design system: set all fills and text styles using only ids from \`resolved\` (no raw px/hex). Strictly follow getAIToolRules for **colors, typography, and buttons**. Then call **getDesignScreenshot**. Check: do colors, typography, and buttons match the design system? If not, fix and re-check.
|
|
3865
|
+
|
|
3866
|
+
**Pass 3 \u2014 Confirm no hardcoded values**
|
|
3867
|
+
Audit: ensure no raw px, hex, or font values remain; every fill and text style must use an id from \`resolved\`. Call **getDesignScreenshot** once more. If everything is correct: call **finalizeDesignFrame** with \`frameId\`, \`name\` = a short description of the design + " \u2705", and \`fillVariableId\` (or \`fillPaintStyleId\`) from resolved for the surface/background so the placeholder gray is removed. Then **summarise** what was built and any fixes made across the three passes.
|
|
3868
|
+
|
|
3869
|
+
If the bridge is not reachable: tell the user to run the Atomix plugin in Figma and click Connect to Cursor, then retry.`
|
|
2579
3870
|
}
|
|
2580
3871
|
}
|
|
2581
3872
|
]
|
|
2582
3873
|
};
|
|
2583
|
-
return response;
|
|
2584
3874
|
}
|
|
2585
3875
|
case "refactor": {
|
|
2586
3876
|
if (!lastSyncAffectedTokens) {
|
|
@@ -2694,49 +3984,46 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
2694
3984
|
};
|
|
2695
3985
|
}
|
|
2696
3986
|
case "atomix-setup": {
|
|
2697
|
-
const setupInstructions = `You are running
|
|
3987
|
+
const setupInstructions = `You are running **/--get-started** (get started with design system). Three phases only.
|
|
2698
3988
|
|
|
2699
|
-
**Rule:** Do not create, write, or modify any file until Phase 3 and only after the user has explicitly approved (e.g.
|
|
3989
|
+
**Rule:** Do not create, write, or modify any file until Phase 3 and only after the user has explicitly approved (e.g. "Yes", "Yes for all", "Go ahead").
|
|
2700
3990
|
|
|
2701
3991
|
## Todo list (required)
|
|
2702
3992
|
|
|
2703
|
-
-
|
|
2704
|
-
- Work **step by step**: complete Phase 1, then Phase 2; do not run Phase 3 until the user has replied with approval.
|
|
2705
|
-
- Mark todo items completed as you finish each phase. Use this pattern so the user and you can track progress.
|
|
3993
|
+
- Create a todo list at the start: Phase 1, Phase 2, Phase 3. Work step by step; do not run Phase 3 until the user has replied with approval. Mark items completed as you go.
|
|
2706
3994
|
|
|
2707
3995
|
## Phase 1 \u2013 Scan
|
|
2708
3996
|
|
|
2709
|
-
- Resolve platform/stack: infer from the project (e.g. package.json, build.gradle, Xcode) or ask
|
|
2710
|
-
- Call
|
|
2711
|
-
- Scan the repo for
|
|
2712
|
-
- **
|
|
2713
|
-
- **Native (iOS/Android):** Scan for existing theme or style files (e.g. SwiftUI theme/asset catalog, Android themes.xml/styles.xml, Compose theme). Note whether the project already has a theme or style setup.
|
|
2714
|
-
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Only include: icon package, fonts, skill (.cursor/skills/atomix-ds/SKILL.md), token files. No MCP.
|
|
3997
|
+
- 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.
|
|
3998
|
+
- Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
|
|
3999
|
+
- Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/design-in-figma.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).
|
|
4000
|
+
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md), **Figma design skill** (.cursor/skills/atomix-ds/design-in-figma.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
|
|
2715
4001
|
- Do not write, create, or add anything in Phase 1.
|
|
2716
4002
|
|
|
2717
|
-
## Phase 2 \u2013 Report and ask
|
|
4003
|
+
## Phase 2 \u2013 Report and ask
|
|
2718
4004
|
|
|
2719
4005
|
- Reply with: **Suggested:** [list] and **Already present:** [list].
|
|
2720
|
-
- Ask exactly once: "Do you want me to add the suggested items? (Yes for all, or say which ones.)"
|
|
2721
|
-
- **Stop.** Do not run Phase 3 in this response.
|
|
4006
|
+
- Ask exactly once: "Do you want me to add the suggested items? (Yes for all, or say which ones.)"
|
|
4007
|
+
- **Stop.** Wait for the user to reply. Do not run Phase 3 in this response.
|
|
2722
4008
|
|
|
2723
|
-
## Phase 3 \u2013 Create
|
|
4009
|
+
## Phase 3 \u2013 Create (only after user approval)
|
|
2724
4010
|
|
|
2725
4011
|
- Run only when the user has said yes (all or specific items).
|
|
2726
4012
|
- For each approved item:
|
|
2727
|
-
- **Skill:** Write the skill content from getDependencies to .cursor/skills/atomix-ds/SKILL.md.
|
|
2728
|
-
- **
|
|
2729
|
-
- **
|
|
2730
|
-
-
|
|
2731
|
-
- **
|
|
2732
|
-
- **
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
4013
|
+
- **Skill:** Write the skill content from getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md).
|
|
4014
|
+
- **Figma design skill:** Write the skill content from getDependencies \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` (.cursor/skills/atomix-ds/design-in-figma.md). Use this when designing in Figma so the agent follows principal-product-designer rules and prefers existing Figma variables.
|
|
4015
|
+
- **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css"). syncAll also writes skills and atomix-dependencies.json. You must call syncAll; do not only suggest the user run it later.
|
|
4016
|
+
- **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.
|
|
4017
|
+
- **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **typeset** in CSS: use **getToken** / **listTokens** (category \`typography\`) to get fontFamily, fontSize, fontWeight, lineHeight, letterSpacing for display, heading, and body, and write CSS rules (e.g. \`.typeset-display\`, \`.typeset-heading\`, \`.typeset-body\`, or \`h1\`/\`h2\`/\`p\`) that set those properties to \`var(--atmx-typography-*)\`. The typeset file (or section) must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
|
|
4018
|
+
- **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1), FONT_FAMILY_VAR (body), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
|
|
4019
|
+
- Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
|
|
4020
|
+
- **After reporting \u2013 styles/theme:**
|
|
4021
|
+
- **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014CSS classes or element rules that set font-family, font-size, font-weight, line-height, letter-spacing using \`var(--atmx-typography-*)\` from the token file (e.g. \`.typeset-display\`, \`.typeset-heading\`, \`.typeset-body\`). You must call getToken/listTokens to get the exact typography token paths and write the corresponding var() references. The output must not be only a font import; it must define the full typeset (Display, Heading, body) with every style detail from the design system.
|
|
4022
|
+
- **iOS/Android:** If the project already has theme/style files: recommend how to integrate Atomix tokens. Do not suggest a new global theme. Only if there is **no** theme/style at all, ask once: "There's no theme/style setup yet. Do you want a minimal token-based theme?" and add only if the user says yes.
|
|
2736
4023
|
|
|
2737
|
-
Create your todo list first, then
|
|
4024
|
+
Create your todo list first, then Phase 1 (resolve platform/stack, call getDependencies, scan, build lists), then Phase 2 (report and ask). Do not perform Phase 3 until the user replies.`;
|
|
2738
4025
|
return {
|
|
2739
|
-
description: "
|
|
4026
|
+
description: "Get started with design system in project (/--get-started). Create todo list; Phase 1 scan, Phase 2 report and ask, Phase 3 create only after user approval.",
|
|
2740
4027
|
messages: [
|
|
2741
4028
|
{
|
|
2742
4029
|
role: "user",
|
|
@@ -2807,12 +4094,12 @@ ${tokenSummary}
|
|
|
2807
4094
|
| Command | What to expect |
|
|
2808
4095
|
|---------|----------------|
|
|
2809
4096
|
| **/--hello** | Get started - overview, tokens, and tools. Run this first! |
|
|
2810
|
-
| **/--
|
|
4097
|
+
| **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
|
|
2811
4098
|
| **/--rules** | Governance rules for your AI tool (Cursor, Copilot, Windsurf, etc.). |
|
|
2812
|
-
| **/--sync** | Sync tokens
|
|
4099
|
+
| **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
|
|
2813
4100
|
| **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
|
|
2814
4101
|
|
|
2815
|
-
**Suggested next step:** Run **/--
|
|
4102
|
+
**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.
|
|
2816
4103
|
|
|
2817
4104
|
---
|
|
2818
4105
|
|
|
@@ -2822,17 +4109,32 @@ ${tokenSummary}
|
|
|
2822
4109
|
async function startServer() {
|
|
2823
4110
|
if (!dsId) {
|
|
2824
4111
|
console.error("Error: Missing --ds-id argument");
|
|
2825
|
-
console.error("Usage: npx
|
|
2826
|
-
console.error("
|
|
4112
|
+
console.error("Usage: npx @atomixstudio/mcp@latest --ds-id <id> --atomix-token <token>");
|
|
4113
|
+
console.error("Get your DS ID and Atomix access token from account settings.");
|
|
2827
4114
|
console.error("");
|
|
2828
|
-
console.error("For sync command: npx atomix sync --help");
|
|
2829
|
-
console.error("Get your DS ID from https://atomixstudio.eu/ds/[your-ds-id]");
|
|
2830
4115
|
process.exit(1);
|
|
2831
4116
|
}
|
|
4117
|
+
if (!accessToken) {
|
|
4118
|
+
console.error("Error: Missing --atomix-token argument");
|
|
4119
|
+
console.error("Usage: npx @atomixstudio/mcp@latest --ds-id <id> --atomix-token <token>");
|
|
4120
|
+
console.error("Get your DS ID and Atomix access token from account settings.");
|
|
4121
|
+
console.error("");
|
|
4122
|
+
process.exit(1);
|
|
4123
|
+
}
|
|
4124
|
+
startFigmaBridge();
|
|
2832
4125
|
const transport = new StdioServerTransport();
|
|
2833
4126
|
await server.connect(transport);
|
|
2834
4127
|
console.error(`Atomix MCP Server started for design system: ${dsId}`);
|
|
4128
|
+
console.error(`Atomix MCP API base: ${apiBase}`);
|
|
4129
|
+
console.error(
|
|
4130
|
+
"If you switched MCP config (e.g. free vs pro DS), restart Cursor so this process uses the new --ds-id and --atomix-token."
|
|
4131
|
+
);
|
|
4132
|
+
}
|
|
4133
|
+
function onShutdown() {
|
|
4134
|
+
closeFigmaBridge();
|
|
2835
4135
|
}
|
|
4136
|
+
process.on("SIGINT", onShutdown);
|
|
4137
|
+
process.on("SIGTERM", onShutdown);
|
|
2836
4138
|
async function main() {
|
|
2837
4139
|
await startServer();
|
|
2838
4140
|
}
|