@atomixstudio/mcp 1.0.28 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +326 -794
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1288,183 +1288,7 @@ function getTokenStats(data) {
|
|
|
1288
1288
|
};
|
|
1289
1289
|
}
|
|
1290
1290
|
|
|
1291
|
-
//
|
|
1292
|
-
import * as path2 from "path";
|
|
1293
|
-
import * as fs2 from "fs";
|
|
1294
|
-
import { execSync } from "child_process";
|
|
1295
|
-
import { platform } from "os";
|
|
1296
|
-
import WebSocket, { WebSocketServer } from "ws";
|
|
1297
|
-
var FIGMA_BRIDGE_PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 8765;
|
|
1298
|
-
var FIGMA_BRIDGE_HOST = process.env.FIGMA_BRIDGE_HOST || "127.0.0.1";
|
|
1299
|
-
var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
|
|
1300
|
-
var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
|
|
1301
|
-
var FIGMA_CONNECTION_INSTRUCTIONS = {
|
|
1302
|
-
installAndRun: "In Figma: Open Plugins and run the Atomix plugin (Atomix Token Extractor). If it's not installed yet, install it from the Figma Community or your team's plugin library, then run it.",
|
|
1303
|
-
connect: 'In the plugin UI, tap **Connect** and wait until the status shows "Connected".',
|
|
1304
|
-
startBridge: "The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server running (e.g. in MCP settings), then in Figma run the Atomix plugin and tap Connect."
|
|
1305
|
-
};
|
|
1306
|
-
var bridgeWss = null;
|
|
1307
|
-
var pluginWs = null;
|
|
1308
|
-
var pendingBridgeRequests = /* @__PURE__ */ new Map();
|
|
1309
|
-
function ensureFigmaBridgePortFree(port) {
|
|
1310
|
-
const portStr = String(port);
|
|
1311
|
-
const ourPid = String(process.pid);
|
|
1312
|
-
try {
|
|
1313
|
-
if (platform() === "win32") {
|
|
1314
|
-
const out = execSync(`netstat -ano`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1315
|
-
const pids = /* @__PURE__ */ new Set();
|
|
1316
|
-
for (const line of out.split(/\r?\n/)) {
|
|
1317
|
-
if (line.includes(`:${portStr}`) && line.includes("LISTENING")) {
|
|
1318
|
-
const parts = line.trim().split(/\s+/);
|
|
1319
|
-
const pid = parts[parts.length - 1];
|
|
1320
|
-
if (/^\d+$/.test(pid) && pid !== ourPid) pids.add(pid);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
for (const pid of pids) {
|
|
1324
|
-
try {
|
|
1325
|
-
execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1326
|
-
console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
|
|
1327
|
-
} catch (_) {
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
} else {
|
|
1331
|
-
const out = execSync(`lsof -ti :${portStr}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1332
|
-
if (!out) return;
|
|
1333
|
-
const pids = out.split(/\s+/).filter((p) => p && p !== ourPid);
|
|
1334
|
-
for (const pid of pids) {
|
|
1335
|
-
try {
|
|
1336
|
-
execSync(`kill -9 ${pid}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1337
|
-
console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
|
|
1338
|
-
} catch (_) {
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
} catch {
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
function startFigmaBridge() {
|
|
1346
|
-
if (bridgeWss) return;
|
|
1347
|
-
try {
|
|
1348
|
-
ensureFigmaBridgePortFree(FIGMA_BRIDGE_PORT);
|
|
1349
|
-
bridgeWss = new WebSocketServer({
|
|
1350
|
-
host: FIGMA_BRIDGE_HOST,
|
|
1351
|
-
port: FIGMA_BRIDGE_PORT,
|
|
1352
|
-
clientTracking: true
|
|
1353
|
-
});
|
|
1354
|
-
bridgeWss.on("connection", (ws, req) => {
|
|
1355
|
-
const url = req.url || "";
|
|
1356
|
-
const params = new URLSearchParams(url.startsWith("/") ? url.slice(1) : url);
|
|
1357
|
-
const token = params.get("token");
|
|
1358
|
-
const role = params.get("role");
|
|
1359
|
-
if (FIGMA_BRIDGE_TOKEN && token !== FIGMA_BRIDGE_TOKEN) {
|
|
1360
|
-
ws.close(4003, "Invalid or missing bridge token");
|
|
1361
|
-
return;
|
|
1362
|
-
}
|
|
1363
|
-
if (role !== "plugin") {
|
|
1364
|
-
ws.close(4002, "Only role=plugin is accepted (bridge runs in MCP server)");
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
if (pluginWs) {
|
|
1368
|
-
try {
|
|
1369
|
-
pluginWs.close();
|
|
1370
|
-
} catch (_) {
|
|
1371
|
-
}
|
|
1372
|
-
pluginWs = null;
|
|
1373
|
-
}
|
|
1374
|
-
pluginWs = ws;
|
|
1375
|
-
ws.on("message", (raw) => {
|
|
1376
|
-
const text = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
1377
|
-
let msg;
|
|
1378
|
-
try {
|
|
1379
|
-
msg = JSON.parse(text);
|
|
1380
|
-
} catch {
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
const parsed = msg;
|
|
1384
|
-
if (parsed?.type === "ping" && typeof parsed.id === "string") {
|
|
1385
|
-
try {
|
|
1386
|
-
ws.send(JSON.stringify({ type: "pong", id: parsed.id }));
|
|
1387
|
-
} catch (_) {
|
|
1388
|
-
}
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
if (typeof parsed.id === "string" && ("result" in parsed || "error" in parsed)) {
|
|
1392
|
-
const pending = pendingBridgeRequests.get(parsed.id);
|
|
1393
|
-
if (pending) {
|
|
1394
|
-
clearTimeout(pending.timeout);
|
|
1395
|
-
pendingBridgeRequests.delete(parsed.id);
|
|
1396
|
-
if (parsed.error) pending.reject(new Error(parsed.error));
|
|
1397
|
-
else pending.resolve(parsed.result);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
});
|
|
1401
|
-
ws.on("close", () => {
|
|
1402
|
-
if (pluginWs === ws) pluginWs = null;
|
|
1403
|
-
});
|
|
1404
|
-
ws.on("error", () => {
|
|
1405
|
-
if (pluginWs === ws) pluginWs = null;
|
|
1406
|
-
});
|
|
1407
|
-
});
|
|
1408
|
-
bridgeWss.on("listening", () => {
|
|
1409
|
-
console.error(`[atomix-mcp] Figma bridge listening on ws://${FIGMA_BRIDGE_HOST}:${FIGMA_BRIDGE_PORT} (local only)`);
|
|
1410
|
-
if (FIGMA_BRIDGE_TOKEN) {
|
|
1411
|
-
console.error("[atomix-mcp] Figma bridge token required (FIGMA_BRIDGE_TOKEN)");
|
|
1412
|
-
}
|
|
1413
|
-
});
|
|
1414
|
-
bridgeWss.on("error", (err) => {
|
|
1415
|
-
console.error("[atomix-mcp] Figma bridge server error:", err);
|
|
1416
|
-
});
|
|
1417
|
-
} catch (err) {
|
|
1418
|
-
console.error("[atomix-mcp] Failed to start Figma bridge:", err);
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
function closeFigmaBridge() {
|
|
1422
|
-
if (pluginWs) {
|
|
1423
|
-
try {
|
|
1424
|
-
pluginWs.close();
|
|
1425
|
-
} catch (_) {
|
|
1426
|
-
}
|
|
1427
|
-
pluginWs = null;
|
|
1428
|
-
}
|
|
1429
|
-
if (bridgeWss) {
|
|
1430
|
-
try {
|
|
1431
|
-
bridgeWss.close();
|
|
1432
|
-
} catch (_) {
|
|
1433
|
-
}
|
|
1434
|
-
bridgeWss = null;
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
function isBridgeReachable() {
|
|
1438
|
-
return Promise.resolve(!!(pluginWs && pluginWs.readyState === WebSocket.OPEN));
|
|
1439
|
-
}
|
|
1440
|
-
function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS) {
|
|
1441
|
-
const normalized = normalizeBridgeMethod(method);
|
|
1442
|
-
if (!isAllowedMethod(normalized)) {
|
|
1443
|
-
return Promise.reject(new Error(`Bridge method not allowed: ${method}`));
|
|
1444
|
-
}
|
|
1445
|
-
const ws = pluginWs;
|
|
1446
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1447
|
-
return Promise.reject(
|
|
1448
|
-
new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect.")
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1452
|
-
return new Promise((resolve4, reject) => {
|
|
1453
|
-
const timeout = setTimeout(() => {
|
|
1454
|
-
if (pendingBridgeRequests.delete(id)) {
|
|
1455
|
-
reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
|
|
1456
|
-
}
|
|
1457
|
-
}, timeoutMs);
|
|
1458
|
-
pendingBridgeRequests.set(id, { resolve: resolve4, reject, timeout });
|
|
1459
|
-
try {
|
|
1460
|
-
ws.send(JSON.stringify({ id, method: normalized, params }));
|
|
1461
|
-
} catch (e) {
|
|
1462
|
-
pendingBridgeRequests.delete(id);
|
|
1463
|
-
clearTimeout(timeout);
|
|
1464
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
1465
|
-
}
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1291
|
+
// ../figma-tools/dist/index.js
|
|
1468
1292
|
function figmaColorNameWithGroup(key) {
|
|
1469
1293
|
if (key.includes("/")) {
|
|
1470
1294
|
const [group2, ...rest] = key.split("/");
|
|
@@ -1558,6 +1382,35 @@ function parseBoxShadowToFigmaEffects(shadowStr) {
|
|
|
1558
1382
|
}
|
|
1559
1383
|
return out;
|
|
1560
1384
|
}
|
|
1385
|
+
function colorTo8DigitHex(color) {
|
|
1386
|
+
if (!color || typeof color !== "string") return null;
|
|
1387
|
+
const s = color.trim();
|
|
1388
|
+
const rgbaMatch = s.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i);
|
|
1389
|
+
if (rgbaMatch) {
|
|
1390
|
+
const r = Math.max(0, Math.min(255, parseInt(rgbaMatch[1], 10)));
|
|
1391
|
+
const g = Math.max(0, Math.min(255, parseInt(rgbaMatch[2], 10)));
|
|
1392
|
+
const b = Math.max(0, Math.min(255, parseInt(rgbaMatch[3], 10)));
|
|
1393
|
+
const a = rgbaMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbaMatch[4]))) : 1;
|
|
1394
|
+
const aByte = Math.round(a * 255);
|
|
1395
|
+
const hex = "#" + [r, g, b, aByte].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
1396
|
+
return hex;
|
|
1397
|
+
}
|
|
1398
|
+
const hexMatch = s.match(/^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/i);
|
|
1399
|
+
if (hexMatch) {
|
|
1400
|
+
const rgb = hexMatch[1];
|
|
1401
|
+
const aa = hexMatch[2] ?? "FF";
|
|
1402
|
+
return `#${rgb}${aa}`.toUpperCase();
|
|
1403
|
+
}
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
function typesetKeyToFontFamilyRole(key) {
|
|
1407
|
+
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
1408
|
+
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
1409
|
+
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
1410
|
+
if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
|
|
1411
|
+
if (prefix.startsWith("body")) return "body";
|
|
1412
|
+
return "body";
|
|
1413
|
+
}
|
|
1561
1414
|
function buildFigmaPayloadsFromDS(data) {
|
|
1562
1415
|
const tokens = data.tokens;
|
|
1563
1416
|
const colors = tokens?.colors;
|
|
@@ -1578,15 +1431,17 @@ function buildFigmaPayloadsFromDS(data) {
|
|
|
1578
1431
|
if (!orderedKeys.includes(k)) orderedKeys.push(k);
|
|
1579
1432
|
}
|
|
1580
1433
|
for (const key of orderedKeys) {
|
|
1581
|
-
const
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1434
|
+
const lightVal = light[key];
|
|
1435
|
+
const darkVal = dark[key];
|
|
1436
|
+
const lightHex = typeof lightVal === "string" ? colorTo8DigitHex(lightVal) ?? (hexRe.test(lightVal) ? lightVal : null) : null;
|
|
1437
|
+
if (lightHex && hexRe.test(lightHex)) {
|
|
1584
1438
|
const figmaName = figmaColorNameWithGroup(key);
|
|
1585
1439
|
if (addedNames.has(figmaName)) continue;
|
|
1586
1440
|
addedNames.add(figmaName);
|
|
1441
|
+
const darkHex = typeof darkVal === "string" ? colorTo8DigitHex(darkVal) ?? (hexRe.test(darkVal) ? darkVal : null) : null;
|
|
1587
1442
|
const values = {};
|
|
1588
1443
|
if (modes.includes("Light")) values.Light = lightHex;
|
|
1589
|
-
if (modes.includes("Dark")) values.Dark =
|
|
1444
|
+
if (modes.includes("Dark")) values.Dark = darkHex && hexRe.test(darkHex) ? darkHex : lightHex;
|
|
1590
1445
|
variables.push({ name: figmaName, values });
|
|
1591
1446
|
paintStyles.push({ name: figmaName, color: lightHex });
|
|
1592
1447
|
}
|
|
@@ -1605,6 +1460,37 @@ function buildFigmaPayloadsFromDS(data) {
|
|
|
1605
1460
|
variables.push({ name: figmaName, values });
|
|
1606
1461
|
}
|
|
1607
1462
|
}
|
|
1463
|
+
const alphaScales = colors?.alphaScales;
|
|
1464
|
+
if (alphaScales?.whiteAlpha && typeof alphaScales.whiteAlpha === "object") {
|
|
1465
|
+
if (modes.length === 0) modes.push("Light");
|
|
1466
|
+
for (const [step, value] of Object.entries(alphaScales.whiteAlpha)) {
|
|
1467
|
+
if (typeof value !== "string") continue;
|
|
1468
|
+
const hex = colorTo8DigitHex(value);
|
|
1469
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
1470
|
+
const figmaName = `WhiteAlpha / ${step}`;
|
|
1471
|
+
if (addedNames.has(figmaName)) continue;
|
|
1472
|
+
addedNames.add(figmaName);
|
|
1473
|
+
const values = {};
|
|
1474
|
+
for (const m of modes) values[m] = hex;
|
|
1475
|
+
variables.push({ name: figmaName, values });
|
|
1476
|
+
paintStyles.push({ name: figmaName, color: hex });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (alphaScales?.blackAlpha && typeof alphaScales.blackAlpha === "object") {
|
|
1480
|
+
if (modes.length === 0) modes.push("Light");
|
|
1481
|
+
for (const [step, value] of Object.entries(alphaScales.blackAlpha)) {
|
|
1482
|
+
if (typeof value !== "string") continue;
|
|
1483
|
+
const hex = colorTo8DigitHex(value);
|
|
1484
|
+
if (!hex || !hexRe.test(hex)) continue;
|
|
1485
|
+
const figmaName = `BlackAlpha / ${step}`;
|
|
1486
|
+
if (addedNames.has(figmaName)) continue;
|
|
1487
|
+
addedNames.add(figmaName);
|
|
1488
|
+
const values = {};
|
|
1489
|
+
for (const m of modes) values[m] = hex;
|
|
1490
|
+
variables.push({ name: figmaName, values });
|
|
1491
|
+
paintStyles.push({ name: figmaName, color: hex });
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1608
1494
|
if (variables.length === 0 && modes.length === 0) modes.push("Light");
|
|
1609
1495
|
const dsName = data.meta?.name;
|
|
1610
1496
|
const collectionPrefix = dsName ? `${dsName} ` : "";
|
|
@@ -1727,33 +1613,51 @@ function buildFigmaPayloadsFromDS(data) {
|
|
|
1727
1613
|
const numberVariableCollections = [];
|
|
1728
1614
|
const spacing = tokens?.spacing;
|
|
1729
1615
|
if (spacing?.scale && typeof spacing.scale === "object") {
|
|
1730
|
-
const
|
|
1616
|
+
const vars = [];
|
|
1731
1617
|
for (const [key, val] of Object.entries(spacing.scale)) {
|
|
1732
1618
|
const n = tokenValueToNumber(val);
|
|
1733
|
-
if (n >= 0)
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
if (
|
|
1619
|
+
if (n >= 0) vars.push({ name: `Spacing / ${key}`, value: n });
|
|
1620
|
+
}
|
|
1621
|
+
vars.sort((a, b) => a.value - b.value);
|
|
1622
|
+
if (vars.length > 0)
|
|
1623
|
+
numberVariableCollections.push({
|
|
1624
|
+
collectionName: `${collectionPrefix}Spacing`,
|
|
1625
|
+
categoryKey: "Spacing",
|
|
1626
|
+
variables: vars,
|
|
1627
|
+
scopes: ["GAP"]
|
|
1628
|
+
});
|
|
1737
1629
|
}
|
|
1738
1630
|
const radius = tokens?.radius;
|
|
1739
1631
|
if (radius?.scale && typeof radius.scale === "object") {
|
|
1740
|
-
const
|
|
1632
|
+
const vars = [];
|
|
1741
1633
|
for (const [key, val] of Object.entries(radius.scale)) {
|
|
1742
1634
|
const n = tokenValueToNumber(val);
|
|
1743
|
-
if (n >= 0)
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
if (
|
|
1635
|
+
if (n >= 0) vars.push({ name: `Radius / ${key}`, value: n });
|
|
1636
|
+
}
|
|
1637
|
+
vars.sort((a, b) => a.value - b.value);
|
|
1638
|
+
if (vars.length > 0)
|
|
1639
|
+
numberVariableCollections.push({
|
|
1640
|
+
collectionName: `${collectionPrefix}Radius`,
|
|
1641
|
+
categoryKey: "Radius",
|
|
1642
|
+
variables: vars,
|
|
1643
|
+
scopes: ["CORNER_RADIUS"]
|
|
1644
|
+
});
|
|
1747
1645
|
}
|
|
1748
1646
|
const borders = tokens?.borders;
|
|
1749
1647
|
if (borders?.width && typeof borders.width === "object") {
|
|
1750
|
-
const
|
|
1648
|
+
const vars = [];
|
|
1751
1649
|
for (const [key, val] of Object.entries(borders.width)) {
|
|
1752
1650
|
const n = tokenValueToNumber(val);
|
|
1753
|
-
if (n >= 0)
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
if (
|
|
1651
|
+
if (n >= 0) vars.push({ name: `Borders / ${key}`, value: n });
|
|
1652
|
+
}
|
|
1653
|
+
vars.sort((a, b) => a.value - b.value);
|
|
1654
|
+
if (vars.length > 0)
|
|
1655
|
+
numberVariableCollections.push({
|
|
1656
|
+
collectionName: `${collectionPrefix}Borders`,
|
|
1657
|
+
categoryKey: "Borders",
|
|
1658
|
+
variables: vars,
|
|
1659
|
+
scopes: ["STROKE_FLOAT"]
|
|
1660
|
+
});
|
|
1757
1661
|
}
|
|
1758
1662
|
const sizing = tokens?.sizing;
|
|
1759
1663
|
const sizingVariables = [];
|
|
@@ -1771,17 +1675,28 @@ function buildFigmaPayloadsFromDS(data) {
|
|
|
1771
1675
|
}
|
|
1772
1676
|
sizingVariables.sort((a, b) => a.value - b.value);
|
|
1773
1677
|
if (sizingVariables.length > 0) {
|
|
1774
|
-
numberVariableCollections.push({
|
|
1678
|
+
numberVariableCollections.push({
|
|
1679
|
+
collectionName: `${collectionPrefix}Sizing`,
|
|
1680
|
+
categoryKey: "Sizing",
|
|
1681
|
+
variables: sizingVariables,
|
|
1682
|
+
scopes: ["WIDTH_HEIGHT"]
|
|
1683
|
+
});
|
|
1775
1684
|
}
|
|
1776
1685
|
const layout = tokens?.layout;
|
|
1777
1686
|
if (layout?.breakpoint && typeof layout.breakpoint === "object") {
|
|
1778
|
-
const
|
|
1687
|
+
const vars = [];
|
|
1779
1688
|
for (const [key, val] of Object.entries(layout.breakpoint)) {
|
|
1780
1689
|
const n = tokenValueToNumber(val);
|
|
1781
|
-
if (n >= 0)
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
if (
|
|
1690
|
+
if (n >= 0) vars.push({ name: `Breakpoint / ${key}`, value: n });
|
|
1691
|
+
}
|
|
1692
|
+
vars.sort((a, b) => a.value - b.value);
|
|
1693
|
+
if (vars.length > 0)
|
|
1694
|
+
numberVariableCollections.push({
|
|
1695
|
+
collectionName: `${collectionPrefix}Layout`,
|
|
1696
|
+
categoryKey: "Layout",
|
|
1697
|
+
variables: vars,
|
|
1698
|
+
scopes: ["WIDTH_HEIGHT"]
|
|
1699
|
+
});
|
|
1785
1700
|
}
|
|
1786
1701
|
const effectStyles = [];
|
|
1787
1702
|
const shadows = tokens?.shadows;
|
|
@@ -1789,48 +1704,206 @@ function buildFigmaPayloadsFromDS(data) {
|
|
|
1789
1704
|
for (const [key, val] of Object.entries(shadows.elevation)) {
|
|
1790
1705
|
if (typeof val !== "string") continue;
|
|
1791
1706
|
const effects = parseBoxShadowToFigmaEffects(val);
|
|
1792
|
-
if (effects.length > 0) {
|
|
1793
|
-
effectStyles.push({
|
|
1794
|
-
name: `Shadow / ${key}`,
|
|
1795
|
-
effects
|
|
1796
|
-
});
|
|
1797
|
-
}
|
|
1707
|
+
if (effects.length > 0) effectStyles.push({ name: `Shadow / ${key}`, effects });
|
|
1798
1708
|
}
|
|
1799
1709
|
}
|
|
1800
1710
|
if (shadows?.focus && typeof shadows.focus === "string") {
|
|
1801
1711
|
const effects = parseBoxShadowToFigmaEffects(shadows.focus);
|
|
1802
|
-
if (effects.length > 0) {
|
|
1803
|
-
|
|
1712
|
+
if (effects.length > 0) effectStyles.push({ name: "Shadow / focus", effects });
|
|
1713
|
+
}
|
|
1714
|
+
effectStyles.sort((a, b) => {
|
|
1715
|
+
const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
|
|
1716
|
+
const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
|
|
1717
|
+
const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
|
|
1718
|
+
const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
|
|
1719
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
1720
|
+
return nameA.localeCompare(nameB);
|
|
1721
|
+
});
|
|
1722
|
+
return {
|
|
1723
|
+
colorVariables: { collectionName: colorCollectionName, modes, variables },
|
|
1724
|
+
paintStyles,
|
|
1725
|
+
textStyles,
|
|
1726
|
+
numberVariableCollections,
|
|
1727
|
+
effectStyles
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/index.ts
|
|
1732
|
+
import * as path2 from "path";
|
|
1733
|
+
import * as fs2 from "fs";
|
|
1734
|
+
import { execSync } from "child_process";
|
|
1735
|
+
import { platform } from "os";
|
|
1736
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
1737
|
+
var FIGMA_BRIDGE_PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 8765;
|
|
1738
|
+
var FIGMA_BRIDGE_HOST = process.env.FIGMA_BRIDGE_HOST || "127.0.0.1";
|
|
1739
|
+
var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
|
|
1740
|
+
var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
|
|
1741
|
+
var FIGMA_CONNECTION_INSTRUCTIONS = {
|
|
1742
|
+
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.",
|
|
1743
|
+
connect: 'In the plugin UI, tap **Connect** and wait until the status shows "Connected".',
|
|
1744
|
+
startBridge: "The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server running (e.g. in MCP settings), then in Figma run the Atomix plugin and tap Connect."
|
|
1745
|
+
};
|
|
1746
|
+
var bridgeWss = null;
|
|
1747
|
+
var pluginWs = null;
|
|
1748
|
+
var pendingBridgeRequests = /* @__PURE__ */ new Map();
|
|
1749
|
+
function ensureFigmaBridgePortFree(port) {
|
|
1750
|
+
const portStr = String(port);
|
|
1751
|
+
const ourPid = String(process.pid);
|
|
1752
|
+
try {
|
|
1753
|
+
if (platform() === "win32") {
|
|
1754
|
+
const out = execSync(`netstat -ano`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1755
|
+
const pids = /* @__PURE__ */ new Set();
|
|
1756
|
+
for (const line of out.split(/\r?\n/)) {
|
|
1757
|
+
if (line.includes(`:${portStr}`) && line.includes("LISTENING")) {
|
|
1758
|
+
const parts = line.trim().split(/\s+/);
|
|
1759
|
+
const pid = parts[parts.length - 1];
|
|
1760
|
+
if (/^\d+$/.test(pid) && pid !== ourPid) pids.add(pid);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
for (const pid of pids) {
|
|
1764
|
+
try {
|
|
1765
|
+
execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1766
|
+
console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
|
|
1767
|
+
} catch (_) {
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
} else {
|
|
1771
|
+
const out = execSync(`lsof -ti :${portStr}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1772
|
+
if (!out) return;
|
|
1773
|
+
const pids = out.split(/\s+/).filter((p) => p && p !== ourPid);
|
|
1774
|
+
for (const pid of pids) {
|
|
1775
|
+
try {
|
|
1776
|
+
execSync(`kill -9 ${pid}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
1777
|
+
console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
|
|
1778
|
+
} catch (_) {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function startFigmaBridge() {
|
|
1786
|
+
if (bridgeWss) return;
|
|
1787
|
+
try {
|
|
1788
|
+
ensureFigmaBridgePortFree(FIGMA_BRIDGE_PORT);
|
|
1789
|
+
bridgeWss = new WebSocketServer({
|
|
1790
|
+
host: FIGMA_BRIDGE_HOST,
|
|
1791
|
+
port: FIGMA_BRIDGE_PORT,
|
|
1792
|
+
clientTracking: true
|
|
1793
|
+
});
|
|
1794
|
+
bridgeWss.on("connection", (ws, req) => {
|
|
1795
|
+
const url = req.url || "";
|
|
1796
|
+
const params = new URLSearchParams(url.startsWith("/") ? url.slice(1) : url);
|
|
1797
|
+
const token = params.get("token");
|
|
1798
|
+
const role = params.get("role");
|
|
1799
|
+
if (FIGMA_BRIDGE_TOKEN && token !== FIGMA_BRIDGE_TOKEN) {
|
|
1800
|
+
ws.close(4003, "Invalid or missing bridge token");
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (role !== "plugin") {
|
|
1804
|
+
ws.close(4002, "Only role=plugin is accepted (bridge runs in MCP server)");
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (pluginWs) {
|
|
1808
|
+
try {
|
|
1809
|
+
pluginWs.close();
|
|
1810
|
+
} catch (_) {
|
|
1811
|
+
}
|
|
1812
|
+
pluginWs = null;
|
|
1813
|
+
}
|
|
1814
|
+
pluginWs = ws;
|
|
1815
|
+
ws.on("message", (raw) => {
|
|
1816
|
+
const text = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
1817
|
+
let msg;
|
|
1818
|
+
try {
|
|
1819
|
+
msg = JSON.parse(text);
|
|
1820
|
+
} catch {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const parsed = msg;
|
|
1824
|
+
if (parsed?.type === "ping" && typeof parsed.id === "string") {
|
|
1825
|
+
try {
|
|
1826
|
+
ws.send(JSON.stringify({ type: "pong", id: parsed.id }));
|
|
1827
|
+
} catch (_) {
|
|
1828
|
+
}
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (typeof parsed.id === "string" && ("result" in parsed || "error" in parsed)) {
|
|
1832
|
+
const pending = pendingBridgeRequests.get(parsed.id);
|
|
1833
|
+
if (pending) {
|
|
1834
|
+
clearTimeout(pending.timeout);
|
|
1835
|
+
pendingBridgeRequests.delete(parsed.id);
|
|
1836
|
+
if (parsed.error) pending.reject(new Error(parsed.error));
|
|
1837
|
+
else pending.resolve(parsed.result);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
ws.on("close", () => {
|
|
1842
|
+
if (pluginWs === ws) pluginWs = null;
|
|
1843
|
+
});
|
|
1844
|
+
ws.on("error", () => {
|
|
1845
|
+
if (pluginWs === ws) pluginWs = null;
|
|
1846
|
+
});
|
|
1847
|
+
});
|
|
1848
|
+
bridgeWss.on("listening", () => {
|
|
1849
|
+
console.error(`[atomix-mcp] Figma bridge listening on ws://${FIGMA_BRIDGE_HOST}:${FIGMA_BRIDGE_PORT} (local only)`);
|
|
1850
|
+
if (FIGMA_BRIDGE_TOKEN) {
|
|
1851
|
+
console.error("[atomix-mcp] Figma bridge token required (FIGMA_BRIDGE_TOKEN)");
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
bridgeWss.on("error", (err) => {
|
|
1855
|
+
console.error("[atomix-mcp] Figma bridge server error:", err);
|
|
1856
|
+
});
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
console.error("[atomix-mcp] Failed to start Figma bridge:", err);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function closeFigmaBridge() {
|
|
1862
|
+
if (pluginWs) {
|
|
1863
|
+
try {
|
|
1864
|
+
pluginWs.close();
|
|
1865
|
+
} catch (_) {
|
|
1866
|
+
}
|
|
1867
|
+
pluginWs = null;
|
|
1868
|
+
}
|
|
1869
|
+
if (bridgeWss) {
|
|
1870
|
+
try {
|
|
1871
|
+
bridgeWss.close();
|
|
1872
|
+
} catch (_) {
|
|
1873
|
+
}
|
|
1874
|
+
bridgeWss = null;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function isBridgeReachable() {
|
|
1878
|
+
return Promise.resolve(!!(pluginWs && pluginWs.readyState === WebSocket.OPEN));
|
|
1879
|
+
}
|
|
1880
|
+
function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS) {
|
|
1881
|
+
const normalized = normalizeBridgeMethod(method);
|
|
1882
|
+
if (!isAllowedMethod(normalized)) {
|
|
1883
|
+
return Promise.reject(new Error(`Bridge method not allowed: ${method}`));
|
|
1884
|
+
}
|
|
1885
|
+
const ws = pluginWs;
|
|
1886
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1887
|
+
return Promise.reject(
|
|
1888
|
+
new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect.")
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1892
|
+
return new Promise((resolve4, reject) => {
|
|
1893
|
+
const timeout = setTimeout(() => {
|
|
1894
|
+
if (pendingBridgeRequests.delete(id)) {
|
|
1895
|
+
reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
|
|
1896
|
+
}
|
|
1897
|
+
}, timeoutMs);
|
|
1898
|
+
pendingBridgeRequests.set(id, { resolve: resolve4, reject, timeout });
|
|
1899
|
+
try {
|
|
1900
|
+
ws.send(JSON.stringify({ id, method: normalized, params }));
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
pendingBridgeRequests.delete(id);
|
|
1903
|
+
clearTimeout(timeout);
|
|
1904
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
1804
1905
|
}
|
|
1805
|
-
}
|
|
1806
|
-
effectStyles.sort((a, b) => {
|
|
1807
|
-
const nameA = a.name.startsWith("Shadow / ") ? a.name.slice(9) : a.name;
|
|
1808
|
-
const nameB = b.name.startsWith("Shadow / ") ? b.name.slice(9) : b.name;
|
|
1809
|
-
const orderA = FIGMA_SHADOW_ORDER[nameA] ?? 100;
|
|
1810
|
-
const orderB = FIGMA_SHADOW_ORDER[nameB] ?? 100;
|
|
1811
|
-
if (orderA !== orderB) return orderA - orderB;
|
|
1812
|
-
return nameA.localeCompare(nameB);
|
|
1813
1906
|
});
|
|
1814
|
-
return {
|
|
1815
|
-
colorVariables: { collectionName: colorCollectionName, modes, variables },
|
|
1816
|
-
paintStyles,
|
|
1817
|
-
textStyles,
|
|
1818
|
-
numberVariableCollections,
|
|
1819
|
-
effectStyles
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
function getExpectedFigmaNamesFromDS(data) {
|
|
1823
|
-
const payloads = buildFigmaPayloadsFromDS(data);
|
|
1824
|
-
const numberVariableNames = payloads.numberVariableCollections.flatMap(
|
|
1825
|
-
(c) => c.variables.map((v) => v.name)
|
|
1826
|
-
);
|
|
1827
|
-
return {
|
|
1828
|
-
colorVariableNames: payloads.colorVariables.variables.map((v) => v.name),
|
|
1829
|
-
paintStyleNames: payloads.paintStyles.map((s) => s.name),
|
|
1830
|
-
textStyleNames: payloads.textStyles.map((s) => s.name),
|
|
1831
|
-
effectStyleNames: payloads.effectStyles.map((s) => s.name),
|
|
1832
|
-
numberVariableNames
|
|
1833
|
-
};
|
|
1834
1907
|
}
|
|
1835
1908
|
function parseArgs() {
|
|
1836
1909
|
const args = process.argv.slice(2);
|
|
@@ -1867,21 +1940,6 @@ function hasValidAuthConfig() {
|
|
|
1867
1940
|
}
|
|
1868
1941
|
var AUTH_REQUIRED_MESSAGE = "Atomix MCP requires authentication. Add both --ds-id and --atomix-token to your MCP config (Settings \u2192 MCP), then restart your AI tool. Get your token from Atomix Studio: Export modal or Settings \u2192 Regenerate Atomix access token.";
|
|
1869
1942
|
var lastChangeSummary = null;
|
|
1870
|
-
var FIGMA_SYNC_TOOL_NAME = "syncToFigma";
|
|
1871
|
-
var FIGMA_DESIGN_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1872
|
-
"getFigmaVariablesAndStyles",
|
|
1873
|
-
"createDesignPlaceholder",
|
|
1874
|
-
"resolveFigmaIdsForTokens",
|
|
1875
|
-
"designCreateFrame",
|
|
1876
|
-
"designCreateText",
|
|
1877
|
-
"designCreateRectangle",
|
|
1878
|
-
"designSetAutoLayout",
|
|
1879
|
-
"designSetLayoutConstraints",
|
|
1880
|
-
"designAppendChild",
|
|
1881
|
-
"getDesignScreenshot",
|
|
1882
|
-
"finalizeDesignFrame"
|
|
1883
|
-
]);
|
|
1884
|
-
var FIGMA_TOOL_NAMES = /* @__PURE__ */ new Set([FIGMA_SYNC_TOOL_NAME, ...FIGMA_DESIGN_TOOL_NAMES]);
|
|
1885
1943
|
var lastSyncAffectedTokens = null;
|
|
1886
1944
|
function getLastChangeSummary() {
|
|
1887
1945
|
return lastChangeSummary;
|
|
@@ -1952,7 +2010,7 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
|
1952
2010
|
return result.data;
|
|
1953
2011
|
}
|
|
1954
2012
|
var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
|
|
1955
|
-
function
|
|
2013
|
+
function typesetKeyToFontFamilyRole2(key) {
|
|
1956
2014
|
const prefix = (key.split("-")[0] ?? key).toLowerCase();
|
|
1957
2015
|
if (prefix === "display" || prefix.startsWith("display")) return "display";
|
|
1958
2016
|
if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
|
|
@@ -1972,7 +2030,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
|
|
|
1972
2030
|
const p = cssPrefix ? `${cssPrefix}-` : "";
|
|
1973
2031
|
const typesets = [];
|
|
1974
2032
|
for (const key of Object.keys(fontSize)) {
|
|
1975
|
-
const role =
|
|
2033
|
+
const role = typesetKeyToFontFamilyRole2(key);
|
|
1976
2034
|
const familyName = fontFamily[role] ?? fontFamily.body;
|
|
1977
2035
|
const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
|
|
1978
2036
|
const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
|
|
@@ -1998,7 +2056,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
|
|
|
1998
2056
|
var server = new Server(
|
|
1999
2057
|
{
|
|
2000
2058
|
name: "atomix-mcp-user",
|
|
2001
|
-
version: "1.0.
|
|
2059
|
+
version: "1.0.29"
|
|
2002
2060
|
},
|
|
2003
2061
|
{
|
|
2004
2062
|
capabilities: {
|
|
@@ -2018,11 +2076,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2018
2076
|
try {
|
|
2019
2077
|
await fetchDesignSystemForMCP(true);
|
|
2020
2078
|
if (cachedMcpTier === "pro") {
|
|
2021
|
-
console.error("[Atomix MCP] Resolved tier = pro.
|
|
2079
|
+
console.error("[Atomix MCP] Resolved tier = pro.");
|
|
2022
2080
|
} else if (cachedMcpTier === "free") {
|
|
2023
|
-
console.error(
|
|
2024
|
-
"[Atomix MCP] Resolved tier = free. Figma design tools and --design-in-figma are hidden. syncToFigma and /--sync-to-figma are available. Pro design tools appear when the DS owner has Pro and the pro_figma_export flag is enabled."
|
|
2025
|
-
);
|
|
2081
|
+
console.error("[Atomix MCP] Resolved tier = free. syncToFigma and /--sync-to-figma are available.");
|
|
2026
2082
|
}
|
|
2027
2083
|
} catch (err) {
|
|
2028
2084
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2164,7 +2220,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2164
2220
|
},
|
|
2165
2221
|
{
|
|
2166
2222
|
name: "syncAll",
|
|
2167
|
-
description: "Sync tokens, AI rules, skills (
|
|
2223
|
+
description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Response includes a VALIDATION section\u2014agent must check it to confirm success. Optional: output (default ./tokens.css), format (default css), skipTokens (if true, only skills and manifest), dryRun (if true, report only; no files written).",
|
|
2168
2224
|
inputSchema: {
|
|
2169
2225
|
type: "object",
|
|
2170
2226
|
properties: {
|
|
@@ -2216,160 +2272,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2216
2272
|
properties: {},
|
|
2217
2273
|
required: []
|
|
2218
2274
|
}
|
|
2219
|
-
},
|
|
2220
|
-
{
|
|
2221
|
-
name: "getFigmaVariablesAndStyles",
|
|
2222
|
-
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.",
|
|
2223
|
-
inputSchema: {
|
|
2224
|
-
type: "object",
|
|
2225
|
-
properties: {},
|
|
2226
|
-
required: []
|
|
2227
|
-
}
|
|
2228
|
-
},
|
|
2229
|
-
{
|
|
2230
|
-
name: "createDesignPlaceholder",
|
|
2231
|
-
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).",
|
|
2232
|
-
inputSchema: {
|
|
2233
|
-
type: "object",
|
|
2234
|
-
properties: {
|
|
2235
|
-
width: { type: "number", description: "Frame width in px (optional, default 400)" },
|
|
2236
|
-
height: { type: "number", description: "Frame height in px (optional, default 400)" }
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
},
|
|
2240
|
-
{
|
|
2241
|
-
name: "resolveFigmaIdsForTokens",
|
|
2242
|
-
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.",
|
|
2243
|
-
inputSchema: {
|
|
2244
|
-
type: "object",
|
|
2245
|
-
properties: {},
|
|
2246
|
-
required: []
|
|
2247
|
-
}
|
|
2248
|
-
},
|
|
2249
|
-
{
|
|
2250
|
-
name: "designCreateFrame",
|
|
2251
|
-
description: "Create a frame in Figma under the given parent. Use fillVariableId or fillPaintStyleId from resolveFigmaIdsForTokens for background. Returns nodeId.",
|
|
2252
|
-
inputSchema: {
|
|
2253
|
-
type: "object",
|
|
2254
|
-
properties: {
|
|
2255
|
-
parentId: { type: "string", description: "Parent frame/node id (e.g. from createDesignPlaceholder)" },
|
|
2256
|
-
name: { type: "string", description: "Layer name" },
|
|
2257
|
-
width: { type: "number", description: "Width in px (optional)" },
|
|
2258
|
-
height: { type: "number", description: "Height in px (optional)" },
|
|
2259
|
-
fillVariableId: { type: "string", description: "Color variable id from resolveFigmaIdsForTokens" },
|
|
2260
|
-
fillPaintStyleId: { type: "string", description: "Paint style id from resolveFigmaIdsForTokens" }
|
|
2261
|
-
},
|
|
2262
|
-
required: ["parentId", "name"]
|
|
2263
|
-
}
|
|
2264
|
-
},
|
|
2265
|
-
{
|
|
2266
|
-
name: "designCreateText",
|
|
2267
|
-
description: "Create a text node in Figma. Bind to a text style via textStyleId from resolveFigmaIdsForTokens.",
|
|
2268
|
-
inputSchema: {
|
|
2269
|
-
type: "object",
|
|
2270
|
-
properties: {
|
|
2271
|
-
parentId: { type: "string", description: "Parent frame id" },
|
|
2272
|
-
characters: { type: "string", description: "Text content" },
|
|
2273
|
-
textStyleId: { type: "string", description: "Text style id from resolveFigmaIdsForTokens (required)" },
|
|
2274
|
-
name: { type: "string", description: "Layer name (optional)" }
|
|
2275
|
-
},
|
|
2276
|
-
required: ["parentId", "characters", "textStyleId"]
|
|
2277
|
-
}
|
|
2278
|
-
},
|
|
2279
|
-
{
|
|
2280
|
-
name: "designCreateRectangle",
|
|
2281
|
-
description: "Create a rectangle in Figma. Use fillVariableId or fillPaintStyleId from resolveFigmaIdsForTokens.",
|
|
2282
|
-
inputSchema: {
|
|
2283
|
-
type: "object",
|
|
2284
|
-
properties: {
|
|
2285
|
-
parentId: { type: "string", description: "Parent frame id" },
|
|
2286
|
-
width: { type: "number", description: "Width in px" },
|
|
2287
|
-
height: { type: "number", description: "Height in px" },
|
|
2288
|
-
fillVariableId: { type: "string", description: "Color variable id" },
|
|
2289
|
-
fillPaintStyleId: { type: "string", description: "Paint style id" },
|
|
2290
|
-
name: { type: "string", description: "Layer name (optional)" }
|
|
2291
|
-
},
|
|
2292
|
-
required: ["parentId", "width", "height"]
|
|
2293
|
-
}
|
|
2294
|
-
},
|
|
2295
|
-
{
|
|
2296
|
-
name: "designSetAutoLayout",
|
|
2297
|
-
description: "Set auto-layout on a frame. Use variable ids from resolveFigmaIdsForTokens for padding and itemSpacing (e.g. Spacing / lg).",
|
|
2298
|
-
inputSchema: {
|
|
2299
|
-
type: "object",
|
|
2300
|
-
properties: {
|
|
2301
|
-
nodeId: { type: "string", description: "Frame node id" },
|
|
2302
|
-
direction: { type: "string", description: "HORIZONTAL or VERTICAL" },
|
|
2303
|
-
paddingVariableId: { type: "string", description: "Number variable id for all padding" },
|
|
2304
|
-
paddingTopVariableId: { type: "string" },
|
|
2305
|
-
paddingRightVariableId: { type: "string" },
|
|
2306
|
-
paddingBottomVariableId: { type: "string" },
|
|
2307
|
-
paddingLeftVariableId: { type: "string" },
|
|
2308
|
-
itemSpacingVariableId: { type: "string", description: "Number variable id for gap between children" },
|
|
2309
|
-
primaryAxisAlignItems: { type: "string", description: "MIN, CENTER, MAX, SPACE_BETWEEN" },
|
|
2310
|
-
counterAxisAlignItems: { type: "string", description: "MIN, CENTER, MAX, BASELINE" },
|
|
2311
|
-
layoutSizingHorizontal: { type: "string", description: "HUG or FILL" },
|
|
2312
|
-
layoutSizingVertical: { type: "string", description: "HUG or FILL" }
|
|
2313
|
-
},
|
|
2314
|
-
required: ["nodeId", "direction"]
|
|
2315
|
-
}
|
|
2316
|
-
},
|
|
2317
|
-
{
|
|
2318
|
-
name: "designSetLayoutConstraints",
|
|
2319
|
-
description: "Set min/max width and height on a frame (e.g. breakpoint variables). Use number variable ids from resolveFigmaIdsForTokens.",
|
|
2320
|
-
inputSchema: {
|
|
2321
|
-
type: "object",
|
|
2322
|
-
properties: {
|
|
2323
|
-
nodeId: { type: "string", description: "Frame node id" },
|
|
2324
|
-
minWidthVariableId: { type: "string" },
|
|
2325
|
-
maxWidthVariableId: { type: "string" },
|
|
2326
|
-
minHeightVariableId: { type: "string" },
|
|
2327
|
-
maxHeightVariableId: { type: "string" }
|
|
2328
|
-
},
|
|
2329
|
-
required: ["nodeId"]
|
|
2330
|
-
}
|
|
2331
|
-
},
|
|
2332
|
-
{
|
|
2333
|
-
name: "designAppendChild",
|
|
2334
|
-
description: "Move a node under a new parent (reparent). Use to reorder or nest nodes.",
|
|
2335
|
-
inputSchema: {
|
|
2336
|
-
type: "object",
|
|
2337
|
-
properties: {
|
|
2338
|
-
parentId: { type: "string", description: "New parent frame id" },
|
|
2339
|
-
childId: { type: "string", description: "Node id to move" }
|
|
2340
|
-
},
|
|
2341
|
-
required: ["parentId", "childId"]
|
|
2342
|
-
}
|
|
2343
|
-
},
|
|
2344
|
-
{
|
|
2345
|
-
name: "getDesignScreenshot",
|
|
2346
|
-
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.",
|
|
2347
|
-
inputSchema: {
|
|
2348
|
-
type: "object",
|
|
2349
|
-
properties: {
|
|
2350
|
-
frameId: { type: "string", description: "Frame node id (e.g. from createDesignPlaceholder)" },
|
|
2351
|
-
scale: { type: "number", description: "Export scale 1\u20134 (optional, default 1)" }
|
|
2352
|
-
},
|
|
2353
|
-
required: ["frameId"]
|
|
2354
|
-
}
|
|
2355
|
-
},
|
|
2356
|
-
{
|
|
2357
|
-
name: "finalizeDesignFrame",
|
|
2358
|
-
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.",
|
|
2359
|
-
inputSchema: {
|
|
2360
|
-
type: "object",
|
|
2361
|
-
properties: {
|
|
2362
|
-
frameId: { type: "string", description: "Frame node id to finalize" },
|
|
2363
|
-
name: { type: "string", description: "New frame name (e.g. 'Login card \u2705')" },
|
|
2364
|
-
fillVariableId: { type: "string", description: "Variable id for frame fill (removes placeholder bg)" },
|
|
2365
|
-
fillPaintStyleId: { type: "string", description: "Paint style id for frame fill" }
|
|
2366
|
-
},
|
|
2367
|
-
required: ["frameId", "name"]
|
|
2368
|
-
}
|
|
2369
2275
|
}
|
|
2370
2276
|
];
|
|
2371
|
-
|
|
2372
|
-
return { tools };
|
|
2277
|
+
return { tools: toolsList };
|
|
2373
2278
|
});
|
|
2374
2279
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2375
2280
|
const { name, arguments: args } = request.params;
|
|
@@ -2385,17 +2290,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2385
2290
|
try {
|
|
2386
2291
|
const shouldForceRefresh = name === "syncAll";
|
|
2387
2292
|
const data = await fetchDesignSystemForMCP(shouldForceRefresh);
|
|
2388
|
-
if (FIGMA_DESIGN_TOOL_NAMES.has(name) && cachedMcpTier !== "pro") {
|
|
2389
|
-
return {
|
|
2390
|
-
content: [
|
|
2391
|
-
{
|
|
2392
|
-
type: "text",
|
|
2393
|
-
text: "This design system does not have Pro Figma access. Figma design tools (design-in-Figma, createDesignPlaceholder, resolveFigmaIdsForTokens, etc.) are available when the design system owner has a Pro subscription. Sync to Figma (syncToFigma) is available on all tiers."
|
|
2394
|
-
}
|
|
2395
|
-
],
|
|
2396
|
-
isError: true
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
2293
|
async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun) {
|
|
2400
2294
|
const output = tokenOutput;
|
|
2401
2295
|
const format = tokenFormat;
|
|
@@ -2972,12 +2866,9 @@ Version: ${designSystemData.meta.version}`,
|
|
|
2972
2866
|
const dsExportedAt = data.meta.exportedAt;
|
|
2973
2867
|
const skillsDir = path2.resolve(process.cwd(), ".cursor/skills/atomix-ds");
|
|
2974
2868
|
const skillPath1 = path2.join(skillsDir, "SKILL.md");
|
|
2975
|
-
const skillPath2 = path2.join(skillsDir, "FIGMA-SKILL.md");
|
|
2976
2869
|
const manifestPath = path2.resolve(process.cwd(), "atomix-dependencies.json");
|
|
2977
2870
|
if (dryRun) {
|
|
2978
|
-
parts.push(
|
|
2979
|
-
cachedMcpTier === "pro" ? "Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/FIGMA-SKILL.md" : "Would write skills: .cursor/skills/atomix-ds/SKILL.md"
|
|
2980
|
-
);
|
|
2871
|
+
parts.push("Would write skills: .cursor/skills/atomix-ds/SKILL.md");
|
|
2981
2872
|
parts.push("Would write manifest: atomix-dependencies.json");
|
|
2982
2873
|
const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
|
|
2983
2874
|
return {
|
|
@@ -2990,14 +2881,7 @@ ${reportText}` }]
|
|
|
2990
2881
|
const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
|
|
2991
2882
|
fs2.writeFileSync(skillPath1, genericWithVersion);
|
|
2992
2883
|
allValidation.push({ path: skillPath1, status: fs2.existsSync(skillPath1) ? "OK" : "FAIL", detail: "Written." });
|
|
2993
|
-
|
|
2994
|
-
const figmaWithVersion = injectSkillVersion(FIGMA_DESIGN_SKILL_MD, dsVersion, dsExportedAt);
|
|
2995
|
-
fs2.writeFileSync(skillPath2, figmaWithVersion);
|
|
2996
|
-
allValidation.push({ path: skillPath2, status: fs2.existsSync(skillPath2) ? "OK" : "FAIL", detail: "Written." });
|
|
2997
|
-
}
|
|
2998
|
-
parts.push(
|
|
2999
|
-
cachedMcpTier === "pro" ? "Skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/FIGMA-SKILL.md (synced at DS v" + dsVersion + ")" : "Skills: .cursor/skills/atomix-ds/SKILL.md (synced at DS v" + dsVersion + ")"
|
|
3000
|
-
);
|
|
2884
|
+
parts.push("Skills: .cursor/skills/atomix-ds/SKILL.md (synced at DS v" + dsVersion + ")");
|
|
3001
2885
|
const tokens = data.tokens;
|
|
3002
2886
|
const typography = tokens?.typography;
|
|
3003
2887
|
const fontFamily = typography?.fontFamily;
|
|
@@ -3028,7 +2912,6 @@ ${reportText}` }]
|
|
|
3028
2912
|
fonts: { families: fontNames },
|
|
3029
2913
|
skills: {
|
|
3030
2914
|
skill: ".cursor/skills/atomix-ds/SKILL.md",
|
|
3031
|
-
...cachedMcpTier === "pro" ? { skillFigmaDesign: ".cursor/skills/atomix-ds/FIGMA-SKILL.md" } : {},
|
|
3032
2915
|
syncedAtVersion: data.meta.version ?? "1.0.0"
|
|
3033
2916
|
}
|
|
3034
2917
|
};
|
|
@@ -3085,12 +2968,6 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
|
|
|
3085
2968
|
path: ".cursor/skills/atomix-ds/SKILL.md",
|
|
3086
2969
|
content: GENERIC_SKILL_MD
|
|
3087
2970
|
},
|
|
3088
|
-
...cachedMcpTier === "pro" ? {
|
|
3089
|
-
skillFigmaDesign: {
|
|
3090
|
-
path: ".cursor/skills/atomix-ds/FIGMA-SKILL.md",
|
|
3091
|
-
content: FIGMA_DESIGN_SKILL_MD
|
|
3092
|
-
}
|
|
3093
|
-
} : {},
|
|
3094
2971
|
tokenFiles: {
|
|
3095
2972
|
files: ["tokens.css", "tokens.json"],
|
|
3096
2973
|
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."
|
|
@@ -3303,268 +3180,13 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
|
|
|
3303
3180
|
...out.error ? { isError: true } : {}
|
|
3304
3181
|
};
|
|
3305
3182
|
}
|
|
3306
|
-
case "getFigmaVariablesAndStyles": {
|
|
3307
|
-
try {
|
|
3308
|
-
const reachable = await isBridgeReachable();
|
|
3309
|
-
if (!reachable) {
|
|
3310
|
-
return {
|
|
3311
|
-
content: [{
|
|
3312
|
-
type: "text",
|
|
3313
|
-
text: JSON.stringify({
|
|
3314
|
-
error: "Figma bridge not reachable.",
|
|
3315
|
-
bridgeNotRunning: true,
|
|
3316
|
-
agentInstruction: "The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server running, then in Figma run the Atomix plugin and tap Connect. Then call getFigmaVariablesAndStyles again."
|
|
3317
|
-
}, null, 2)
|
|
3318
|
-
}],
|
|
3319
|
-
isError: true
|
|
3320
|
-
};
|
|
3321
|
-
}
|
|
3322
|
-
const result = await sendBridgeRequest("get_figma_variables_and_styles");
|
|
3323
|
-
return {
|
|
3324
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3325
|
-
};
|
|
3326
|
-
} catch (e) {
|
|
3327
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
3328
|
-
return {
|
|
3329
|
-
content: [{
|
|
3330
|
-
type: "text",
|
|
3331
|
-
text: JSON.stringify({ error: message, hint: "Ensure the bridge is running and the Atomix plugin is connected." }, null, 2)
|
|
3332
|
-
}],
|
|
3333
|
-
isError: true
|
|
3334
|
-
};
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
case "createDesignPlaceholder": {
|
|
3338
|
-
try {
|
|
3339
|
-
const reachable = await isBridgeReachable();
|
|
3340
|
-
if (!reachable) {
|
|
3341
|
-
return {
|
|
3342
|
-
content: [{
|
|
3343
|
-
type: "text",
|
|
3344
|
-
text: JSON.stringify({
|
|
3345
|
-
error: "Figma bridge not reachable.",
|
|
3346
|
-
hint: "The bridge runs with this MCP server. Ensure your AI environment has this MCP server running, then run the Atomix plugin and tap Connect."
|
|
3347
|
-
}, null, 2)
|
|
3348
|
-
}],
|
|
3349
|
-
isError: true
|
|
3350
|
-
};
|
|
3351
|
-
}
|
|
3352
|
-
const w = args?.width;
|
|
3353
|
-
const h = args?.height;
|
|
3354
|
-
const placeholderParams = {};
|
|
3355
|
-
if (typeof w === "number" && w > 0) placeholderParams.width = Math.round(w);
|
|
3356
|
-
if (typeof h === "number" && h > 0) placeholderParams.height = Math.round(h);
|
|
3357
|
-
const result = await sendBridgeRequest("create_design_placeholder", placeholderParams);
|
|
3358
|
-
return {
|
|
3359
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3360
|
-
};
|
|
3361
|
-
} catch (e) {
|
|
3362
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
3363
|
-
return {
|
|
3364
|
-
content: [{
|
|
3365
|
-
type: "text",
|
|
3366
|
-
text: JSON.stringify({ error: message }, null, 2)
|
|
3367
|
-
}],
|
|
3368
|
-
isError: true
|
|
3369
|
-
};
|
|
3370
|
-
}
|
|
3371
|
-
}
|
|
3372
|
-
case "resolveFigmaIdsForTokens": {
|
|
3373
|
-
try {
|
|
3374
|
-
const reachable = await isBridgeReachable();
|
|
3375
|
-
if (!reachable) {
|
|
3376
|
-
return {
|
|
3377
|
-
content: [{
|
|
3378
|
-
type: "text",
|
|
3379
|
-
text: JSON.stringify({
|
|
3380
|
-
error: "Figma bridge not reachable.",
|
|
3381
|
-
hint: "Run syncToFigma first, then ensure the plugin is connected. Then call resolveFigmaIdsForTokens again."
|
|
3382
|
-
}, null, 2)
|
|
3383
|
-
}],
|
|
3384
|
-
isError: true
|
|
3385
|
-
};
|
|
3386
|
-
}
|
|
3387
|
-
const raw = await sendBridgeRequest("get_figma_variables_and_styles");
|
|
3388
|
-
const response = raw;
|
|
3389
|
-
const variableByName = /* @__PURE__ */ new Map();
|
|
3390
|
-
if (response.variableCollections) {
|
|
3391
|
-
const numberCategorySuffixes = [" Spacing", " Radius", " Borders", " Height", " Icon", " Breakpoint"];
|
|
3392
|
-
for (const coll of response.variableCollections) {
|
|
3393
|
-
if (coll.name.endsWith(" Colors")) {
|
|
3394
|
-
for (const v of coll.variables || []) {
|
|
3395
|
-
variableByName.set(v.name, v.id);
|
|
3396
|
-
}
|
|
3397
|
-
continue;
|
|
3398
|
-
}
|
|
3399
|
-
const numberSuffix = numberCategorySuffixes.find((s) => coll.name.endsWith(s));
|
|
3400
|
-
if (numberSuffix) {
|
|
3401
|
-
const category = numberSuffix.slice(1);
|
|
3402
|
-
for (const v of coll.variables || []) {
|
|
3403
|
-
variableByName.set(`${category} / ${v.name}`, v.id);
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
const paintByName = /* @__PURE__ */ new Map();
|
|
3409
|
-
for (const s of response.paintStyles || []) {
|
|
3410
|
-
paintByName.set(s.name, s.id);
|
|
3411
|
-
}
|
|
3412
|
-
const textByName = /* @__PURE__ */ new Map();
|
|
3413
|
-
for (const s of response.textStyles || []) {
|
|
3414
|
-
textByName.set(s.name, s.id);
|
|
3415
|
-
}
|
|
3416
|
-
const effectByName = /* @__PURE__ */ new Map();
|
|
3417
|
-
for (const s of response.effectStyles || []) {
|
|
3418
|
-
effectByName.set(s.name, s.id);
|
|
3419
|
-
}
|
|
3420
|
-
const expected = getExpectedFigmaNamesFromDS(data);
|
|
3421
|
-
const resolved = {};
|
|
3422
|
-
const allNames = /* @__PURE__ */ new Set([
|
|
3423
|
-
...expected.colorVariableNames,
|
|
3424
|
-
...expected.paintStyleNames,
|
|
3425
|
-
...expected.textStyleNames,
|
|
3426
|
-
...expected.effectStyleNames,
|
|
3427
|
-
...expected.numberVariableNames
|
|
3428
|
-
]);
|
|
3429
|
-
for (const name2 of allNames) {
|
|
3430
|
-
const entry = {};
|
|
3431
|
-
const vId = variableByName.get(name2);
|
|
3432
|
-
if (vId) entry.variableId = vId;
|
|
3433
|
-
const pId = paintByName.get(name2);
|
|
3434
|
-
if (pId) entry.paintStyleId = pId;
|
|
3435
|
-
const tId = textByName.get(name2);
|
|
3436
|
-
if (tId) entry.textStyleId = tId;
|
|
3437
|
-
const eId = effectByName.get(name2);
|
|
3438
|
-
if (eId) entry.effectStyleId = eId;
|
|
3439
|
-
if (vId || pId || tId || eId) resolved[name2] = entry;
|
|
3440
|
-
}
|
|
3441
|
-
return {
|
|
3442
|
-
content: [{ type: "text", text: JSON.stringify({ resolved }, null, 2) }]
|
|
3443
|
-
};
|
|
3444
|
-
} catch (e) {
|
|
3445
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
3446
|
-
return {
|
|
3447
|
-
content: [{
|
|
3448
|
-
type: "text",
|
|
3449
|
-
text: JSON.stringify({ error: message }, null, 2)
|
|
3450
|
-
}],
|
|
3451
|
-
isError: true
|
|
3452
|
-
};
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
case "getDesignScreenshot": {
|
|
3456
|
-
try {
|
|
3457
|
-
const reachable = await isBridgeReachable();
|
|
3458
|
-
if (!reachable) {
|
|
3459
|
-
return {
|
|
3460
|
-
content: [{
|
|
3461
|
-
type: "text",
|
|
3462
|
-
text: JSON.stringify({
|
|
3463
|
-
error: "Figma bridge not reachable.",
|
|
3464
|
-
hint: "Run the Atomix plugin in Figma and tap Connect, then retry."
|
|
3465
|
-
}, null, 2)
|
|
3466
|
-
}],
|
|
3467
|
-
isError: true
|
|
3468
|
-
};
|
|
3469
|
-
}
|
|
3470
|
-
const frameId = args?.frameId;
|
|
3471
|
-
const scale = args?.scale;
|
|
3472
|
-
const params = { frameId };
|
|
3473
|
-
if (typeof scale === "number" && scale >= 1 && scale <= 4) params.scale = scale;
|
|
3474
|
-
const result = await sendBridgeRequest("get_design_screenshot", params);
|
|
3475
|
-
if (result.error) {
|
|
3476
|
-
return {
|
|
3477
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3478
|
-
isError: true
|
|
3479
|
-
};
|
|
3480
|
-
}
|
|
3481
|
-
const content = [
|
|
3482
|
-
{ type: "text", text: `Screenshot captured (format: ${result.format ?? "PNG"}, scale: ${result.scale ?? 1}). Use the image below to verify layout, fill, and content hug.` }
|
|
3483
|
-
];
|
|
3484
|
-
if (result.imageBase64) {
|
|
3485
|
-
content.push({ type: "image", data: result.imageBase64, mimeType: "image/png" });
|
|
3486
|
-
}
|
|
3487
|
-
return { content };
|
|
3488
|
-
} catch (e) {
|
|
3489
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
3490
|
-
return {
|
|
3491
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
|
|
3492
|
-
isError: true
|
|
3493
|
-
};
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
case "finalizeDesignFrame": {
|
|
3497
|
-
try {
|
|
3498
|
-
const reachable = await isBridgeReachable();
|
|
3499
|
-
if (!reachable) {
|
|
3500
|
-
return {
|
|
3501
|
-
content: [{
|
|
3502
|
-
type: "text",
|
|
3503
|
-
text: JSON.stringify({
|
|
3504
|
-
error: "Figma bridge not reachable.",
|
|
3505
|
-
hint: "Run the Atomix plugin in Figma and tap Connect, then retry."
|
|
3506
|
-
}, null, 2)
|
|
3507
|
-
}],
|
|
3508
|
-
isError: true
|
|
3509
|
-
};
|
|
3510
|
-
}
|
|
3511
|
-
const params = args && typeof args === "object" ? args : {};
|
|
3512
|
-
const result = await sendBridgeRequest("finalize_design_frame", params);
|
|
3513
|
-
return {
|
|
3514
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3515
|
-
};
|
|
3516
|
-
} catch (e) {
|
|
3517
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
3518
|
-
return {
|
|
3519
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
|
|
3520
|
-
isError: true
|
|
3521
|
-
};
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
case "designCreateFrame":
|
|
3525
|
-
case "designCreateText":
|
|
3526
|
-
case "designCreateRectangle":
|
|
3527
|
-
case "designSetAutoLayout":
|
|
3528
|
-
case "designSetLayoutConstraints":
|
|
3529
|
-
case "designAppendChild": {
|
|
3530
|
-
try {
|
|
3531
|
-
const reachable = await isBridgeReachable();
|
|
3532
|
-
if (!reachable) {
|
|
3533
|
-
return {
|
|
3534
|
-
content: [{
|
|
3535
|
-
type: "text",
|
|
3536
|
-
text: JSON.stringify({
|
|
3537
|
-
error: "Figma bridge not reachable.",
|
|
3538
|
-
hint: "Run the Atomix plugin in Figma and tap Connect, then retry."
|
|
3539
|
-
}, null, 2)
|
|
3540
|
-
}],
|
|
3541
|
-
isError: true
|
|
3542
|
-
};
|
|
3543
|
-
}
|
|
3544
|
-
const method = normalizeBridgeMethod(name);
|
|
3545
|
-
const params = args && typeof args === "object" ? args : {};
|
|
3546
|
-
const result = await sendBridgeRequest(method, params);
|
|
3547
|
-
return {
|
|
3548
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3549
|
-
};
|
|
3550
|
-
} catch (e) {
|
|
3551
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
3552
|
-
return {
|
|
3553
|
-
content: [{
|
|
3554
|
-
type: "text",
|
|
3555
|
-
text: JSON.stringify({ error: message }, null, 2)
|
|
3556
|
-
}],
|
|
3557
|
-
isError: true
|
|
3558
|
-
};
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
3183
|
default:
|
|
3562
3184
|
return {
|
|
3563
3185
|
content: [{
|
|
3564
3186
|
type: "text",
|
|
3565
3187
|
text: JSON.stringify({
|
|
3566
3188
|
error: `Unknown tool: ${name}`,
|
|
3567
|
-
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma"
|
|
3189
|
+
availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma"]
|
|
3568
3190
|
}, null, 2)
|
|
3569
3191
|
}]
|
|
3570
3192
|
};
|
|
@@ -3664,49 +3286,6 @@ Use the returned rules and token paths/values when generating or editing code. P
|
|
|
3664
3286
|
- **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.
|
|
3665
3287
|
- **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.
|
|
3666
3288
|
`;
|
|
3667
|
-
var FIGMA_DESIGN_SKILL_MD = `---
|
|
3668
|
-
name: atomix-design-in-figma
|
|
3669
|
-
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.
|
|
3670
|
-
---
|
|
3671
|
-
|
|
3672
|
-
# Design in Figma (principal product designer)
|
|
3673
|
-
|
|
3674
|
-
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**).
|
|
3675
|
-
|
|
3676
|
-
## Mandatory: no hardcoding
|
|
3677
|
-
|
|
3678
|
-
- **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.
|
|
3679
|
-
|
|
3680
|
-
## Mandatory: user AI rules (colors, typography, buttons)
|
|
3681
|
-
|
|
3682
|
-
- **Colors:** Only variable/paint style ids from resolveFigmaIdsForTokens and getAIToolRules; no hex or raw values.
|
|
3683
|
-
- **Typography:** Only text style ids from resolved + rules; no raw font size/weight/family.
|
|
3684
|
-
- **Buttons:** Use token-based sizing and hierarchy from getAIToolRules; primary/secondary/ghost from resolved styles.
|
|
3685
|
-
|
|
3686
|
-
## Mandatory: set up variables first
|
|
3687
|
-
|
|
3688
|
-
- **Call syncToFigma first.** Then getAIToolRules, listTokens, and resolveFigmaIdsForTokens. Use the \`resolved\` map for every fill, text style, and spacing.
|
|
3689
|
-
|
|
3690
|
-
## Mandatory: auto-layout and breakpoints
|
|
3691
|
-
|
|
3692
|
-
- **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.
|
|
3693
|
-
- **Breakpoints (non-mobile):** Use **designSetLayoutConstraints** with \`maxWidthVariableId\` from resolved for container frames.
|
|
3694
|
-
|
|
3695
|
-
## Three-pass flow (strict order)
|
|
3696
|
-
|
|
3697
|
-
**Setup (once):** syncToFigma \u2192 createDesignPlaceholder (save \`frameId\`) \u2192 getAIToolRules + listTokens \u2192 resolveFigmaIdsForTokens. Keep \`resolved\` and \`frameId\` for all passes.
|
|
3698
|
-
|
|
3699
|
-
**Pass 1 \u2014 Layout and contents (wireframe)**
|
|
3700
|
-
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.
|
|
3701
|
-
|
|
3702
|
-
**Pass 2 \u2014 Apply design tokens and AI rules**
|
|
3703
|
-
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.
|
|
3704
|
-
|
|
3705
|
-
**Pass 3 \u2014 Confirm no hardcoded values and follow AI rules**
|
|
3706
|
-
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.
|
|
3707
|
-
|
|
3708
|
-
Do not generate or run any JavaScript code. Use only the MCP tools listed above.
|
|
3709
|
-
`;
|
|
3710
3289
|
var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
|
|
3711
3290
|
<html lang="en">
|
|
3712
3291
|
<head>
|
|
@@ -3845,7 +3424,6 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
|
|
|
3845
3424
|
}
|
|
3846
3425
|
throw new Error(`Unknown resource: ${uri}`);
|
|
3847
3426
|
});
|
|
3848
|
-
var FIGMA_DESIGN_PROMPT_NAMES = /* @__PURE__ */ new Set(["--design-in-figma"]);
|
|
3849
3427
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
3850
3428
|
if (!hasValidAuthConfig()) {
|
|
3851
3429
|
authFailedNoTools = true;
|
|
@@ -3861,16 +3439,14 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
|
3861
3439
|
if (authFailedNoTools) {
|
|
3862
3440
|
throw new Error(AUTH_REQUIRED_MESSAGE);
|
|
3863
3441
|
}
|
|
3864
|
-
const
|
|
3442
|
+
const prompts = [
|
|
3865
3443
|
{ name: "--hello", description: "Get started with this design system - overview, tokens, and tools. Run this first!" },
|
|
3866
3444
|
{ name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
|
|
3867
3445
|
{ name: "--rules", description: "Get the design system governance rules for your AI coding tool (default: cursor)." },
|
|
3868
3446
|
{ name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
|
|
3869
3447
|
{ name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
|
|
3870
|
-
{ name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
|
|
3871
|
-
{ 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." }
|
|
3448
|
+
{ name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
|
|
3872
3449
|
];
|
|
3873
|
-
const prompts = cachedMcpTier === "pro" ? allPrompts : allPrompts.filter((p) => !FIGMA_DESIGN_PROMPT_NAMES.has(p.name));
|
|
3874
3450
|
return { prompts };
|
|
3875
3451
|
});
|
|
3876
3452
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
@@ -3887,7 +3463,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
3887
3463
|
}]
|
|
3888
3464
|
};
|
|
3889
3465
|
}
|
|
3890
|
-
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
|
|
3466
|
+
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;
|
|
3891
3467
|
const shouldForceRefresh = canonicalName === "sync";
|
|
3892
3468
|
let data = null;
|
|
3893
3469
|
let stats = null;
|
|
@@ -3972,20 +3548,6 @@ Both are required. Configure the MCP server in your AI tool's MCP settings, then
|
|
|
3972
3548
|
}
|
|
3973
3549
|
throw error;
|
|
3974
3550
|
}
|
|
3975
|
-
if (FIGMA_DESIGN_PROMPT_NAMES.has(name) && cachedMcpTier !== "pro") {
|
|
3976
|
-
return {
|
|
3977
|
-
description: "Pro Figma required",
|
|
3978
|
-
messages: [
|
|
3979
|
-
{
|
|
3980
|
-
role: "user",
|
|
3981
|
-
content: {
|
|
3982
|
-
type: "text",
|
|
3983
|
-
text: "This design system does not have Pro Figma access. The design-in-Figma prompt is available when the design system owner has a Pro subscription. Sync to Figma (/--sync-to-figma) is available on all tiers."
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
]
|
|
3987
|
-
};
|
|
3988
|
-
}
|
|
3989
3551
|
const buildCategoryPrompt = (category, instructions) => {
|
|
3990
3552
|
const lines = [];
|
|
3991
3553
|
lines.push(`## Design System Information`);
|
|
@@ -4232,35 +3794,6 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
|
|
|
4232
3794
|
]
|
|
4233
3795
|
};
|
|
4234
3796
|
}
|
|
4235
|
-
case "design-in-figma": {
|
|
4236
|
-
return {
|
|
4237
|
-
description: "Design in Figma using granular MCP commands: 3 passes (wireframe \u2192 tokens \u2192 audit), screenshot checks, then finalize and summarise",
|
|
4238
|
-
messages: [
|
|
4239
|
-
{
|
|
4240
|
-
role: "user",
|
|
4241
|
-
content: {
|
|
4242
|
-
type: "text",
|
|
4243
|
-
text: `You must **design in Figma** by calling a **sequence of MCP tools**\u2014do not generate or run any JavaScript code. Follow the design-in-Figma skill if present; otherwise follow this **three-pass flow**.
|
|
4244
|
-
|
|
4245
|
-
Prerequisites: Figma bridge runs inside this MCP server. In Figma, run the Atomix plugin and tap Connect; leave the plugin open.
|
|
4246
|
-
|
|
4247
|
-
**Setup (once):** syncToFigma \u2192 createDesignPlaceholder (save \`frameId\`) \u2192 getAIToolRules + listTokens \u2192 resolveFigmaIdsForTokens. Use the returned \`resolved\` map and \`frameId\` for all passes.
|
|
4248
|
-
|
|
4249
|
-
**Pass 1 \u2014 Layout and contents (wireframe)**
|
|
4250
|
-
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.
|
|
4251
|
-
|
|
4252
|
-
**Pass 2 \u2014 Apply design tokens and AI rules**
|
|
4253
|
-
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.
|
|
4254
|
-
|
|
4255
|
-
**Pass 3 \u2014 Confirm no hardcoded values**
|
|
4256
|
-
Audit: ensure no raw px, hex, or font values remain; every fill and text style must use an id from \`resolved\`. Call **getDesignScreenshot** once more. If everything is correct: call **finalizeDesignFrame** with \`frameId\`, \`name\` = a short description of the design + " \u2705", and \`fillVariableId\` (or \`fillPaintStyleId\`) from resolved for the surface/background so the placeholder gray is removed. Then **summarise** what was built and any fixes made across the three passes.
|
|
4257
|
-
|
|
4258
|
-
If the bridge is not reachable: tell the user to run the Atomix plugin in Figma and tap Connect, then retry.`
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
]
|
|
4262
|
-
};
|
|
4263
|
-
}
|
|
4264
3797
|
case "refactor": {
|
|
4265
3798
|
if (!lastSyncAffectedTokens) {
|
|
4266
3799
|
return {
|
|
@@ -4385,8 +3918,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4385
3918
|
|
|
4386
3919
|
- 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.
|
|
4387
3920
|
- Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
|
|
4388
|
-
- Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links.
|
|
4389
|
-
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md)
|
|
3921
|
+
- Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
|
|
3922
|
+
- Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
|
|
4390
3923
|
- Do not write, create, or add anything in Phase 1.
|
|
4391
3924
|
|
|
4392
3925
|
## Phase 2 \u2013 Report and ask
|
|
@@ -4400,7 +3933,6 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
|
|
|
4400
3933
|
- Run only when the user has said yes (all or specific items).
|
|
4401
3934
|
- For each approved item:
|
|
4402
3935
|
- **Skill:** Write the skill content from getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md).
|
|
4403
|
-
- **Figma design skill:** Only if getDependencies returned \`skillFigmaDesign\`, write \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` (.cursor/skills/atomix-ds/FIGMA-SKILL.md). Use this when designing in Figma so the agent follows principal-product-designer rules and prefers existing Figma variables. If \`skillFigmaDesign\` was not in the response, do not add this file.
|
|
4404
3936
|
- **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.
|
|
4405
3937
|
- **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.
|
|
4406
3938
|
- **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
|