@atomixstudio/mcp 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- FIGMA_DESIGN_CATALOG,
4
- FIGMA_DESIGN_SKILL_MD,
5
- buildFigmaPayloadsFromDS,
6
- buildResolvers,
7
- formatCatalogForMCP,
8
- getDesignMethodNames,
9
- getQueryMethodNames,
10
- isAllowedMethod,
11
- normalizeBridgeMethod,
12
- resolveStepParams
13
- } from "./chunk-426RNS3G.js";
14
2
 
15
3
  // src/index.ts
16
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -1227,180 +1215,6 @@ function getTokenStats(data) {
1227
1215
  // src/index.ts
1228
1216
  import * as path from "path";
1229
1217
  import * as fs from "fs";
1230
- import { execSync } from "child_process";
1231
- import { platform } from "os";
1232
- import WebSocket, { WebSocketServer } from "ws";
1233
- var FIGMA_BRIDGE_PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 8765;
1234
- var FIGMA_BRIDGE_HOST = process.env.FIGMA_BRIDGE_HOST || "127.0.0.1";
1235
- var FIGMA_BRIDGE_TIMEOUT_MS = 15e3;
1236
- var FIGMA_BRIDGE_TOKEN = process.env.FIGMA_BRIDGE_TOKEN || null;
1237
- var FIGMA_CONNECTION_INSTRUCTIONS = {
1238
- 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.",
1239
- connect: 'In the plugin UI, tap **Connect** and wait until the status shows "Connected".',
1240
- 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."
1241
- };
1242
- var bridgeWss = null;
1243
- var pluginWs = null;
1244
- var pendingBridgeRequests = /* @__PURE__ */ new Map();
1245
- function ensureFigmaBridgePortFree(port) {
1246
- const portStr = String(port);
1247
- const ourPid = String(process.pid);
1248
- try {
1249
- if (platform() === "win32") {
1250
- const out = execSync(`netstat -ano`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1251
- const pids = /* @__PURE__ */ new Set();
1252
- for (const line of out.split(/\r?\n/)) {
1253
- if (line.includes(`:${portStr}`) && line.includes("LISTENING")) {
1254
- const parts = line.trim().split(/\s+/);
1255
- const pid = parts[parts.length - 1];
1256
- if (/^\d+$/.test(pid) && pid !== ourPid) pids.add(pid);
1257
- }
1258
- }
1259
- for (const pid of pids) {
1260
- try {
1261
- execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1262
- console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
1263
- } catch (_) {
1264
- }
1265
- }
1266
- } else {
1267
- const out = execSync(`lsof -ti :${portStr}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1268
- if (!out) return;
1269
- const pids = out.split(/\s+/).filter((p) => p && p !== ourPid);
1270
- for (const pid of pids) {
1271
- try {
1272
- execSync(`kill -9 ${pid}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
1273
- console.error(`[atomix-mcp] Freed Figma bridge port ${port} (killed PID ${pid})`);
1274
- } catch (_) {
1275
- }
1276
- }
1277
- }
1278
- } catch {
1279
- }
1280
- }
1281
- function startFigmaBridge() {
1282
- if (bridgeWss) return;
1283
- try {
1284
- ensureFigmaBridgePortFree(FIGMA_BRIDGE_PORT);
1285
- bridgeWss = new WebSocketServer({
1286
- host: FIGMA_BRIDGE_HOST,
1287
- port: FIGMA_BRIDGE_PORT,
1288
- clientTracking: true
1289
- });
1290
- bridgeWss.on("connection", (ws, req) => {
1291
- const url = req.url || "";
1292
- const params = new URLSearchParams(url.startsWith("/") ? url.slice(1) : url);
1293
- const token = params.get("token");
1294
- const role = params.get("role");
1295
- if (FIGMA_BRIDGE_TOKEN && token !== FIGMA_BRIDGE_TOKEN) {
1296
- ws.close(4003, "Invalid or missing bridge token");
1297
- return;
1298
- }
1299
- if (role !== "plugin") {
1300
- ws.close(4002, "Only role=plugin is accepted (bridge runs in MCP server)");
1301
- return;
1302
- }
1303
- if (pluginWs) {
1304
- try {
1305
- pluginWs.close();
1306
- } catch (_) {
1307
- }
1308
- pluginWs = null;
1309
- }
1310
- pluginWs = ws;
1311
- ws.on("message", (raw) => {
1312
- const text = typeof raw === "string" ? raw : raw.toString("utf8");
1313
- let msg;
1314
- try {
1315
- msg = JSON.parse(text);
1316
- } catch {
1317
- return;
1318
- }
1319
- const parsed = msg;
1320
- if (parsed?.type === "ping" && typeof parsed.id === "string") {
1321
- try {
1322
- ws.send(JSON.stringify({ type: "pong", id: parsed.id }));
1323
- } catch (_) {
1324
- }
1325
- return;
1326
- }
1327
- if (typeof parsed.id === "string" && ("result" in parsed || "error" in parsed)) {
1328
- const pending = pendingBridgeRequests.get(parsed.id);
1329
- if (pending) {
1330
- clearTimeout(pending.timeout);
1331
- pendingBridgeRequests.delete(parsed.id);
1332
- if (parsed.error) pending.reject(new Error(parsed.error));
1333
- else pending.resolve(parsed.result);
1334
- }
1335
- }
1336
- });
1337
- ws.on("close", () => {
1338
- if (pluginWs === ws) pluginWs = null;
1339
- });
1340
- ws.on("error", () => {
1341
- if (pluginWs === ws) pluginWs = null;
1342
- });
1343
- });
1344
- bridgeWss.on("listening", () => {
1345
- console.error(`[atomix-mcp] Figma bridge listening on ws://${FIGMA_BRIDGE_HOST}:${FIGMA_BRIDGE_PORT} (local only)`);
1346
- if (FIGMA_BRIDGE_TOKEN) {
1347
- console.error("[atomix-mcp] Figma bridge token required (FIGMA_BRIDGE_TOKEN)");
1348
- }
1349
- });
1350
- bridgeWss.on("error", (err) => {
1351
- console.error("[atomix-mcp] Figma bridge server error:", err);
1352
- });
1353
- } catch (err) {
1354
- console.error("[atomix-mcp] Failed to start Figma bridge:", err);
1355
- }
1356
- }
1357
- function closeFigmaBridge() {
1358
- if (pluginWs) {
1359
- try {
1360
- pluginWs.close();
1361
- } catch (_) {
1362
- }
1363
- pluginWs = null;
1364
- }
1365
- if (bridgeWss) {
1366
- try {
1367
- bridgeWss.close();
1368
- } catch (_) {
1369
- }
1370
- bridgeWss = null;
1371
- }
1372
- }
1373
- function isBridgeReachable() {
1374
- return Promise.resolve(!!(pluginWs && pluginWs.readyState === WebSocket.OPEN));
1375
- }
1376
- function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS) {
1377
- const normalized = normalizeBridgeMethod(method);
1378
- if (!isAllowedMethod(normalized)) {
1379
- return Promise.reject(new Error(`Bridge method not allowed: ${method}`));
1380
- }
1381
- const ws = pluginWs;
1382
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1383
- return Promise.reject(
1384
- new Error("Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect.")
1385
- );
1386
- }
1387
- const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1388
- return new Promise((resolve3, reject) => {
1389
- const timeout = setTimeout(() => {
1390
- if (pendingBridgeRequests.delete(id)) {
1391
- reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
1392
- }
1393
- }, timeoutMs);
1394
- pendingBridgeRequests.set(id, { resolve: resolve3, reject, timeout });
1395
- try {
1396
- ws.send(JSON.stringify({ id, method: normalized, params }));
1397
- } catch (e) {
1398
- pendingBridgeRequests.delete(id);
1399
- clearTimeout(timeout);
1400
- reject(e instanceof Error ? e : new Error(String(e)));
1401
- }
1402
- });
1403
- }
1404
1218
  function parseArgs() {
1405
1219
  const args = process.argv.slice(2);
1406
1220
  let dsId2 = null;
@@ -1427,7 +1241,7 @@ function parseArgs() {
1427
1241
  var cliArgs = parseArgs();
1428
1242
  var { dsId, apiKey, accessToken } = cliArgs;
1429
1243
  var apiBase = cliArgs.apiBase || "https://atomix.studio";
1430
- var MCP_VERSION = "1.0.34";
1244
+ var MCP_VERSION = "1.0.36";
1431
1245
  var cachedData = null;
1432
1246
  var cachedETag = null;
1433
1247
  var cachedMcpTier = null;
@@ -1601,7 +1415,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1601
1415
  if (cachedMcpTier === "pro") {
1602
1416
  console.error("[Atomix MCP] Resolved tier = pro.");
1603
1417
  } else if (cachedMcpTier === "free") {
1604
- console.error("[Atomix MCP] Resolved tier = free. syncToFigma and /--sync-to-figma are available.");
1418
+ console.error("[Atomix MCP] Resolved tier = free.");
1605
1419
  }
1606
1420
  } catch (err) {
1607
1421
  const msg = err instanceof Error ? err.message : String(err);
@@ -1742,7 +1556,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1742
1556
  },
1743
1557
  {
1744
1558
  name: "syncAll",
1745
- description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
1559
+ description: "Sync tokens, AI rules, skill (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
1746
1560
  inputSchema: {
1747
1561
  type: "object",
1748
1562
  properties: {
@@ -1798,54 +1612,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1798
1612
  properties: {},
1799
1613
  required: []
1800
1614
  }
1801
- },
1802
- {
1803
- name: "syncToFigma",
1804
- 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'.",
1805
- inputSchema: {
1806
- type: "object",
1807
- properties: {},
1808
- required: []
1809
- }
1810
1615
  }
1811
1616
  ];
1812
- if (cachedMcpTier === "pro") {
1813
- toolsList.push({
1814
- name: "designInFigma",
1815
- description: "Design UI in the connected Figma file using the design system tokens. Call with action:'catalog' to discover available bridge methods, their parameters, and the file's variables/styles. Call with action:'query' to read from Figma: get_selection (current selection), get_node_info (nodeId), get_design_screenshot (frameId; returns PNG image for review). Call with action:'execute' and an array of steps to create the design on canvas. Requires the Atomix Figma plugin to be connected.",
1816
- inputSchema: {
1817
- type: "object",
1818
- properties: {
1819
- action: {
1820
- type: "string",
1821
- enum: ["catalog", "query", "execute"],
1822
- description: "catalog = discover methods + file context; query = read selection/node/screenshot; execute = run design steps"
1823
- },
1824
- queryMethod: {
1825
- type: "string",
1826
- description: "Required when action is 'query'. One of: get_selection, get_node_info, get_document_info, get_design_screenshot, get_figma_variables_and_styles, list_local_components, get_component_catalog, get_variable_collection_modes, get_frame_variable_mode."
1827
- },
1828
- queryParams: {
1829
- type: "object",
1830
- description: "Optional params for query. get_node_info needs { nodeId }. get_design_screenshot needs { frameId } and optional { scale }."
1831
- },
1832
- steps: {
1833
- type: "array",
1834
- description: "Required when action is 'execute'. Array of { method, params } design commands.",
1835
- items: {
1836
- type: "object",
1837
- properties: {
1838
- method: { type: "string" },
1839
- params: { type: "object" }
1840
- },
1841
- required: ["method"]
1842
- }
1843
- }
1844
- },
1845
- required: ["action"]
1846
- }
1847
- });
1848
- }
1849
1617
  return { tools: toolsList };
1850
1618
  });
1851
1619
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -1988,7 +1756,7 @@ Version: ${designSystemData.meta.version}`,
1988
1756
  ` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
1989
1757
  changeLine,
1990
1758
  "",
1991
- "Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md",
1759
+ "Would write skills: .cursor/skills/atomix-ds/SKILL.md",
1992
1760
  "",
1993
1761
  "Call syncAll again with dryRun: false to apply."
1994
1762
  ].filter(Boolean).join("\n");
@@ -2194,7 +1962,7 @@ Version: ${designSystemData.meta.version}`,
2194
1962
  const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
2195
1963
  console.error(`[getRules] Fetching: ${rulesUrl}${topic ? ` topic=${topic}` : ""}`);
2196
1964
  const headers = { "Content-Type": "application/json" };
2197
- if (apiKey) headers["x-api-key"] = apiKey;
1965
+ if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
2198
1966
  try {
2199
1967
  const response = await fetch(rulesUrl, { headers });
2200
1968
  console.error(`[getRules] Response status: ${response.status}`);
@@ -2470,7 +2238,7 @@ Version: ${designSystemData.meta.version}`,
2470
2238
  const dsExportedAt = data.meta.exportedAt;
2471
2239
  const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
2472
2240
  const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
2473
- const dependencySkills = getSyncDependencySkills(data, dsVersion, dsExportedAt);
2241
+ const dependencySkills = getSyncDependencySkills(dsVersion, dsExportedAt);
2474
2242
  if (dryRun) {
2475
2243
  const skillList = dependencySkills.map((s) => s.path).join(", ");
2476
2244
  parts.push(`Would write skills: ${skillList}`);
@@ -2514,9 +2282,6 @@ ${reportText}` }]
2514
2282
  const lib = icons?.library || "lucide";
2515
2283
  const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
2516
2284
  const skillsManifestEntry = { skill: ".cursor/skills/atomix-ds/SKILL.md", syncedAtVersion: String(data.meta.version ?? "1.0.0") };
2517
- if (dependencySkills.some((s) => s.shortName === "figma-design-SKILL.md")) {
2518
- skillsManifestEntry.figmaDesignSkill = ".cursor/skills/atomix-ds/figma-design-SKILL.md";
2519
- }
2520
2285
  const manifest = {
2521
2286
  designSystem: { name: data.meta.name, version: data.meta.version },
2522
2287
  tokenFile: skipTokens ? void 0 : output,
@@ -2593,7 +2358,7 @@ ${tokenResponseText}` : "") + validationBlock;
2593
2358
  };
2594
2359
  }
2595
2360
  case "getDependencies": {
2596
- const platform2 = args?.platform;
2361
+ const platform = args?.platform;
2597
2362
  const stack = args?.stack;
2598
2363
  const tokens = data.tokens;
2599
2364
  const typography = tokens?.typography;
@@ -2626,28 +2391,22 @@ ${tokenResponseText}` : "") + validationBlock;
2626
2391
  performanceHint: "Link fonts via URL (e.g. Google Fonts <link> or CSS @import); no need to download font files or add them to the repo. Prefer font-display: swap when possible. You must also build a complete typeset CSS: call listTypesets to get every typeset from the design system, then emit one CSS class per typeset (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too so the result is 1:1 with the DS. Use the CSS variable names returned by listTypesets. Do not create a file that only contains a font import."
2627
2392
  },
2628
2393
  skill: (() => {
2629
- const list = getSyncDependencySkills(data, String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
2394
+ const list = getSyncDependencySkills(String(data.meta.version ?? "1.0.0"), data.meta.exportedAt);
2630
2395
  const generic = list.find((s) => s.shortName === "SKILL.md");
2631
2396
  return generic ? { path: generic.path, content: GENERIC_SKILL_MD } : { path: ".cursor/skills/atomix-ds/SKILL.md", content: GENERIC_SKILL_MD };
2632
2397
  })(),
2633
- ...getEffectiveTier() === "pro" ? {
2634
- skillFigmaDesign: {
2635
- path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
2636
- content: FIGMA_DESIGN_SKILL_MD
2637
- }
2638
- } : {},
2639
2398
  tokenFiles: {
2640
2399
  files: ["tokens.css", "tokens.json"],
2641
2400
  copyInstructions: "Call the syncAll MCP tool to create the token file, skills, and atomix-dependencies.json; do not only suggest the user run sync later."
2642
2401
  },
2643
- showcase: platform2 === "web" || !platform2 ? {
2402
+ showcase: platform === "web" || !platform ? {
2644
2403
  path: "atomix-setup-showcase.html",
2645
2404
  template: SHOWCASE_HTML_TEMPLATE,
2646
2405
  substitutionInstructions: 'The synced token file (from syncAll) always uses the --atmx- prefix for every CSS variable. Keep all var(--atmx-*) references in the template; do not remove or change the prefix. Replace placeholders with values from the synced token file. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css). {{TYPESETS_LINK}} = if a typeset CSS file was created, the full <link rel=\\"stylesheet\\" href=\\"typesets.css\\"> tag, otherwise empty string. {{DS_NAME}} = design system name. {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl), or empty string if no typeset file. {{LARGEST_BODY_TYPESET_CLASS}} = largest body typeset class from listTypesets (body role, largest font size; e.g. typeset-body-lg), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = default body typeset class from listTypesets (e.g. typeset-body-md), or empty string. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). Icon on circle uses luminance of brand primary (script sets white or black); no semantic foreground var. {{BUTTON_PADDING_VAR}} = var(--atmx-spacing-scale-md) or closest spacing token for button padding. {{BUTTON_HEIGHT_VAR}} = var(--atmx-sizing-height-md) or closest height token. {{BUTTON_RADIUS_VAR}} = var(--atmx-radius-scale-md) or var(--atmx-radius-scale-lg). {{CIRCLE_PADDING_VAR}} = var(--atmx-spacing-scale-md) or var(--atmx-spacing-scale-sm) for icon circle padding. {{ICON_SIZE_VAR}} = var(--atmx-sizing-icon-md) or var(--atmx-sizing-icon-lg). {{CHECK_ICON_SVG}} = inline SVG for Check icon from the design system icon library (getDependencies.iconLibrary.package: lucide-react, @heroicons/react, or phosphor-react). Use 24x24 viewBox; stroke=\\"currentColor\\" for Lucide/Heroicons, fill=\\"currentColor\\" for Phosphor so the script can set icon color by luminance. If unavailable, use: <svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M20 6L9 17l-5-5\\"/></svg>. Do not invent CSS variable names; use only vars that exist in the export.'
2647
2406
  } : void 0,
2648
2407
  meta: {
2649
2408
  dsName: data.meta.name,
2650
- platform: platform2 ?? void 0,
2409
+ platform: platform ?? void 0,
2651
2410
  stack: stack ?? void 0,
2652
2411
  designSystemVersion: data.meta.version ?? "1.0.0",
2653
2412
  designSystemExportedAt: data.meta.exportedAt
@@ -2660,482 +2419,13 @@ ${tokenResponseText}` : "") + validationBlock;
2660
2419
  }]
2661
2420
  };
2662
2421
  }
2663
- case "syncToFigma": {
2664
- const payloads = buildFigmaPayloadsFromDS(data);
2665
- const out = {};
2666
- const agentStartBridge = `The Figma bridge runs with this MCP server. Ensure your AI environment has this MCP server configured and running, then in Figma run the Atomix plugin and tap Connect. Then call **syncToFigma** again.`;
2667
- const userSteps = [
2668
- FIGMA_CONNECTION_INSTRUCTIONS.startBridge,
2669
- FIGMA_CONNECTION_INSTRUCTIONS.installAndRun,
2670
- FIGMA_CONNECTION_INSTRUCTIONS.connect,
2671
- "Then run Sync to Figma again."
2672
- ].join(" ");
2673
- try {
2674
- const reachable = await isBridgeReachable();
2675
- if (!reachable) {
2676
- out.bridgeNotRunning = true;
2677
- out.agentInstruction = agentStartBridge;
2678
- out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
2679
- out.figmaPayload = {
2680
- colorVariableCollections: payloads.colorVariableCollections,
2681
- paintStyles: payloads.paintStyles,
2682
- textStyles: payloads.textStyles,
2683
- numberVariableCollections: payloads.numberVariableCollections,
2684
- effectStyles: payloads.effectStyles
2685
- };
2686
- return {
2687
- content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
2688
- };
2689
- }
2690
- const colorResults = [];
2691
- for (const coll of payloads.colorVariableCollections) {
2692
- if (coll.variables.length > 0) {
2693
- const result = await sendBridgeRequest("create_color_variables", {
2694
- collectionName: coll.collectionName,
2695
- modes: coll.modes,
2696
- variables: coll.variables,
2697
- removeVariablesNotInPayload: true,
2698
- applyScopes: coll.applyScopes
2699
- });
2700
- colorResults.push({ collectionName: coll.collectionName, result });
2701
- }
2702
- }
2703
- if (colorResults.length > 0) {
2704
- out.colorVariables = colorResults.length === 1 ? colorResults[0].result : colorResults;
2705
- }
2706
- if (payloads.paintStyles.length > 0) {
2707
- out.paintStyles = await sendBridgeRequest("create_paint_styles", {
2708
- styles: payloads.paintStyles,
2709
- removePaintStylesNotInPayload: true
2710
- });
2711
- }
2712
- if (payloads.numberVariableCollections.length > 0) {
2713
- const numberResults = [];
2714
- try {
2715
- for (const coll of payloads.numberVariableCollections) {
2716
- const result = await sendBridgeRequest("create_number_variables", {
2717
- collectionName: coll.collectionName,
2718
- variables: coll.variables.map((v) => ({ name: v.name, value: v.value })),
2719
- scopes: coll.scopes,
2720
- removeVariablesNotInPayload: true
2721
- });
2722
- numberResults.push({ categoryKey: coll.categoryKey, result });
2723
- }
2724
- out.numberVariables = numberResults;
2725
- } catch (e) {
2726
- const msg = e instanceof Error ? e.message : String(e);
2727
- out.numberVariables = { error: msg };
2728
- if (msg.includes("Method not allowed") && msg.includes("create_number_variables")) {
2729
- 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.";
2730
- }
2731
- }
2732
- }
2733
- if (payloads.textStyles.length > 0) {
2734
- out.textStyles = await sendBridgeRequest("create_text_styles", {
2735
- styles: payloads.textStyles,
2736
- removeTextStylesNotInPayload: true
2737
- });
2738
- }
2739
- if (payloads.effectStyles.length > 0) {
2740
- out.effectStyles = await sendBridgeRequest("create_effect_styles", {
2741
- styles: payloads.effectStyles,
2742
- removeShadowStylesNotInPayload: true
2743
- });
2744
- }
2745
- out.figmaPayload = {
2746
- colorVariableCollections: payloads.colorVariableCollections,
2747
- paintStyles: payloads.paintStyles,
2748
- textStyles: payloads.textStyles,
2749
- numberVariableCollections: payloads.numberVariableCollections,
2750
- effectStyles: payloads.effectStyles
2751
- };
2752
- } catch (e) {
2753
- out.error = e instanceof Error ? e.message : String(e);
2754
- out.figmaPayload = {
2755
- colorVariableCollections: payloads.colorVariableCollections,
2756
- paintStyles: payloads.paintStyles,
2757
- textStyles: payloads.textStyles,
2758
- numberVariableCollections: payloads.numberVariableCollections,
2759
- effectStyles: payloads.effectStyles
2760
- };
2761
- const errMsg = out.error.toLowerCase();
2762
- const connectionFailure = errMsg.includes("econnrefused") || errMsg.includes("bridge timeout") || errMsg.includes("websocket") || errMsg.includes("network");
2763
- if (connectionFailure) {
2764
- out.bridgeNotRunning = true;
2765
- out.agentInstruction = agentStartBridge;
2766
- out.userInstruction = `If the bridge still does not connect: ${userSteps}`;
2767
- } else if (errMsg.includes("plugin not connected") || errMsg.includes("figma plugin")) {
2768
- out.userInstruction = `${FIGMA_CONNECTION_INSTRUCTIONS.installAndRun} ${FIGMA_CONNECTION_INSTRUCTIONS.connect}`;
2769
- }
2770
- }
2771
- const textStylesResult = out.textStyles;
2772
- if (textStylesResult?.failed && textStylesResult.failures?.length) {
2773
- const firstReason = textStylesResult.failures[0].reason;
2774
- out.summary = `Text styles: ${textStylesResult.failed} could not be created. ${firstReason}`;
2775
- }
2776
- if (out.numberVariablesHint) {
2777
- out.summary = [out.summary, out.numberVariablesHint].filter(Boolean).join(" ");
2778
- }
2779
- const summaryParts = [];
2780
- const colorResult = out.colorVariables;
2781
- if (colorResult) {
2782
- const results = Array.isArray(colorResult) ? colorResult : [{ result: colorResult }];
2783
- let totalSynced = 0;
2784
- let totalRemoved = 0;
2785
- for (const r of results) {
2786
- const res = r.result;
2787
- if (res?.variableNames?.length) totalSynced += res.variableNames.length;
2788
- if ((res?.removed ?? 0) > 0) totalRemoved += res.removed ?? 0;
2789
- }
2790
- if (totalSynced > 0 || totalRemoved > 0) {
2791
- const parts = [];
2792
- if (totalSynced > 0) parts.push(`${totalSynced} synced`);
2793
- if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
2794
- summaryParts.push(`Colors: ${parts.join(", ")}.`);
2795
- }
2796
- }
2797
- const paintResult = out.paintStyles;
2798
- if (paintResult) {
2799
- const c = paintResult.created ?? 0;
2800
- const u = paintResult.updated ?? 0;
2801
- const r = paintResult.removed ?? 0;
2802
- if (c + u + r > 0) {
2803
- const parts = [];
2804
- if (c > 0) parts.push(`${c} created`);
2805
- if (u > 0) parts.push(`${u} updated`);
2806
- if (r > 0) parts.push(`${r} removed`);
2807
- summaryParts.push(`Paint styles: ${parts.join(", ")}.`);
2808
- }
2809
- }
2810
- const effectResult = out.effectStyles;
2811
- if (effectResult) {
2812
- const c = effectResult.created ?? 0;
2813
- const u = effectResult.updated ?? 0;
2814
- const r = effectResult.removed ?? 0;
2815
- if (c + u + r > 0) {
2816
- const parts = [];
2817
- if (c > 0) parts.push(`${c} created`);
2818
- if (u > 0) parts.push(`${u} updated`);
2819
- if (r > 0) parts.push(`${r} removed`);
2820
- summaryParts.push(`Effect styles (shadows): ${parts.join(", ")}.`);
2821
- if (effectResult.removedNames?.length) {
2822
- summaryParts.push(`Removed: ${effectResult.removedNames.join(", ")}.`);
2823
- }
2824
- }
2825
- }
2826
- const numResult = out.numberVariables;
2827
- if (Array.isArray(numResult)) {
2828
- const total = numResult.reduce((acc, r) => acc + (r.result?.variableNames?.length ?? 0), 0);
2829
- const totalRemoved = numResult.reduce((acc, r) => acc + (r.result?.removed ?? 0), 0);
2830
- if (total > 0 || totalRemoved > 0) {
2831
- const parts = [];
2832
- if (total > 0) parts.push(`${total} synced`);
2833
- if (totalRemoved > 0) parts.push(`${totalRemoved} removed`);
2834
- summaryParts.push(`Number variables: ${parts.join(", ")}.`);
2835
- }
2836
- }
2837
- const textStylesWithRemoved = out.textStyles;
2838
- if (textStylesWithRemoved && (textStylesWithRemoved.created ?? 0) + (textStylesWithRemoved.updated ?? 0) + (textStylesWithRemoved.removed ?? 0) > 0) {
2839
- const c = textStylesWithRemoved.created ?? 0;
2840
- const u = textStylesWithRemoved.updated ?? 0;
2841
- const r = textStylesWithRemoved.removed ?? 0;
2842
- const parts = [];
2843
- if (c > 0) parts.push(`${c} created`);
2844
- if (u > 0) parts.push(`${u} updated`);
2845
- if (r > 0) parts.push(`${r} removed`);
2846
- summaryParts.push(`Text styles: ${parts.join(", ")}.`);
2847
- }
2848
- if (summaryParts.length > 0 && !out.error) {
2849
- out.summary = [out.summary, summaryParts.join(" ")].filter(Boolean).join(" ");
2850
- }
2851
- 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.";
2852
- const responseText = out.summary ? `${out.summary}
2853
-
2854
- ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
2855
- return {
2856
- content: [{
2857
- type: "text",
2858
- text: responseText
2859
- }],
2860
- ...out.error ? { isError: true } : {}
2861
- };
2862
- }
2863
- case "designInFigma": {
2864
- if (cachedMcpTier !== "pro") {
2865
- return {
2866
- content: [{ type: "text", text: JSON.stringify({ error: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio" }, null, 2) }],
2867
- isError: true
2868
- };
2869
- }
2870
- const action = args?.action;
2871
- if (action === "catalog") {
2872
- try {
2873
- const reachable = await isBridgeReachable();
2874
- let fileCtx;
2875
- if (reachable) {
2876
- try {
2877
- const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
2878
- fileCtx = raw;
2879
- } catch {
2880
- }
2881
- }
2882
- const localVars = [];
2883
- const libVars = [];
2884
- const textStyleNames = [];
2885
- const effectStyleNames = [];
2886
- if (fileCtx) {
2887
- for (const coll of fileCtx.variableCollections ?? []) {
2888
- for (const v of coll.variables) localVars.push(v.name);
2889
- }
2890
- for (const coll of fileCtx.variableCollectionsLibrary ?? []) {
2891
- for (const v of coll.variables) libVars.push(v.name);
2892
- }
2893
- for (const s of fileCtx.textStyles ?? []) textStyleNames.push(s.name);
2894
- for (const s of fileCtx.effectStyles ?? []) effectStyleNames.push(s.name);
2895
- }
2896
- const catalogPayload = formatCatalogForMCP(FIGMA_DESIGN_CATALOG, {
2897
- localVariables: localVars,
2898
- libraryVariables: libVars,
2899
- textStyles: textStyleNames,
2900
- effectStyles: effectStyleNames
2901
- });
2902
- let designAssets = void 0;
2903
- if (reachable) {
2904
- try {
2905
- const raw = await sendBridgeRequest("get_component_catalog", {});
2906
- if (raw && typeof raw === "object") {
2907
- designAssets = raw;
2908
- }
2909
- } catch {
2910
- }
2911
- }
2912
- return {
2913
- content: [{
2914
- type: "text",
2915
- text: JSON.stringify({
2916
- ...catalogPayload,
2917
- pluginConnected: reachable,
2918
- ...designAssets ? { designAssets } : {},
2919
- ...reachable ? {
2920
- queryMethods: getQueryMethodNames(),
2921
- queryHint: "Use action:'query' with queryMethod (e.g. get_selection, get_node_info, get_design_screenshot) and optional queryParams to read selection, node details, or a frame screenshot for review."
2922
- } : {},
2923
- ...reachable ? {} : { hint: "Figma plugin not connected. Open Figma, run Atomix plugin, and tap Connect." }
2924
- }, null, 2)
2925
- }]
2926
- };
2927
- } catch (e) {
2928
- const msg = e instanceof Error ? e.message : String(e);
2929
- return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
2930
- }
2931
- }
2932
- if (action === "query") {
2933
- const queryMethodRaw = args?.queryMethod;
2934
- const queryParams = args?.queryParams ?? {};
2935
- if (!queryMethodRaw || typeof queryMethodRaw !== "string") {
2936
- return {
2937
- content: [{ type: "text", text: JSON.stringify({ error: "query requires queryMethod (e.g. get_selection, get_node_info, get_design_screenshot)" }, null, 2) }],
2938
- isError: true
2939
- };
2940
- }
2941
- const queryMethod = queryMethodRaw.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
2942
- const allowedQueryMethods = new Set(getQueryMethodNames());
2943
- if (!allowedQueryMethods.has(queryMethod)) {
2944
- return {
2945
- content: [{ type: "text", text: JSON.stringify({ error: `Unknown query method "${queryMethod}". Allowed: ${[...allowedQueryMethods].join(", ")}` }, null, 2) }],
2946
- isError: true
2947
- };
2948
- }
2949
- try {
2950
- const reachable = await isBridgeReachable();
2951
- if (!reachable) {
2952
- return {
2953
- content: [{
2954
- type: "text",
2955
- text: JSON.stringify({
2956
- error: "Figma plugin not connected.",
2957
- bridgeNotRunning: true,
2958
- agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma with action:'query' again."
2959
- }, null, 2)
2960
- }],
2961
- isError: true
2962
- };
2963
- }
2964
- const result = await sendBridgeRequest(queryMethod, queryParams);
2965
- const content = [];
2966
- const summary = { method: queryMethod, success: true };
2967
- if (result && typeof result === "object" && "imageBase64" in result && typeof result.imageBase64 === "string") {
2968
- summary.screenshot = "included below (base64 PNG)";
2969
- summary.format = result.format ?? "PNG";
2970
- summary.scale = result.scale;
2971
- content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2972
- content.push({
2973
- type: "image",
2974
- data: result.imageBase64,
2975
- mimeType: "image/png"
2976
- });
2977
- } else {
2978
- summary.result = result;
2979
- content.push({ type: "text", text: JSON.stringify(summary, null, 2) });
2980
- }
2981
- return { content };
2982
- } catch (e) {
2983
- const msg = e instanceof Error ? e.message : String(e);
2984
- return { content: [{ type: "text", text: JSON.stringify({ method: queryMethod, success: false, error: msg }, null, 2) }], isError: true };
2985
- }
2986
- }
2987
- if (action === "execute") {
2988
- const rawSteps = args?.steps;
2989
- if (!rawSteps || !Array.isArray(rawSteps) || rawSteps.length === 0) {
2990
- return {
2991
- content: [{ type: "text", text: JSON.stringify({ error: "steps array is required for action:'execute'" }, null, 2) }],
2992
- isError: true
2993
- };
2994
- }
2995
- try {
2996
- const reachable = await isBridgeReachable();
2997
- if (!reachable) {
2998
- return {
2999
- content: [{
3000
- type: "text",
3001
- text: JSON.stringify({
3002
- error: "Figma plugin not connected.",
3003
- bridgeNotRunning: true,
3004
- agentInstruction: "Run the Atomix plugin in Figma and tap Connect, then call designInFigma again."
3005
- }, null, 2)
3006
- }],
3007
- isError: true
3008
- };
3009
- }
3010
- const designMethods = /* @__PURE__ */ new Set([
3011
- ...getDesignMethodNames(),
3012
- "list_local_components",
3013
- "get_component_catalog",
3014
- "get_design_screenshot",
3015
- "get_variable_collection_modes",
3016
- "get_frame_variable_mode"
3017
- ]);
3018
- const warnings = [];
3019
- let fileCtx;
3020
- try {
3021
- const raw = await sendBridgeRequest("get_figma_variables_and_styles", {});
3022
- fileCtx = raw;
3023
- } catch {
3024
- }
3025
- const resolvers = fileCtx ? buildResolvers(fileCtx) : null;
3026
- const isFigmaNodeId = (s) => /^\d+:\d+$/.test(s);
3027
- let rootFrameId = null;
3028
- let lastCreatedFrameNodeId = null;
3029
- const namedNodeIds = /* @__PURE__ */ new Map();
3030
- const results = [];
3031
- for (let i = 0; i < rawSteps.length; i++) {
3032
- const step = rawSteps[i];
3033
- const method = (step.method ?? "").replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
3034
- if (!designMethods.has(method)) {
3035
- warnings.push({ step: i, method, issue: `Unknown method "${method}" \u2014 skipped` });
3036
- continue;
3037
- }
3038
- let params = { ...step.params ?? {} };
3039
- if (resolvers) {
3040
- params = resolveStepParams(params, resolvers);
3041
- }
3042
- if (rootFrameId) {
3043
- const parentId = params.parentId;
3044
- const frameId = params.frameId;
3045
- if (typeof parentId === "string" && !isFigmaNodeId(parentId)) {
3046
- params.parentId = namedNodeIds.get(parentId) ?? rootFrameId;
3047
- }
3048
- if (typeof frameId === "string" && !isFigmaNodeId(frameId)) {
3049
- params.frameId = namedNodeIds.get(frameId) ?? rootFrameId;
3050
- }
3051
- if (method === "finalize_design_frame" && !params.frameId) {
3052
- params.frameId = rootFrameId;
3053
- }
3054
- const needsParent = method.startsWith("design_create_") || method === "design_create_component";
3055
- if (needsParent && !params.parentId) {
3056
- params.parentId = rootFrameId;
3057
- }
3058
- }
3059
- const nodeId = params.nodeId;
3060
- if (typeof nodeId === "string" && nodeId && !isFigmaNodeId(nodeId)) {
3061
- const resolved = namedNodeIds.get(nodeId) ?? lastCreatedFrameNodeId ?? rootFrameId;
3062
- params.nodeId = resolved ?? nodeId;
3063
- }
3064
- const needsNode = [
3065
- "design_set_auto_layout",
3066
- "design_set_layout_sizing",
3067
- "design_set_effects",
3068
- "design_set_strokes",
3069
- "design_resize_node",
3070
- "design_set_resize_constraints",
3071
- "design_set_layout_constraints",
3072
- "design_set_node_position",
3073
- "design_set_text_properties"
3074
- ].includes(method);
3075
- if (needsNode && !params.nodeId) {
3076
- params.nodeId = lastCreatedFrameNodeId ?? rootFrameId;
3077
- }
3078
- const childId = params.childId;
3079
- if (typeof childId === "string" && childId && !isFigmaNodeId(childId)) {
3080
- params.childId = namedNodeIds.get(childId) ?? childId;
3081
- }
3082
- try {
3083
- const response = await sendBridgeRequest(method, params);
3084
- const res = response;
3085
- if (method === "create_design_placeholder" && typeof res?.frameId === "string") {
3086
- rootFrameId = res.frameId;
3087
- }
3088
- const isCreateMethod = method.startsWith("design_create_") || method === "design_convert_to_component" || method === "design_combine_as_variants" || method === "design_create_frame_from_preset" || method === "design_group_nodes";
3089
- if (isCreateMethod && typeof res?.nodeId === "string") {
3090
- const createdNodeId = res.nodeId;
3091
- if (method === "design_create_frame" || method === "design_create_frame_from_preset") {
3092
- lastCreatedFrameNodeId = createdNodeId;
3093
- if (!rootFrameId && res?.isRoot === true) {
3094
- rootFrameId = createdNodeId;
3095
- }
3096
- }
3097
- const stepName = step.params?.name ?? "";
3098
- if (stepName) namedNodeIds.set(stepName, createdNodeId);
3099
- }
3100
- results.push({ step: i, method, result: response });
3101
- } catch (e) {
3102
- const errMsg = e instanceof Error ? e.message : String(e);
3103
- results.push({ step: i, method, error: errMsg });
3104
- if (!rootFrameId && (method === "design_create_frame" || method === "create_design_placeholder")) break;
3105
- }
3106
- }
3107
- const errorCount = results.filter((r) => r.error).length;
3108
- const success = errorCount === 0;
3109
- return {
3110
- content: [{
3111
- type: "text",
3112
- text: JSON.stringify({
3113
- success,
3114
- stepsExecuted: results.length,
3115
- errors: errorCount,
3116
- warnings,
3117
- results
3118
- }, null, 2)
3119
- }],
3120
- ...errorCount > 0 ? { isError: true } : {}
3121
- };
3122
- } catch (e) {
3123
- const msg = e instanceof Error ? e.message : String(e);
3124
- return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
3125
- }
3126
- }
3127
- return {
3128
- content: [{ type: "text", text: JSON.stringify({ error: "action must be 'catalog', 'query', or 'execute'" }, null, 2) }],
3129
- isError: true
3130
- };
3131
- }
3132
2422
  default:
3133
2423
  return {
3134
2424
  content: [{
3135
2425
  type: "text",
3136
2426
  text: JSON.stringify({
3137
2427
  error: `Unknown tool: ${name}`,
3138
- availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma", "designInFigma"]
2428
+ availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion"]
3139
2429
  }, null, 2)
3140
2430
  }]
3141
2431
  };
@@ -3177,32 +2467,15 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3177
2467
  };
3178
2468
  }
3179
2469
  });
3180
- function getEffectiveTier() {
3181
- return cachedMcpTier;
3182
- }
3183
- function getSyncDependencySkills(data, dsVersion, dsExportedAt) {
3184
- const tier = getEffectiveTier();
3185
- const skills = [];
2470
+ function getSyncDependencySkills(dsVersion, dsExportedAt) {
3186
2471
  const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
3187
- skills.push({
3188
- path: ".cursor/skills/atomix-ds/SKILL.md",
3189
- content: genericWithVersion,
3190
- shortName: "SKILL.md"
3191
- });
3192
- if (tier === "pro") {
3193
- const figmaSkillWithFrontmatter = `---
3194
- name: atomix-figma-design
3195
- description: Figma design skill \u2014 create high-quality, token-bound designs in Figma from any reference (screenshots, images, code snippets, URLs). Covers mobile, web, token application, and component/variant creation. Use with syncToFigma and designInFigma MCP tools when the Atomix plugin is connected.
3196
- ---
3197
- ${FIGMA_DESIGN_SKILL_MD}`;
3198
- const figmaSkillWithVersion = injectSkillVersion(figmaSkillWithFrontmatter, dsVersion, dsExportedAt);
3199
- skills.push({
3200
- path: ".cursor/skills/atomix-ds/figma-design-SKILL.md",
3201
- content: figmaSkillWithVersion,
3202
- shortName: "figma-design-SKILL.md"
3203
- });
3204
- }
3205
- return skills;
2472
+ return [
2473
+ {
2474
+ path: ".cursor/skills/atomix-ds/SKILL.md",
2475
+ content: genericWithVersion,
2476
+ shortName: "SKILL.md"
2477
+ }
2478
+ ];
3206
2479
  }
3207
2480
  function readSkillVersionFromFile(filePath) {
3208
2481
  if (!fs.existsSync(filePath)) return null;
@@ -3306,7 +2579,7 @@ When a semantic token doesn't exist for your use case, use the closest primitive
3306
2579
 
3307
2580
  ### 4. Syncing tokens to a file
3308
2581
 
3309
- \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md and figma-design-SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
2582
+ \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skill (.cursor/skills/atomix-ds/SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
3310
2583
 
3311
2584
  ## Workflow
3312
2585
 
@@ -3565,7 +2838,7 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
3565
2838
  const topicRaw = rulesMatch[1]?.toLowerCase().trim();
3566
2839
  const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3567
2840
  const headers = { "Content-Type": "application/json" };
3568
- if (apiKey) headers["x-api-key"] = apiKey;
2841
+ if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
3569
2842
  const response = await fetch(rulesUrl, { headers });
3570
2843
  if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
3571
2844
  const payload = await response.json();
@@ -3622,12 +2895,8 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
3622
2895
  { name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
3623
2896
  { name: "--rules", description: "Get design system governance rules (optionally by topic: colors, typo, motion, icons, layout, visual)." },
3624
2897
  { name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
3625
- { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
3626
- { name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
2898
+ { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." }
3627
2899
  ];
3628
- if (cachedMcpTier === "pro") {
3629
- prompts.push({ name: "--design-in-figma", description: "Load Figma design skill and use designInFigma tool to design UI in the connected Figma file. Pro only." });
3630
- }
3631
2900
  return { prompts };
3632
2901
  });
3633
2902
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
@@ -3644,7 +2913,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3644
2913
  }]
3645
2914
  };
3646
2915
  }
3647
- const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name === "--design-in-figma" || name === "designInFigma" ? "design-in-figma" : name;
2916
+ const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name;
3648
2917
  const shouldForceRefresh = [
3649
2918
  "hello",
3650
2919
  "atomix-setup",
@@ -3652,9 +2921,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3652
2921
  "design-system-rules",
3653
2922
  // --rules
3654
2923
  "sync",
3655
- "refactor",
3656
- "sync-to-figma",
3657
- "design-in-figma"
2924
+ "refactor"
3658
2925
  ].includes(canonicalName);
3659
2926
  let data = null;
3660
2927
  let stats = null;
@@ -3801,7 +3068,7 @@ ${welcome}`;
3801
3068
  const topic = args?.topic?.toLowerCase().trim();
3802
3069
  const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3803
3070
  const headers = { "Content-Type": "application/json" };
3804
- if (apiKey) headers["x-api-key"] = apiKey;
3071
+ if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
3805
3072
  const response = await fetch(rulesUrl, { headers });
3806
3073
  if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
3807
3074
  const payload = await response.json();
@@ -4000,94 +3267,6 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4000
3267
  ]
4001
3268
  });
4002
3269
  }
4003
- case "sync-to-figma": {
4004
- return withMcpNotice({
4005
- description: "Push design system to Figma via MCP tool",
4006
- messages: [
4007
- {
4008
- role: "user",
4009
- content: {
4010
- type: "text",
4011
- text: `Call the MCP tool **syncToFigma** now (no arguments). It pushes the design system into the open Figma file via the built-in bridge and Atomix plugin. Do not use the Figma REST API or external scripts. If the response includes \`bridgeNotRunning\` and \`agentInstruction\`, ensure your AI environment has this MCP server running, then in Figma run the Atomix plugin and tap Connect, then call syncToFigma again. Only if that fails, tell the user: (1) Ensure this MCP server is configured and running in your AI tool. (2) In Figma, open and run the Atomix plugin, then tap **Connect**. (3) Run Sync to Figma again. After a successful sync, report the **summary** from the response (what was created, updated, or removed)\u2014do not say "we added the whole design system" when only some items changed.`
4012
- }
4013
- }
4014
- ]
4015
- });
4016
- }
4017
- case "design-in-figma": {
4018
- if (getEffectiveTier() !== "pro") {
4019
- return withMcpNotice({
4020
- description: "Design in Figma (Pro required)",
4021
- messages: [
4022
- {
4023
- role: "user",
4024
- content: {
4025
- type: "text",
4026
- text: "designInFigma requires a Pro subscription. Upgrade at https://atomix.studio to use Design in Figma."
4027
- }
4028
- }
4029
- ]
4030
- });
4031
- }
4032
- if (!data) {
4033
- return withMcpNotice({
4034
- description: "Design in Figma",
4035
- messages: [
4036
- {
4037
- role: "user",
4038
- content: {
4039
- type: "text",
4040
- text: "Failed to fetch design system. Check your --ds-id and --atomix-token configuration, then try /--design-in-figma again."
4041
- }
4042
- }
4043
- ]
4044
- });
4045
- }
4046
- const figmaSkillPath = path.resolve(process.cwd(), ".cursor/skills/atomix-ds/figma-design-SKILL.md");
4047
- const skillExists = fs.existsSync(figmaSkillPath);
4048
- const currentVersion = String(data.meta.version ?? "1.0.0");
4049
- const fileVersion = skillExists ? readSkillVersionFromFile(figmaSkillPath) : null;
4050
- const isOutdated = !skillExists || fileVersion === null || fileVersion !== currentVersion;
4051
- if (isOutdated) {
4052
- return withMcpNotice({
4053
- description: "Figma design skill missing or outdated",
4054
- messages: [
4055
- {
4056
- role: "user",
4057
- content: {
4058
- type: "text",
4059
- text: skillExists ? `The Figma design skill at \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` is outdated (design system version ${currentVersion}). Run **/--sync** to update the skill and other design system files, then run **/--design-in-figma** again.` : `The Figma design skill is missing. Run **/--sync** to write \`.cursor/skills/atomix-ds/figma-design-SKILL.md\` (and other design system files), then run **/--design-in-figma** again.`
4060
- }
4061
- }
4062
- ]
4063
- });
4064
- }
4065
- return withMcpNotice({
4066
- description: "Load Figma design skill and use designInFigma tool",
4067
- messages: [
4068
- {
4069
- role: "user",
4070
- content: {
4071
- type: "text",
4072
- text: `You are using **/--design-in-figma**: load the Figma design skill below and use the **designInFigma** MCP tool to design UI in the connected Figma file.
4073
-
4074
- **Instructions:**
4075
- 1. Treat the following as the Figma design skill (advisory and generative UI on the Figma canvas). Follow it when creating or editing designs.
4076
- 2. Call **designInFigma** with \`action: "catalog"\` first to discover available bridge methods, file variables/styles, and query/execute capabilities.
4077
- 3. Use \`action: "query"\` to read from Figma (e.g. get_selection, get_node_info, get_design_screenshot).
4078
- 4. Use \`action: "execute"\` with an array of steps to create or modify the design on the canvas.
4079
- 5. If the response includes \`bridgeNotRunning\` or \`pluginConnected: false\`, tell the user to run the Atomix plugin in Figma and tap Connect, then try again.
4080
-
4081
- ---
4082
-
4083
- ## Figma design skill
4084
-
4085
- ${FIGMA_DESIGN_SKILL_MD}`
4086
- }
4087
- }
4088
- ]
4089
- });
4090
- }
4091
3270
  case "refactor": {
4092
3271
  const refactorOutput = args?.output || "./tokens.css";
4093
3272
  const refactorFormat = args?.format || "css";
@@ -4234,8 +3413,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4234
3413
 
4235
3414
  - Resolve platform/stack: infer from the project (e.g. package.json, build.gradle, Xcode) or ask once: "Which platform? (e.g. web, Android, iOS)" and if relevant "Which stack? (e.g. React, Vue, Next, Swift, Kotlin)." Do not assume a default.
4236
3415
  - Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
4237
- - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
4238
- - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skills (SKILL.md and figma-design-SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
3416
+ - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
3417
+ - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (SKILL.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4239
3418
  - Do not write, create, or add anything in Phase 1.
4240
3419
 
4241
3420
  ## Phase 2 \u2013 Report and ask
@@ -4248,8 +3427,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4248
3427
 
4249
3428
  - Run only when the user has said yes (all or specific items).
4250
3429
  - For each approved item:
4251
- - **Skills:** Prefer calling **syncAll** (it writes both skills into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` and \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` under the project root.
4252
- - **Token file and skills in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/figma-design-SKILL.md, and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
3430
+ - **Skill:** Prefer calling **syncAll** (it writes the skill into the repo). If writing manually, write getDependencies \`skill.content\` to \`skill.path\` under the project root.
3431
+ - **Token file and skill in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
4253
3432
  - **Icon package:** Install per getDependencies. When rendering icons, apply the design system's icon tokens: use getToken(\`sizing.icon.*\`) or listTokens(\`sizing\`) for size, and getToken(\`icons.strokeWidth\`) for stroke width when the DS defines it; do not use hardcoded sizes or stroke widths.
4254
3433
  - **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
4255
3434
  - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, TYPESETS_LINK, DS_NAME, HEADING_FONT_VAR, FONT_FAMILY_VAR, LARGEST_DISPLAY_TYPESET_CLASS, LARGEST_BODY_TYPESET_CLASS, BODY_TYPESET_CLASS, FONT_LINK_TAG, BRAND_PRIMARY_VAR, BUTTON_PADDING_VAR, BUTTON_HEIGHT_VAR, BUTTON_RADIUS_VAR, CIRCLE_PADDING_VAR, ICON_SIZE_VAR, CHECK_ICON_SVG (inline SVG from the design system icon library). The page uses semantic colors (mode-aware) and a Dark/Light toggle. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
@@ -4334,8 +3513,6 @@ ${tokenSummary}
4334
3513
  | **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
4335
3514
  | **/--rules** | Governance rules for your AI tool (e.g. Cursor, Copilot, Windsurf). |
4336
3515
  | **/--sync** | Sync tokens, rules, skills, and dependencies manifest (icons, fonts). Safe: adds new, updates existing, marks deprecated. |
4337
- | **/--sync-to-figma** | Push design system to Figma (variables, paint/text/effect styles). Uses built-in bridge + Atomix plugin; connect plugin in Figma then run. Available on all tiers. |
4338
- | **/--design-in-figma** | Design in Figma: loads Figma design skill and use designInFigma tool (catalog, query, execute). Pro only. |
4339
3516
  | **/--refactor** | Migrate deprecated tokens in codebase. Run after /--sync. |
4340
3517
 
4341
3518
  **Suggested next step:** Run **/--get-started** to set up global styles, icons, fonts, and token files; the AI will list options and ask before adding anything.
@@ -4360,7 +3537,6 @@ async function startServer() {
4360
3537
  console.error("");
4361
3538
  process.exit(1);
4362
3539
  }
4363
- startFigmaBridge();
4364
3540
  const transport = new StdioServerTransport();
4365
3541
  await server.connect(transport);
4366
3542
  console.error(`Atomix MCP Server started for design system: ${dsId}`);
@@ -4370,7 +3546,6 @@ async function startServer() {
4370
3546
  );
4371
3547
  }
4372
3548
  function onShutdown() {
4373
- closeFigmaBridge();
4374
3549
  }
4375
3550
  process.on("SIGINT", onShutdown);
4376
3551
  process.on("SIGTERM", onShutdown);