@atomixstudio/mcp 1.0.33 → 1.0.34

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
@@ -17,8 +17,6 @@ import {
17
17
  } from "@modelcontextprotocol/sdk/types.js";
18
18
 
19
19
  // ../atomix-sync-core/dist/index.js
20
- import * as fs from "fs";
21
- import * as path from "path";
22
20
  import * as path3 from "path";
23
21
  function generateETag(meta) {
24
22
  const hash = `${meta.version}-${meta.updatedAt}`;
@@ -337,74 +335,6 @@ function diffTokens(oldContent, newCssVars, format, newDarkVars) {
337
335
  }
338
336
  return { added, modified, removed, addedDark, modifiedDark, removedDark };
339
337
  }
340
- async function syncRulesFiles(options) {
341
- const { dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2 = "https://atomixstudio.eu", rulesDir = process.cwd() } = options;
342
- const rulesDirResolved = path.resolve(process.cwd(), rulesDir);
343
- const toolsToSync = [
344
- { tool: "cursor", filename: ".cursorrules" },
345
- { tool: "windsurf", filename: ".windsurfrules" },
346
- { tool: "cline", filename: ".clinerules" },
347
- { tool: "continue", filename: ".continuerules" },
348
- { tool: "copilot", filename: "copilot-instructions.md", dir: ".github" },
349
- { tool: "generic", filename: "AI_GUIDELINES.md" }
350
- ];
351
- const existingTools = toolsToSync.filter((t) => {
352
- const filePath = t.dir ? path.join(rulesDirResolved, t.dir, t.filename) : path.join(rulesDirResolved, t.filename);
353
- return fs.existsSync(filePath);
354
- });
355
- const toolsToWrite = existingTools.length > 0 ? existingTools : [{ tool: "cursor", filename: ".cursorrules" }];
356
- const results = [];
357
- for (const { tool, filename, dir } of toolsToWrite) {
358
- try {
359
- const rulesUrl = `${apiBase2}/api/ds/${dsId2}/rules?format=${tool}`;
360
- const headers = { "Content-Type": "application/json" };
361
- if (apiKey2) headers["x-api-key"] = apiKey2;
362
- const response = await fetch(rulesUrl, { headers });
363
- if (!response.ok) {
364
- results.push({
365
- tool,
366
- filename,
367
- path: dir ? `${dir}/${filename}` : filename,
368
- success: false,
369
- error: `Failed to fetch ${tool} rules: ${response.status}`
370
- });
371
- continue;
372
- }
373
- const rulesData = await response.json();
374
- if (!rulesData.content) {
375
- results.push({
376
- tool,
377
- filename,
378
- path: dir ? `${dir}/${filename}` : filename,
379
- success: false,
380
- error: `No content for ${tool} rules`
381
- });
382
- continue;
383
- }
384
- const targetDir = dir ? path.join(rulesDirResolved, dir) : rulesDirResolved;
385
- if (!fs.existsSync(targetDir)) {
386
- fs.mkdirSync(targetDir, { recursive: true });
387
- }
388
- const filePath = path.join(targetDir, filename);
389
- fs.writeFileSync(filePath, rulesData.content);
390
- results.push({
391
- tool,
392
- filename,
393
- path: dir ? `${dir}/${filename}` : filename,
394
- success: true
395
- });
396
- } catch (error) {
397
- results.push({
398
- tool,
399
- filename,
400
- path: dir ? `${dir}/${filename}` : filename,
401
- success: false,
402
- error: error instanceof Error ? error.message : String(error)
403
- });
404
- }
405
- }
406
- return results;
407
- }
408
338
  function generateCSSOutput(cssVariables, darkModeColors, deprecatedTokens = /* @__PURE__ */ new Map()) {
409
339
  const lines = [
410
340
  "/* Atomix Design System Tokens",
@@ -1992,8 +1922,8 @@ function buildFigmaPayloadsFromDS(data) {
1992
1922
  }
1993
1923
 
1994
1924
  // src/index.ts
1995
- import * as path2 from "path";
1996
- import * as fs2 from "fs";
1925
+ import * as path from "path";
1926
+ import * as fs from "fs";
1997
1927
  import { execSync } from "child_process";
1998
1928
  import { platform } from "os";
1999
1929
  import WebSocket, { WebSocketServer } from "ws";
@@ -2152,13 +2082,13 @@ function sendBridgeRequest(method, params, timeoutMs = FIGMA_BRIDGE_TIMEOUT_MS)
2152
2082
  );
2153
2083
  }
2154
2084
  const id = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2155
- return new Promise((resolve4, reject) => {
2085
+ return new Promise((resolve3, reject) => {
2156
2086
  const timeout = setTimeout(() => {
2157
2087
  if (pendingBridgeRequests.delete(id)) {
2158
2088
  reject(new Error("Figma bridge timeout. " + FIGMA_CONNECTION_INSTRUCTIONS.startBridge + " Then " + FIGMA_CONNECTION_INSTRUCTIONS.connect));
2159
2089
  }
2160
2090
  }, timeoutMs);
2161
- pendingBridgeRequests.set(id, { resolve: resolve4, reject, timeout });
2091
+ pendingBridgeRequests.set(id, { resolve: resolve3, reject, timeout });
2162
2092
  try {
2163
2093
  ws.send(JSON.stringify({ id, method: normalized, params }));
2164
2094
  } catch (e) {
@@ -2194,10 +2124,25 @@ function parseArgs() {
2194
2124
  var cliArgs = parseArgs();
2195
2125
  var { dsId, apiKey, accessToken } = cliArgs;
2196
2126
  var apiBase = cliArgs.apiBase || "https://atomix.studio";
2127
+ var MCP_VERSION = "1.0.33";
2197
2128
  var cachedData = null;
2198
2129
  var cachedETag = null;
2199
2130
  var cachedMcpTier = null;
2200
2131
  var authFailedNoTools = false;
2132
+ var mcpUpdateNotice = null;
2133
+ var mcpLatestVersion = null;
2134
+ function isVersionNewer(latest, current) {
2135
+ const toParts = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
2136
+ const a = toParts(latest);
2137
+ const b = toParts(current);
2138
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
2139
+ const x = a[i] ?? 0;
2140
+ const y = b[i] ?? 0;
2141
+ if (x > y) return true;
2142
+ if (x < y) return false;
2143
+ }
2144
+ return false;
2145
+ }
2201
2146
  function hasValidAuthConfig() {
2202
2147
  return !!(dsId && accessToken);
2203
2148
  }
@@ -2218,10 +2163,10 @@ ${changes.summary}`);
2218
2163
  }
2219
2164
  }
2220
2165
  function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
2221
- if (!fs2.existsSync(outputPath)) {
2166
+ if (!fs.existsSync(outputPath)) {
2222
2167
  return { path: outputPath, status: "FAIL", detail: "File not found after write." };
2223
2168
  }
2224
- const content = fs2.readFileSync(outputPath, "utf-8");
2169
+ const content = fs.readFileSync(outputPath, "utf-8");
2225
2170
  if (!content || content.trim().length === 0) {
2226
2171
  return { path: outputPath, status: "FAIL", detail: "File is empty after write." };
2227
2172
  }
@@ -2243,7 +2188,7 @@ function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
2243
2188
  }
2244
2189
  function formatValidationBlock(entries) {
2245
2190
  if (entries.length === 0) return "";
2246
- const displayPath = (p) => p.startsWith("(") ? p : path2.relative(process.cwd(), p);
2191
+ const displayPath = (p) => p.startsWith("(") ? p : path.relative(process.cwd(), p);
2247
2192
  const lines = [
2248
2193
  "",
2249
2194
  "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
@@ -2273,6 +2218,14 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
2273
2218
  cachedData = result.data;
2274
2219
  cachedETag = result.etag;
2275
2220
  cachedMcpTier = result.data.meta.mcpTier ?? null;
2221
+ const latest = result.data.meta.mcpLatestVersion;
2222
+ if (latest && isVersionNewer(latest, MCP_VERSION)) {
2223
+ mcpLatestVersion = latest;
2224
+ mcpUpdateNotice = `**MCP update available:** A new Atomix MCP server (v${latest}) is available. You're on v${MCP_VERSION}. To use the new version in Cursor: 1) Quit Cursor completely and reopen, 2) Clear npx cache: \`rm -rf ~/.npm/_npx\` (macOS/Linux), 3) Ensure your MCP config uses \`@atomixstudio/mcp@latest\` or \`@atomixstudio/mcp@${latest}\`. To test a local build before publishing: point MCP to \`node /path/to/Atom/packages/mcp-user/dist/index.js\` with \`--ds-id\` and \`--atomix-token\`.`;
2225
+ } else {
2226
+ mcpUpdateNotice = null;
2227
+ mcpLatestVersion = null;
2228
+ }
2276
2229
  await updateChangeSummary(result.data);
2277
2230
  return result.data;
2278
2231
  }
@@ -2323,7 +2276,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
2323
2276
  var server = new Server(
2324
2277
  {
2325
2278
  name: "atomix-mcp-user",
2326
- version: "1.0.30"
2279
+ version: MCP_VERSION
2327
2280
  },
2328
2281
  {
2329
2282
  capabilities: {
@@ -2441,18 +2394,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2441
2394
  }
2442
2395
  },
2443
2396
  {
2444
- name: "getAIToolRules",
2445
- description: "Generate design system rules for AI coding tools (Cursor, Copilot, Windsurf, etc.).",
2397
+ name: "getRules",
2398
+ description: "Get design system governance rules. Optionally filter by topic (colors, typo, motion, icons, layout, visual). Call at session start or before writing visual code.",
2446
2399
  inputSchema: {
2447
2400
  type: "object",
2448
2401
  properties: {
2449
- tool: {
2402
+ topic: {
2450
2403
  type: "string",
2451
- enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic", "all"],
2452
- description: "AI tool to generate rules for. Use 'all' to get rules for all tools."
2404
+ enum: ["colors", "typo", "typography", "motion", "icons", "layout", "visual", "style"],
2405
+ description: "Optional. Filter rules by topic: colors, typo/typography, motion, icons, layout, or visual/style (color, border, radius, shadows, icons). Omit for all rules."
2453
2406
  }
2454
- },
2455
- required: ["tool"]
2407
+ }
2456
2408
  }
2457
2409
  },
2458
2410
  {
@@ -2487,10 +2439,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2487
2439
  },
2488
2440
  {
2489
2441
  name: "syncAll",
2490
- 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).",
2442
+ description: "Sync tokens, AI rules, skills (.cursor/skills/atomix-ds/SKILL.md), and atomix-dependencies.json. All paths are resolved under workspaceRoot so files are written inside the project repo (committable). Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Optional: workspaceRoot (project root; default: ATOMIX_PROJECT_ROOT env or process.cwd()), output (default ./tokens.css), format (default css), skipTokens, dryRun.",
2491
2443
  inputSchema: {
2492
2444
  type: "object",
2493
2445
  properties: {
2446
+ workspaceRoot: {
2447
+ type: "string",
2448
+ description: "Absolute path to the project/repo root. Skills and manifest are written under this path so they can be committed. If omitted, uses ATOMIX_PROJECT_ROOT env var, then process.cwd()."
2449
+ },
2494
2450
  output: {
2495
2451
  type: "string",
2496
2452
  description: "Token file path (e.g. ./tokens.css). Default: ./tokens.css. Ignored if skipTokens is true."
@@ -2531,6 +2487,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2531
2487
  required: []
2532
2488
  }
2533
2489
  },
2490
+ {
2491
+ name: "getMcpVersion",
2492
+ description: "Return the current Atomix MCP server version (e.g. 1.0.33) and, if known, the latest available from the API. Call this whenever the user asks about MCP version, Atomix MCP version, what version of the MCP server they are using, or whether an update is available. Prefer this over explaining the MCP protocol spec version.",
2493
+ inputSchema: {
2494
+ type: "object",
2495
+ properties: {},
2496
+ required: []
2497
+ }
2498
+ },
2534
2499
  {
2535
2500
  name: "syncToFigma",
2536
2501
  description: "Push the owner's design system to Figma: creates color variable collection (Light/Dark), color and paint styles, number variables (spacing, radius, borders, sizing, breakpoints), text styles, and shadow effect styles. Uses local WebSocket bridge and Atomix Figma plugin (no Figma REST API). No arguments. If the bridge is not running, the response includes agentInstruction to start it; only if that fails should the user start the bridge and connect the plugin. Call this when the user asks to 'sync to Figma' or 'push DS to Figma'.",
@@ -2554,18 +2519,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2554
2519
  isError: true
2555
2520
  };
2556
2521
  }
2522
+ if (name === "getMcpVersion") {
2523
+ const out = {
2524
+ version: MCP_VERSION,
2525
+ name: "atomix-mcp-user"
2526
+ };
2527
+ if (mcpLatestVersion) {
2528
+ out.latestVersion = mcpLatestVersion;
2529
+ out.updateAvailable = true;
2530
+ }
2531
+ return {
2532
+ content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
2533
+ };
2534
+ }
2557
2535
  try {
2558
2536
  const shouldForceRefresh = name === "syncAll";
2559
2537
  const data = await fetchDesignSystemForMCP(shouldForceRefresh);
2560
- async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun) {
2538
+ async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun, projectRoot) {
2561
2539
  const output = tokenOutput;
2562
2540
  const format = tokenFormat;
2563
- const outputPath = path2.resolve(process.cwd(), output);
2564
- const fileExists = fs2.existsSync(outputPath);
2541
+ const outputPath = path.resolve(projectRoot, output);
2542
+ const fileExists = fs.existsSync(outputPath);
2565
2543
  const deprecatedTokens = /* @__PURE__ */ new Map();
2566
2544
  const existingTokens = /* @__PURE__ */ new Map();
2567
2545
  if (fileExists && ["css", "scss", "less"].includes(format)) {
2568
- const oldContent = fs2.readFileSync(outputPath, "utf-8");
2546
+ const oldContent = fs.readFileSync(outputPath, "utf-8");
2569
2547
  const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
2570
2548
  let match;
2571
2549
  while ((match = oldVarPattern.exec(oldContent)) !== null) {
@@ -2615,7 +2593,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2615
2593
  let changes = [];
2616
2594
  let diff;
2617
2595
  if (fileExists && ["css", "scss", "less"].includes(format)) {
2618
- const oldContent = fs2.readFileSync(outputPath, "utf-8");
2596
+ const oldContent = fs.readFileSync(outputPath, "utf-8");
2619
2597
  diff = diffTokens(oldContent, mergedCssVariables, format, darkModeColors?.dark);
2620
2598
  const lightChanges = diff.added.length + diff.modified.length;
2621
2599
  const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
@@ -2670,7 +2648,7 @@ Version: ${designSystemData.meta.version}`,
2670
2648
  ` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
2671
2649
  changeLine,
2672
2650
  "",
2673
- "Rules: .cursorrules (or existing rules files in project)",
2651
+ "Would write skills: .cursor/skills/atomix-ds/SKILL.md",
2674
2652
  "",
2675
2653
  "Call syncAll again with dryRun: false to apply."
2676
2654
  ].filter(Boolean).join("\n");
@@ -2680,30 +2658,12 @@ Version: ${designSystemData.meta.version}`,
2680
2658
  validation: [{ path: "(dry run)", status: "OK", detail: "No files written." }]
2681
2659
  };
2682
2660
  }
2683
- const outputDir = path2.dirname(outputPath);
2684
- if (!fs2.existsSync(outputDir)) fs2.mkdirSync(outputDir, { recursive: true });
2685
- fs2.writeFileSync(outputPath, newContent);
2661
+ const outputDir = path.dirname(outputPath);
2662
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
2663
+ fs.writeFileSync(outputPath, newContent);
2686
2664
  const validation = [];
2687
2665
  validation.push(validateTokenFileAfterWrite(outputPath, format, tokenCount));
2688
- let rulesResults = [];
2689
- try {
2690
- rulesResults = await syncRulesFiles({
2691
- dsId,
2692
- apiKey: apiKey ?? void 0,
2693
- apiBase: apiBase ?? void 0,
2694
- rulesDir: process.cwd()
2695
- });
2696
- for (const r of rulesResults) {
2697
- const fullPath = path2.resolve(process.cwd(), r.path);
2698
- validation.push({
2699
- path: fullPath,
2700
- status: r.success && fs2.existsSync(fullPath) ? "OK" : "FAIL",
2701
- detail: r.success ? "Written." : r.error || "Write failed."
2702
- });
2703
- }
2704
- } catch (error) {
2705
- console.error(`[syncAll] Failed to sync rules: ${error}`);
2706
- }
2666
+ const rulesResults = [];
2707
2667
  const governanceChanges = cachedData ? detectGovernanceChangesByFoundation(cachedData, designSystemData) : [];
2708
2668
  const response = formatSyncResponse({
2709
2669
  data: designSystemData,
@@ -2725,27 +2685,27 @@ Version: ${designSystemData.meta.version}`,
2725
2685
  }
2726
2686
  switch (name) {
2727
2687
  case "getToken": {
2728
- const path4 = args?.path;
2729
- const value = getTokenByPath(data.tokens, path4);
2688
+ const path2 = args?.path;
2689
+ const value = getTokenByPath(data.tokens, path2);
2730
2690
  if (value === void 0) {
2731
2691
  return {
2732
2692
  content: [{
2733
2693
  type: "text",
2734
2694
  text: JSON.stringify({
2735
- error: `Token not found: ${path4}`,
2695
+ error: `Token not found: ${path2}`,
2736
2696
  suggestion: "Use listTokens or searchTokens to find available tokens.",
2737
2697
  availableCategories: TOKEN_CATEGORIES
2738
2698
  }, null, 2)
2739
2699
  }]
2740
2700
  };
2741
2701
  }
2742
- const cssVarKey = `--atmx-${path4.replace(/\./g, "-")}`;
2702
+ const cssVarKey = `--atmx-${path2.replace(/\./g, "-")}`;
2743
2703
  const cssVar = data.cssVariables[cssVarKey];
2744
2704
  return {
2745
2705
  content: [{
2746
2706
  type: "text",
2747
2707
  text: JSON.stringify({
2748
- path: path4,
2708
+ path: path2,
2749
2709
  value,
2750
2710
  cssVariable: cssVar || `var(${cssVarKey})`,
2751
2711
  usage: `style={{ property: "var(${cssVarKey})" }}`
@@ -2772,13 +2732,13 @@ Version: ${designSystemData.meta.version}`,
2772
2732
  };
2773
2733
  }
2774
2734
  const flat = flattenTokens(tokensToList);
2775
- const tokensWithCssVars = flat.map(({ path: path4, value }) => {
2776
- const fullPath = subcategory ? `${category}.${subcategory}.${path4}` : `${category}.${path4}`;
2735
+ const tokensWithCssVars = flat.map(({ path: path2, value }) => {
2736
+ const fullPath = subcategory ? `${category}.${subcategory}.${path2}` : `${category}.${path2}`;
2777
2737
  let cssVar;
2778
2738
  if (category === "colors" && subcategory === "static.brand") {
2779
- cssVar = data.cssVariables[`--atmx-color-brand-${path4}`];
2739
+ cssVar = data.cssVariables[`--atmx-color-brand-${path2}`];
2780
2740
  } else if (category === "colors" && subcategory?.startsWith("modes.")) {
2781
- cssVar = data.cssVariables[`--atmx-color-${path4}`];
2741
+ cssVar = data.cssVariables[`--atmx-color-${path2}`];
2782
2742
  } else {
2783
2743
  const cssVarKey = `--atmx-${fullPath.replace(/\./g, "-")}`;
2784
2744
  cssVar = data.cssVariables[cssVarKey];
@@ -2887,30 +2847,63 @@ Version: ${designSystemData.meta.version}`,
2887
2847
  }]
2888
2848
  };
2889
2849
  }
2890
- case "getAIToolRules": {
2891
- const tool = args?.tool;
2892
- const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool === "all" ? "all" : tool}`;
2893
- console.error(`[getAIToolRules] Fetching: ${rulesUrl}`);
2850
+ case "getRules": {
2851
+ const topicRaw = args?.topic;
2852
+ const topic = topicRaw?.toLowerCase().trim();
2853
+ const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
2854
+ console.error(`[getRules] Fetching: ${rulesUrl}${topic ? ` topic=${topic}` : ""}`);
2894
2855
  const headers = { "Content-Type": "application/json" };
2895
2856
  if (apiKey) headers["x-api-key"] = apiKey;
2896
2857
  try {
2897
2858
  const response = await fetch(rulesUrl, { headers });
2898
- console.error(`[getAIToolRules] Response status: ${response.status}`);
2859
+ console.error(`[getRules] Response status: ${response.status}`);
2899
2860
  if (!response.ok) {
2900
2861
  const errorText = await response.text();
2901
- console.error(`[getAIToolRules] Error response: ${errorText}`);
2862
+ console.error(`[getRules] Error response: ${errorText}`);
2902
2863
  throw new Error(`Failed to fetch rules: ${response.status} - ${errorText}`);
2903
2864
  }
2904
- const rules = await response.json();
2905
- console.error(`[getAIToolRules] Got ${rules.rules?.length || 0} rules`);
2865
+ const payload = await response.json();
2866
+ const categories = payload.categories ?? {};
2867
+ const allRules = payload.rules ?? [];
2868
+ if (!topic) {
2869
+ return {
2870
+ content: [{ type: "text", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
2871
+ };
2872
+ }
2873
+ const topicToCategories = {
2874
+ colors: ["general", "colors"],
2875
+ typo: ["general", "typography"],
2876
+ typography: ["general", "typography"],
2877
+ motion: ["general", "motion"],
2878
+ icons: ["general", "icons"],
2879
+ layout: ["general", "spacing", "sizing", "layout"],
2880
+ visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
2881
+ style: ["general", "colors", "borders", "radius", "shadows", "icons"]
2882
+ };
2883
+ const categoryKeys = topicToCategories[topic];
2884
+ if (!categoryKeys) {
2885
+ return {
2886
+ content: [{ type: "text", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
2887
+ };
2888
+ }
2889
+ const filteredCategories = {};
2890
+ const filteredRules = [];
2891
+ for (const key of categoryKeys) {
2892
+ const list = categories[key];
2893
+ if (list && list.length > 0) {
2894
+ filteredCategories[key] = list;
2895
+ filteredRules.push(...list);
2896
+ }
2897
+ }
2898
+ console.error(`[getRules] Got ${filteredRules.length} rules for topic=${topic}`);
2906
2899
  return {
2907
2900
  content: [{
2908
2901
  type: "text",
2909
- text: JSON.stringify(rules, null, 2)
2902
+ text: JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2)
2910
2903
  }]
2911
2904
  };
2912
2905
  } catch (fetchError) {
2913
- console.error(`[getAIToolRules] Fetch error:`, fetchError);
2906
+ console.error(`[getRules] Fetch error:`, fetchError);
2914
2907
  throw fetchError;
2915
2908
  }
2916
2909
  }
@@ -2995,7 +2988,7 @@ Version: ${designSystemData.meta.version}`,
2995
2988
  copilot: `# Copilot Setup
2996
2989
 
2997
2990
  1. Create a Copilot instructions file in your project (e.g. \`.github/copilot-instructions.md\`)
2998
- 2. Use getAIToolRules({ tool: "copilot" }) to get the content
2991
+ 2. Run /--sync to write the skill; call getRules() when you need governance rules
2999
2992
  3. Enable custom instructions in your editor (e.g. \`github.copilot.chat.codeGeneration.useInstructionFiles\`: true in settings)
3000
2993
 
3001
2994
  ## File Structure
@@ -3009,7 +3002,7 @@ Version: ${designSystemData.meta.version}`,
3009
3002
  windsurf: `# Windsurf Setup
3010
3003
 
3011
3004
  1. Create \`.windsurf/mcp.json\` in your project root
3012
- 2. Create \`.windsurfrules\` with rules from getAIToolRules
3005
+ 2. Run /--sync to write the skill; call getRules() when you need governance rules
3013
3006
  3. Restart Windsurf Editor
3014
3007
 
3015
3008
  ## File Structure
@@ -3050,7 +3043,7 @@ Version: ${designSystemData.meta.version}`,
3050
3043
  zed: `# Zed Setup
3051
3044
 
3052
3045
  1. Create \`.zed/assistant/rules.md\` in your project
3053
- 2. Use getAIToolRules({ tool: "zed" }) for content
3046
+ 2. Run /--sync to write the skill; call getRules() for governance rules
3054
3047
 
3055
3048
  ## File Structure
3056
3049
 
@@ -3078,9 +3071,8 @@ Version: ${designSystemData.meta.version}`,
3078
3071
  **Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
3079
3072
  generic: `# Generic AI Tool Setup
3080
3073
 
3081
- 1. Create AI_GUIDELINES.md in your project root
3082
- 2. Use getAIToolRules({ tool: "generic" }) for content
3083
- 3. Reference in your prompts or context
3074
+ 1. Run /--sync to write the skill (.cursor/skills/atomix-ds/SKILL.md)
3075
+ 2. Call getRules() when you need governance rules; reference in your prompts or context
3084
3076
 
3085
3077
  ## File Structure
3086
3078
 
@@ -3115,16 +3107,16 @@ Version: ${designSystemData.meta.version}`,
3115
3107
  const dryRun = args?.dryRun === true;
3116
3108
  const output = args?.output || "./tokens.css";
3117
3109
  const format = args?.format || "css";
3110
+ const projectRoot = path.resolve(
3111
+ args?.workspaceRoot || process.env.ATOMIX_PROJECT_ROOT || process.cwd()
3112
+ );
3118
3113
  const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
3119
3114
  let tokenResponseText = "";
3120
3115
  const allValidation = [];
3121
3116
  if (!skipTokens) {
3122
- const result = await performTokenSyncAndRules(data, output, format, dryRun);
3117
+ const result = await performTokenSyncAndRules(data, output, format, dryRun, projectRoot);
3123
3118
  tokenResponseText = result.responseText;
3124
3119
  allValidation.push(...result.validation);
3125
- if (!dryRun && result.rulesResults.length > 0) {
3126
- parts.push(`Rules: ${result.rulesResults.map((r) => r.path).join(", ")}`);
3127
- }
3128
3120
  if (dryRun) {
3129
3121
  parts.push(`Would write tokens: ${output} (${format})`);
3130
3122
  } else {
@@ -3133,9 +3125,9 @@ Version: ${designSystemData.meta.version}`,
3133
3125
  }
3134
3126
  const dsVersion = String(data.meta.version ?? "1.0.0");
3135
3127
  const dsExportedAt = data.meta.exportedAt;
3136
- const skillsDir = path2.resolve(process.cwd(), ".cursor/skills/atomix-ds");
3137
- const skillPath1 = path2.join(skillsDir, "SKILL.md");
3138
- const manifestPath = path2.resolve(process.cwd(), "atomix-dependencies.json");
3128
+ const skillsDir = path.resolve(projectRoot, ".cursor/skills/atomix-ds");
3129
+ const skillPath1 = path.join(skillsDir, "SKILL.md");
3130
+ const manifestPath = path.resolve(projectRoot, "atomix-dependencies.json");
3139
3131
  if (dryRun) {
3140
3132
  parts.push("Would write skills: .cursor/skills/atomix-ds/SKILL.md");
3141
3133
  parts.push("Would write manifest: atomix-dependencies.json");
@@ -3146,10 +3138,10 @@ Version: ${designSystemData.meta.version}`,
3146
3138
  ${reportText}` }]
3147
3139
  };
3148
3140
  }
3149
- if (!fs2.existsSync(skillsDir)) fs2.mkdirSync(skillsDir, { recursive: true });
3141
+ if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
3150
3142
  const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
3151
- fs2.writeFileSync(skillPath1, genericWithVersion);
3152
- allValidation.push({ path: skillPath1, status: fs2.existsSync(skillPath1) ? "OK" : "FAIL", detail: "Written." });
3143
+ fs.writeFileSync(skillPath1, genericWithVersion);
3144
+ allValidation.push({ path: skillPath1, status: fs.existsSync(skillPath1) ? "OK" : "FAIL", detail: "Written." });
3153
3145
  parts.push("Skills: .cursor/skills/atomix-ds/SKILL.md (synced at DS v" + dsVersion + ")");
3154
3146
  const tokens = data.tokens;
3155
3147
  const typography = tokens?.typography;
@@ -3184,8 +3176,8 @@ ${reportText}` }]
3184
3176
  syncedAtVersion: data.meta.version ?? "1.0.0"
3185
3177
  }
3186
3178
  };
3187
- fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3188
- allValidation.push({ path: manifestPath, status: fs2.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
3179
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3180
+ allValidation.push({ path: manifestPath, status: fs.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
3189
3181
  parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
3190
3182
  const summary = parts.join("\n");
3191
3183
  const validationBlock = formatValidationBlock(allValidation);
@@ -3244,7 +3236,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3244
3236
  showcase: platform2 === "web" || !platform2 ? {
3245
3237
  path: "atomix-setup-showcase.html",
3246
3238
  template: SHOWCASE_HTML_TEMPLATE,
3247
- substitutionInstructions: 'Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{TYPESETS_LINK}} = if a typeset CSS file was created (e.g. typesets.css), the full tag e.g. <link rel="stylesheet" href="typesets.css">, otherwise empty string. {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl or typeset-display-bold), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = typeset class for body text from listTypesets (e.g. typeset-body-md), or empty string if no typeset file. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export.'
3239
+ 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.'
3248
3240
  } : void 0,
3249
3241
  meta: {
3250
3242
  dsName: data.meta.name,
@@ -3467,7 +3459,7 @@ ${JSON.stringify(out, null, 2)}` : JSON.stringify(out, null, 2);
3467
3459
  type: "text",
3468
3460
  text: JSON.stringify({
3469
3461
  error: `Unknown tool: ${name}`,
3470
- availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma"]
3462
+ availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "getMcpVersion", "syncToFigma"]
3471
3463
  }, null, 2)
3472
3464
  }]
3473
3465
  };
@@ -3521,51 +3513,124 @@ atomixDsExportedAt: "${exportedAt ?? ""}"
3521
3513
  }
3522
3514
  var GENERIC_SKILL_MD = `---
3523
3515
  name: atomix-ds
3524
- description: Use the Atomix design system for UI, tokens, and styles. Fetch rules and tokens via MCP tools; never hardcode design values.
3516
+ description: Use the project's design system tokens semantically when building or editing any UI. Covers everyday tasks \u2014 creating components, building pages, styling forms, theming, dark mode, responsive layouts, adding icons, choosing colors, setting type, adjusting spacing, and applying motion. Works with any platform or framework (web, React Native, iOS, Android, Flutter, etc.). Fetch token values and governance rules via MCP tools (getToken, listTokens, searchTokens, getRules, validateUsage). Tokens include colors, typography, spacing, sizing, radius, shadows, borders, motion, z-index, and icons. Never hardcode visual values \u2014 always resolve from the design system first.
3525
3517
  ---
3526
3518
 
3527
3519
  # Atomix Design System
3528
3520
 
3529
- Use this skill when editing UI, design system files, or when the user asks to follow the project's design system. Your job is to **get the relevant data from the design system via MCP** and apply it\u2014not to guess or invent values.
3530
-
3531
- ## Goal
3521
+ **Prerequisite:** Load rules first before anything else \u2014 call getRules() at the start of the session or before writing any visual code.
3532
3522
 
3533
- Complete the user's task using the design system as the single source of truth. Every color, spacing, typography, radius, shadow, or sizing value must come from the MCP tools (tokens or governance rules). Do not hardcode hex codes, pixel values, or font names.
3523
+ This project uses a design system with semantic tokens. **Before writing any visual code**, fetch the relevant tokens and governance rules from the MCP tools below. The design system is the single source of truth \u2014 do not guess, invent, or hardcode visual values.
3534
3524
 
3535
3525
  ## When to use
3536
3526
 
3537
- - Building or editing UI components, pages, or styles
3538
- - Working in design system or token files (e.g. \`tokens.css\`, theme files)
3539
- - User asks to "follow the design system", "use tokens", or "match the DS"
3540
- - Validating or refactoring existing code, UI or visual design for token compliance
3527
+ Use this skill for everyday design and frontend tasks:
3528
+
3529
+ **Building UI**
3530
+ - Creating a component (button, card, input, modal, nav, sidebar, table, list, badge, tooltip\u2026)
3531
+ - Laying out a page, screen, or view
3532
+ - Building a form, dialog, popover, or overlay
3533
+ - Adding a header, footer, hero section, or onboarding flow
3534
+
3535
+ **Styling & theming**
3536
+ - Choosing colors for backgrounds, text, borders, icons, or interactive states
3537
+ - Setting typography \u2014 font family, size, weight, line height, letter spacing
3538
+ - Adjusting spacing, padding, margins, or gaps between elements
3539
+ - Applying border radius, shadows, or elevation to surfaces
3540
+ - Adding hover, focus, active, disabled, or loading states
3541
+ - Setting up dark mode, light mode, or theme switching
3542
+ - Configuring responsive or adaptive layouts
3543
+
3544
+ **Working with assets**
3545
+ - Rendering icons (sizing and stroke width are defined by the design system)
3546
+ - Implementing designs from Figma, mockups, screenshots, or design specs
3547
+ - Translating a design handoff into code
3548
+
3549
+ **Maintenance**
3550
+ - Refactoring hardcoded values to use tokens
3551
+ - Auditing code for design system compliance
3552
+ - Updating styles after a design system version change
3553
+
3554
+ If the task has no visual output (pure logic, data, APIs, DevOps), this skill is not needed.
3555
+
3556
+ ## Semantic token usage
3557
+
3558
+ Tokens have two layers \u2014 **primitives** (raw scales) and **semantic** (purpose-driven). Always prefer semantic tokens because they adapt to themes and modes automatically.
3559
+
3560
+ | Intent | Use semantic token | Avoid raw primitive |
3561
+ |--------|-------------------|---------------------|
3562
+ | Page background | \`bg-page\` | \`neutral.50\` |
3563
+ | Card surface | \`bg-surface\` | \`white\` |
3564
+ | Primary text | \`text-primary\` | \`neutral.900\` |
3565
+ | Muted text | \`text-muted\` | \`neutral.500\` |
3566
+ | Default border | \`border-default\` | \`neutral.200\` |
3567
+ | Brand action | \`brand.primary\` | \`green.600\` |
3568
+ | Error state | \`status.error\` | \`red.500\` |
3541
3569
 
3542
- ## How to get design system data
3570
+ Call \`getRules\` for the full mapping of semantic tokens to primitives in each theme.
3543
3571
 
3544
- **1. Governance rules (how to use tokens in code)**
3545
- Call **getAIToolRules** with the tool id for your current environment: \`cursor\`, \`windsurf\`, \`copilot\`, \`cline\`, \`continue\`, \`zed\`, or \`generic\`.
3546
- Example: \`getAIToolRules({ tool: "cursor" })\`.
3547
- Alternatively use the **/--rules** prompt or the resource \`atomix://rules/<tool>\`.
3572
+ When a semantic token doesn't exist for your use case, use the closest primitive from \`listTokens\` \u2014 but document why so the team can promote it to a semantic token later.
3548
3573
 
3549
- **2. Token values (what to use in code)**
3550
- - **getToken(path)** \u2014 One token by path (e.g. \`colors.brand.primary\`, \`typography.fontSize.lg\`, \`sizing.icon.sm\`, \`icons.strokeWidth\`). Icon stroke width is at \`icons.strokeWidth\` when the design system defines it.
3551
- - **listTokens(category)** \u2014 All tokens in a category: \`colors\`, \`typography\`, \`spacing\`, \`sizing\`, \`shadows\`, \`radius\`, \`borders\`, \`motion\`, \`zIndex\`. For icon config (e.g. stroke width), use \`getToken("icons.strokeWidth")\` since \`icons\` is not a list category.
3552
- - **searchTokens(query)** \u2014 Find tokens by name or value.
3574
+ ## How to fetch design system data
3553
3575
 
3554
- **3. Validation**
3555
- - **validateUsage(value, context)** \u2014 Check if a CSS/value should use a token instead (e.g. \`validateUsage("#007061", "color")\`).
3576
+ ### 1. Governance rules \u2014 always fetch first
3556
3577
 
3557
- **4. Syncing tokens to a file**
3558
- - **syncAll({ output?, format?, skipTokens? })** \u2014 Syncs tokens to a file, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Default output \`./tokens.css\`, format \`css\`. Use \`skipTokens: true\` to only write skills and manifest.
3578
+ \`getRules()\` \u2014 optionally with a topic: \`colors\`, \`typo\`, \`motion\`, \`icons\`, \`layout\`, or \`visual\` (color, border, radius, shadows, icons). Returns how tokens should be applied \u2014 naming conventions, variable format, and semantic mappings.
3559
3579
 
3560
- Use the returned rules and token paths/values when generating or editing code. Prefer CSS variables (e.g. \`var(--atmx-*)\`) or the exact token references from the tools.
3580
+ ### 2. Token values \u2014 by task
3581
+
3582
+ | I need\u2026 | MCP call |
3583
+ |---------|----------|
3584
+ | A specific token | \`getToken("colors.brand.primary")\` or \`getToken("spacing.scale.md")\` |
3585
+ | All tokens in a category | \`listTokens("colors")\` \u2014 categories: \`colors\`, \`typography\`, \`spacing\`, \`sizing\`, \`shadows\`, \`radius\`, \`borders\`, \`motion\`, \`zIndex\` |
3586
+ | Search by name or value | \`searchTokens("primary")\` or \`searchTokens("bold")\` |
3587
+ | Icon size or stroke | \`getToken("sizing.icon.sm")\` for dimensions, \`getToken("icons.strokeWidth")\` for stroke |
3588
+ | Typeset classes | \`listTypesets()\` \u2014 emit one class per typeset; include text-transform and text-decoration for 1:1 match |
3589
+
3590
+ ### 3. Validation
3591
+
3592
+ \`validateUsage("#007061", "color")\` \u2014 checks if a raw value should be a token. Run this on any value you suspect is hardcoded.
3593
+
3594
+ ### 4. Syncing tokens to a file
3595
+
3596
+ \`syncAll({ output?, format?, skipTokens? })\` \u2014 writes tokens to a file (default \`./tokens.css\`), skills (.cursor/skills/atomix-ds/SKILL.md), and manifest. Use \`skipTokens: true\` to only write skills and manifest.
3597
+
3598
+ ## Workflow
3599
+
3600
+ 1. **Fetch rules** \u2014 call \`getRules\` once per session (or with a topic when working on a specific area).
3601
+ 2. **Fetch tokens** \u2014 call \`getToken\`, \`listTokens\`, or \`searchTokens\` for the values you need.
3602
+ 3. **Apply semantically** \u2014 use token references or CSS variables (\`var(--atmx-*)\`) depending on your platform. Choose the semantic token that matches the *purpose*, not just the visual appearance.
3603
+ 4. **Self-check** \u2014 scan your output for hardcoded hex codes, pixel values, rem/em literals, duration strings, or font names. If found, replace with the matching token.
3604
+
3605
+ ## Common mistakes
3606
+
3607
+ Do not hardcode visual values. Always resolve from the design system:
3608
+
3609
+ - A hex color (\`#007061\`, \`#333\`) \u2192 call \`getToken\` or \`searchTokens\` for the matching color token
3610
+ - A pixel/rem value (\`16px\`, \`1.5rem\`) \u2192 use the spacing, sizing, or radius token
3611
+ - A font name (\`"Inter"\`, \`"SF Pro"\`) \u2192 use the typography font-family token
3612
+ - A duration (\`200ms\`, \`0.3s\`) \u2192 use the motion duration token
3613
+ - A numeric weight (\`600\`, \`700\`) \u2192 use the typography font-weight token
3614
+ - A shadow string (\`0 4px 6px rgba(\u2026)\`) \u2192 use the shadow elevation token
3615
+
3616
+ If no token matches, call \`searchTokens\` to find the closest option. Never invent a token path.
3561
3617
 
3562
3618
  ## Best practices
3563
3619
 
3564
- - **Fetch first:** Before writing UI or styles, call getAIToolRules and/or getToken/listTokens so you know the exact tokens and conventions.
3565
- - **Icons:** Apply the design system's icon tokens when rendering icons: sizing via \`getToken("sizing.icon.sm")\` or \`listTokens("sizing")\`, and stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it; do not use hardcoded sizes or stroke widths.
3566
- - **Typography:** Use typography tokens from the DS for any text. When creating global typeset CSS, call **listTypesets** and emit one CSS class per typeset (do not skip any); include text-transform and text-decoration when present for 1:1 match.
3567
- - **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.
3568
- - **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.
3620
+ - **Fetch first:** Always call getRules and/or listTokens before writing any styles, regardless of platform or framework.
3621
+ - **Semantic over primitive:** Prefer tokens that describe purpose (\`text-primary\`, \`bg-surface\`) over tokens that describe appearance (\`neutral.900\`, \`white\`).
3622
+ - **Icons:** Size via \`getToken("sizing.icon.sm")\`; stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it.
3623
+ - **Typography:** Use typography tokens for all text. For global typeset output, call **listTypesets** and emit every entry; include text-transform and text-decoration for 1:1 match.
3624
+ - **No guessing:** If a value is not in the rules or token list, call searchTokens or listTokens to find the closest match.
3625
+ - **Platform agnostic:** Token values work across CSS, Tailwind, React Native, SwiftUI, Compose, Flutter, and any style system. Use the output format appropriate to your platform.
3626
+ - **Version check:** If this file has frontmatter \`atomixDsVersion\`, compare to the version from **getDependencies** (\`meta.designSystemVersion\`). If the DS is newer, suggest running **syncAll** to update.
3627
+
3628
+ ## Strict mode and patterns
3629
+
3630
+ - **Strict mode:** NO arbitrary values (e.g. \`bg-[#ff0000]\` forbidden). NO hardcoded colors \u2014 use CSS variables and semantic tokens only. NO hardcoded pixel values \u2014 use spacing/sizing tokens only. NO hardcoded typography \u2014 use typography typeset tokens only. Token vocabulary only \u2014 if a value is not in the design system, do not use it.
3631
+ - **CSS variables:** All tokens follow \`--atmx-{category}-{subcategory}-{token}\`. Correct: \`backgroundColor: "var(--atmx-color-bg-surface)"\`, \`borderRadius: "var(--atmx-radius-scale-md)"\`. Wrong: hex, raw px.
3632
+ - **Dark mode:** Colors switch in dark mode when using CSS variables; \`.dark\` on root toggles color variables.
3633
+ - **Button pattern:** Height and padding use sizing/spacing tokens; typography from typeset tokens; radius and border from tokens; transition from motion tokens; primary/secondary/ghost use action color tokens (e.g. \`--atmx-color-action-primary\`, \`--atmx-color-action-on-primary\`).
3569
3634
  `;
3570
3635
  var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3571
3636
  <html lang="en">
@@ -3581,52 +3646,153 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3581
3646
  body {
3582
3647
  margin: 0;
3583
3648
  font-family: {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3584
- background: {{BRAND_PRIMARY_VAR}};
3585
- color: {{BRAND_PRIMARY_FOREGROUND_VAR}};
3649
+ background: var(--atmx-color-bg-page);
3650
+ color: var(--atmx-color-text-primary);
3586
3651
  min-height: 100vh;
3587
- padding: 2rem;
3652
+ padding: var(--atmx-spacing-scale-2xl);
3588
3653
  display: flex;
3589
3654
  justify-content: center;
3590
3655
  align-items: center;
3591
3656
  }
3592
3657
  .wrap { width: 375px; max-width: 100%; }
3593
- .icon { width: 2rem; height: 2rem; margin: 0 0 1rem; }
3658
+ .top-row {
3659
+ display: flex;
3660
+ justify-content: space-between;
3661
+ align-items: flex-start;
3662
+ margin-bottom: var(--atmx-spacing-scale-lg);
3663
+ }
3664
+ .top-row .mode-toggle { margin-bottom: 0; }
3665
+ .icon-circle {
3666
+ display: inline-flex;
3667
+ align-items: center;
3668
+ justify-content: center;
3669
+ width: calc({{ICON_SIZE_VAR}} + 2 * {{CIRCLE_PADDING_VAR}});
3670
+ height: calc({{ICON_SIZE_VAR}} + 2 * {{CIRCLE_PADDING_VAR}});
3671
+ padding: {{CIRCLE_PADDING_VAR}};
3672
+ background: {{BRAND_PRIMARY_VAR}};
3673
+ border-radius: 50%;
3674
+ }
3675
+ .icon-circle.light-icon { color: #fff; }
3676
+ .icon-circle.dark-icon { color: #000; }
3677
+ .icon-circle svg {
3678
+ width: {{ICON_SIZE_VAR}};
3679
+ height: {{ICON_SIZE_VAR}};
3680
+ flex-shrink: 0;
3681
+ }
3682
+ .mode-toggle {
3683
+ display: inline-flex;
3684
+ align-items: center;
3685
+ justify-content: center;
3686
+ padding: {{BUTTON_PADDING_VAR}};
3687
+ height: {{BUTTON_HEIGHT_VAR}};
3688
+ border-radius: {{BUTTON_RADIUS_VAR}};
3689
+ background: var(--atmx-color-bg-surface);
3690
+ color: var(--atmx-color-text-primary);
3691
+ border: 1px solid var(--atmx-color-border-default);
3692
+ font-family: inherit;
3693
+ font-size: inherit;
3694
+ font-weight: inherit;
3695
+ cursor: pointer;
3696
+ margin-bottom: var(--atmx-spacing-scale-xl);
3697
+ }
3698
+ .mode-toggle:hover {
3699
+ background: var(--atmx-color-bg-muted);
3700
+ }
3594
3701
  h1 {
3595
3702
  font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3596
- font-size: clamp(3rem, 4vw, 5rem);
3597
- font-weight: 700;
3598
- margin: 0 0 0.75rem;
3703
+ margin: 0 0 var(--atmx-spacing-scale-md);
3599
3704
  line-height: 1.2;
3600
3705
  }
3601
- .lead { margin: 0 0 1.5rem; font-size: 1rem; line-height: 1.5; opacity: 0.95; }
3602
- .now { margin: 1.5rem 0 0; font-size: 0.875rem; line-height: 1.6; opacity: 0.95; text-align: left; }
3603
- .now strong { display: block; margin-bottom: 0.5rem; }
3604
- .now ul { margin: 0; padding-left: 1.25rem; }
3605
- .tips { margin-top: 1.5rem; font-size: 0.875rem; line-height: 1.6; opacity: 0.9; }
3706
+ .lead {
3707
+ margin: 0 0 var(--atmx-spacing-scale-xl);
3708
+ opacity: 0.95;
3709
+ }
3710
+ .now { margin: var(--atmx-spacing-scale-xl) 0 0; opacity: 0.95; text-align: left; }
3711
+ .now strong { display: block; margin-bottom: var(--atmx-spacing-scale-sm); }
3712
+ .now ul { margin: 0; padding-left: var(--atmx-spacing-scale-xl); }
3713
+ .tips { margin-top: var(--atmx-spacing-scale-xl); opacity: 0.9; }
3606
3714
  .tips a { color: inherit; text-decoration: underline; }
3715
+ code {
3716
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
3717
+ font-size: 0.8125rem;
3718
+ padding: 0.125rem 0.375rem;
3719
+ border-radius: 0.25rem;
3720
+ background: var(--atmx-color-bg-muted);
3721
+ }
3607
3722
  </style>
3608
3723
  </head>
3609
3724
  <body class="{{BODY_TYPESET_CLASS}}">
3610
3725
  <div class="wrap">
3611
- <div class="icon" aria-hidden="true">
3612
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="44" height="44"><path d="M20 6L9 17l-5-5"/></svg>
3726
+ <div class="top-row">
3727
+ <div class="icon-circle" aria-hidden="true">
3728
+ {{CHECK_ICON_SVG}}
3729
+ </div>
3730
+ <button type="button" class="mode-toggle" id="mode-toggle" aria-label="Toggle light or dark mode">Dark</button>
3613
3731
  </div>
3614
3732
  <h1 class="{{LARGEST_DISPLAY_TYPESET_CLASS}}">You're all set with {{DS_NAME}}</h1>
3615
- <p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon.</p>
3616
- <div class="now">
3733
+ <p class="lead {{LARGEST_BODY_TYPESET_CLASS}}">This page uses your design system: semantic colors (mode-aware), headline and body typesets, and an icon.</p>
3734
+ <div class="now {{LARGEST_BODY_TYPESET_CLASS}}">
3617
3735
  <strong>What you can do now:</strong>
3618
3736
  <ul>
3619
3737
  <li>Ask your agent to build your designs using the design system tokens</li>
3620
3738
  <li>Build components and pages that use <code>var(--atmx-*)</code> for colors, spacing, and typography</li>
3621
- <li>Run <code>/--rules</code> to load governance rules; run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
3739
+ <li>Run <code>/--rules</code> to load governance rules (or call getRules); run <code>/--sync</code> and <code>/--refactor</code> after you change tokens in Atomix Studio</li>
3622
3740
  </ul>
3623
3741
  </div>
3624
- <p class="tips">Keep the source of truth at <a href="https://atomix.studio" target="_blank" rel="noopener">atomix.studio</a> \u2014 avoid editing token values in this repo.</p>
3742
+ <p class="tips {{LARGEST_BODY_TYPESET_CLASS}}">Keep the source of truth at <a href="https://atomix.studio" target="_blank" rel="noopener">atomix.studio</a> \u2014 avoid editing token values in this repo.</p>
3625
3743
  </div>
3744
+ <script>
3745
+ (function() {
3746
+ var root = document.documentElement;
3747
+ var btn = document.getElementById('mode-toggle');
3748
+ var circle = document.querySelector('.icon-circle');
3749
+ function hexToRgb(hex) {
3750
+ var m = hex.slice(1).match(/.{2}/g);
3751
+ return m ? m.map(function(x) { return parseInt(x, 16) / 255; }) : [0, 0, 0];
3752
+ }
3753
+ function relativeLuminance(r, g, b) {
3754
+ var srgb = function(x) { return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); };
3755
+ return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
3756
+ }
3757
+ function setIconContrast() {
3758
+ if (!circle) return;
3759
+ var bg = getComputedStyle(circle).backgroundColor;
3760
+ var r = 0, g = 0, b = 0;
3761
+ var rgbMatch = bg.match(/rgb\\(?\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)?/);
3762
+ if (rgbMatch) {
3763
+ r = parseInt(rgbMatch[1], 10) / 255;
3764
+ g = parseInt(rgbMatch[2], 10) / 255;
3765
+ b = parseInt(rgbMatch[3], 10) / 255;
3766
+ } else if (bg.indexOf('#') === 0) {
3767
+ var parts = hexToRgb(bg);
3768
+ r = parts[0]; g = parts[1]; b = parts[2];
3769
+ } else return;
3770
+ var L = relativeLuminance(r, g, b);
3771
+ circle.classList.remove('light-icon', 'dark-icon');
3772
+ circle.classList.add(L > 0.179 ? 'dark-icon' : 'light-icon');
3773
+ }
3774
+ function updateLabel() {
3775
+ var isDark = root.classList.contains('dark');
3776
+ btn.textContent = isDark ? 'Light' : 'Dark';
3777
+ btn.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode');
3778
+ }
3779
+ btn.addEventListener('click', function() {
3780
+ root.classList.toggle('dark');
3781
+ root.setAttribute('data-theme', root.classList.contains('dark') ? 'dark' : 'light');
3782
+ updateLabel();
3783
+ setIconContrast();
3784
+ });
3785
+ if (root.classList.contains('dark') || root.getAttribute('data-theme') === 'dark') {
3786
+ root.classList.add('dark');
3787
+ root.setAttribute('data-theme', 'dark');
3788
+ }
3789
+ updateLabel();
3790
+ setIconContrast();
3791
+ })();
3792
+ </script>
3626
3793
  </body>
3627
3794
  </html>
3628
3795
  `;
3629
- var AI_TOOLS = ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic"];
3630
3796
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
3631
3797
  if (!hasValidAuthConfig() || authFailedNoTools) {
3632
3798
  throw new Error(AUTH_REQUIRED_MESSAGE);
@@ -3681,26 +3847,44 @@ Get your DS ID and token from the Export modal or Settings \u2192 Regenerate Ato
3681
3847
  }]
3682
3848
  };
3683
3849
  }
3684
- const rulesMatch = uri.match(/^atomix:\/\/rules\/(.+)$/);
3850
+ const rulesMatch = uri.match(/^atomix:\/\/rules(?:\/(.+))?$/);
3685
3851
  if (rulesMatch) {
3686
- const tool = rulesMatch[1];
3687
- if (!AI_TOOLS.includes(tool)) {
3688
- throw new Error(`Unknown tool: ${tool}. Available: ${AI_TOOLS.join(", ")}`);
3689
- }
3690
- const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool}`;
3852
+ const topicRaw = rulesMatch[1]?.toLowerCase().trim();
3853
+ const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3691
3854
  const headers = { "Content-Type": "application/json" };
3692
3855
  if (apiKey) headers["x-api-key"] = apiKey;
3693
3856
  const response = await fetch(rulesUrl, { headers });
3694
- if (!response.ok) {
3695
- throw new Error(`Failed to fetch rules: ${response.status}`);
3857
+ if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
3858
+ const payload = await response.json();
3859
+ const categories = payload.categories ?? {};
3860
+ const allRules = payload.rules ?? [];
3861
+ const topicToCategories = {
3862
+ colors: ["general", "colors"],
3863
+ typo: ["general", "typography"],
3864
+ typography: ["general", "typography"],
3865
+ motion: ["general", "motion"],
3866
+ icons: ["general", "icons"],
3867
+ layout: ["general", "spacing", "sizing", "layout"],
3868
+ visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
3869
+ style: ["general", "colors", "borders", "radius", "shadows", "icons"]
3870
+ };
3871
+ if (!topicRaw || !topicToCategories[topicRaw]) {
3872
+ return {
3873
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ rules: allRules, categories }, null, 2) }]
3874
+ };
3875
+ }
3876
+ const categoryKeys = topicToCategories[topicRaw];
3877
+ const filteredCategories = {};
3878
+ const filteredRules = [];
3879
+ for (const key of categoryKeys) {
3880
+ const list = categories[key];
3881
+ if (list?.length) {
3882
+ filteredCategories[key] = list;
3883
+ filteredRules.push(...list);
3884
+ }
3696
3885
  }
3697
- const rulesData = await response.json();
3698
3886
  return {
3699
- contents: [{
3700
- uri,
3701
- mimeType: "text/markdown",
3702
- text: rulesData.content || JSON.stringify(rulesData, null, 2)
3703
- }]
3887
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2) }]
3704
3888
  };
3705
3889
  }
3706
3890
  throw new Error(`Unknown resource: ${uri}`);
@@ -3723,7 +3907,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
3723
3907
  const prompts = [
3724
3908
  { name: "--hello", description: "Get started with this design system - overview, tokens, and tools. Run this first!" },
3725
3909
  { name: "--get-started", description: "Get started with design system in project. Three phases: scan, report and ask, then create only after you approve." },
3726
- { name: "--rules", description: "Get the design system governance rules for your AI coding tool (default: cursor)." },
3910
+ { name: "--rules", description: "Get design system governance rules (optionally by topic: colors, typo, motion, icons, layout, visual)." },
3727
3911
  { name: "--sync", description: "Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use /--refactor to migrate deprecated tokens." },
3728
3912
  { name: "--refactor", description: "Migrate deprecated tokens in codebase. Run after /--sync." },
3729
3913
  { name: "--sync-to-figma", description: "Push this design system to Figma (variables, color + typography styles). Uses local bridge + plugin; no Figma token." }
@@ -3745,7 +3929,16 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
3745
3929
  };
3746
3930
  }
3747
3931
  const canonicalName = name === "--hello" ? "hello" : name === "--get-started" ? "atomix-setup" : name === "--rules" ? "design-system-rules" : name === "--sync" ? "sync" : name === "--refactor" ? "refactor" : name === "--sync-to-figma" || name === "syncToFigma" ? "sync-to-figma" : name;
3748
- const shouldForceRefresh = canonicalName === "sync" || canonicalName === "refactor";
3932
+ const shouldForceRefresh = [
3933
+ "hello",
3934
+ "atomix-setup",
3935
+ // --get-started
3936
+ "design-system-rules",
3937
+ // --rules
3938
+ "sync",
3939
+ "refactor",
3940
+ "sync-to-figma"
3941
+ ].includes(canonicalName);
3749
3942
  let data = null;
3750
3943
  let stats = null;
3751
3944
  try {
@@ -3848,6 +4041,20 @@ Both are required. Configure the MCP server in your AI tool's MCP settings, then
3848
4041
  lines.push(instructions);
3849
4042
  return lines.join("\n");
3850
4043
  };
4044
+ function withMcpNotice(res) {
4045
+ if (!mcpUpdateNotice || res.messages.length === 0) return res;
4046
+ const first = res.messages[0];
4047
+ if (first?.content?.type === "text" && typeof first.content.text === "string") {
4048
+ return {
4049
+ ...res,
4050
+ messages: [
4051
+ { ...first, content: { ...first.content, text: first.content.text + "\n\n---\n\n" + mcpUpdateNotice } },
4052
+ ...res.messages.slice(1)
4053
+ ]
4054
+ };
4055
+ }
4056
+ return res;
4057
+ }
3851
4058
  switch (canonicalName) {
3852
4059
  case "hello": {
3853
4060
  const welcome = generateWelcomeMessage(data, stats);
@@ -3860,7 +4067,7 @@ Do not add any introduction or commentary before the ASCII art. The ASCII art mu
3860
4067
 
3861
4068
  ---
3862
4069
  ${welcome}`;
3863
- return {
4070
+ return withMcpNotice({
3864
4071
  description: `Hello \u2014 ${data.meta.name} Design System`,
3865
4072
  messages: [
3866
4073
  {
@@ -3871,41 +4078,55 @@ ${welcome}`;
3871
4078
  }
3872
4079
  }
3873
4080
  ]
3874
- };
4081
+ });
3875
4082
  }
3876
4083
  case "design-system-rules": {
3877
- const tool = args?.tool || "cursor";
3878
- const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=${tool}`;
4084
+ const topic = args?.topic?.toLowerCase().trim();
4085
+ const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format=json`;
3879
4086
  const headers = { "Content-Type": "application/json" };
3880
4087
  if (apiKey) headers["x-api-key"] = apiKey;
3881
4088
  const response = await fetch(rulesUrl, { headers });
3882
- if (!response.ok) {
3883
- throw new Error(`Failed to fetch rules: ${response.status}`);
4089
+ if (!response.ok) throw new Error(`Failed to fetch rules: ${response.status}`);
4090
+ const payload = await response.json();
4091
+ const categories = payload.categories ?? {};
4092
+ const allRules = payload.rules ?? [];
4093
+ const topicToCategories = {
4094
+ colors: ["general", "colors"],
4095
+ typo: ["general", "typography"],
4096
+ typography: ["general", "typography"],
4097
+ motion: ["general", "motion"],
4098
+ icons: ["general", "icons"],
4099
+ layout: ["general", "spacing", "sizing", "layout"],
4100
+ visual: ["general", "colors", "borders", "radius", "shadows", "icons"],
4101
+ style: ["general", "colors", "borders", "radius", "shadows", "icons"]
4102
+ };
4103
+ let rulesText;
4104
+ if (topic && topicToCategories[topic]) {
4105
+ const categoryKeys = topicToCategories[topic];
4106
+ const filteredCategories = {};
4107
+ const filteredRules = [];
4108
+ for (const key of categoryKeys) {
4109
+ const list = categories[key];
4110
+ if (list?.length) {
4111
+ filteredCategories[key] = list;
4112
+ filteredRules.push(...list);
4113
+ }
4114
+ }
4115
+ rulesText = JSON.stringify({ rules: filteredRules, categories: filteredCategories }, null, 2);
4116
+ } else {
4117
+ rulesText = JSON.stringify({ rules: allRules, categories }, null, 2);
3884
4118
  }
3885
- const rulesData = await response.json();
3886
- return {
3887
- description: `Design system rules for ${tool}`,
4119
+ return withMcpNotice({
4120
+ description: topic ? `Design system rules (topic: ${topic})` : "Design system rules",
3888
4121
  messages: [
3889
- {
3890
- role: "user",
3891
- content: {
3892
- type: "text",
3893
- text: `Show me the design system rules for ${tool}.`
3894
- }
3895
- },
3896
- {
3897
- role: "assistant",
3898
- content: {
3899
- type: "text",
3900
- text: rulesData.content || JSON.stringify(rulesData, null, 2)
3901
- }
3902
- }
4122
+ { role: "user", content: { type: "text", text: topic ? `Show me the design system rules for topic: ${topic}.` : "Show me the design system rules." } },
4123
+ { role: "assistant", content: { type: "text", text: rulesText } }
3903
4124
  ]
3904
- };
4125
+ });
3905
4126
  }
3906
4127
  case "spacing": {
3907
4128
  const instructions = `List all spacing tokens in a table format. Use the listTokens tool with category "spacing" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "spacing.xs" instead of "spacing.scale.xs").`;
3908
- const response = {
4129
+ return withMcpNotice({
3909
4130
  description: "List all spacing tokens",
3910
4131
  messages: [
3911
4132
  {
@@ -3916,12 +4137,11 @@ ${welcome}`;
3916
4137
  }
3917
4138
  }
3918
4139
  ]
3919
- };
3920
- return response;
4140
+ });
3921
4141
  }
3922
4142
  case "radius": {
3923
4143
  const instructions = `List all border radius tokens in a table format. Use the listTokens tool with category "radius" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "radius.sm" instead of "radius.scale.sm").`;
3924
- return {
4144
+ return withMcpNotice({
3925
4145
  description: "List all border radius tokens",
3926
4146
  messages: [
3927
4147
  {
@@ -3932,7 +4152,7 @@ ${welcome}`;
3932
4152
  }
3933
4153
  }
3934
4154
  ]
3935
- };
4155
+ });
3936
4156
  }
3937
4157
  case "color": {
3938
4158
  const instructions = `List all color tokens in a table format showing both light and dark mode values.
@@ -3949,7 +4169,7 @@ For semantic colors (from modes.light/modes.dark), match tokens by name and show
3949
4169
  The Token Name should be in short format:
3950
4170
  - Brand colors: "colors.brand.primary" (not "colors.static.brand.primary")
3951
4171
  - Semantic colors: "colors.bgSurface" (not "colors.modes.light.bgSurface")`;
3952
- return {
4172
+ return withMcpNotice({
3953
4173
  description: "List all color tokens with light/dark mode",
3954
4174
  messages: [
3955
4175
  {
@@ -3960,11 +4180,11 @@ The Token Name should be in short format:
3960
4180
  }
3961
4181
  }
3962
4182
  ]
3963
- };
4183
+ });
3964
4184
  }
3965
4185
  case "typography": {
3966
4186
  const instructions = `List all typography tokens in a table format. Use the listTokens tool with category "typography" (no subcategory needed). Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group tokens by type (fontSize, fontWeight, lineHeight, etc.) with section headers.`;
3967
- return {
4187
+ return withMcpNotice({
3968
4188
  description: "List all typography tokens",
3969
4189
  messages: [
3970
4190
  {
@@ -3975,11 +4195,11 @@ The Token Name should be in short format:
3975
4195
  }
3976
4196
  }
3977
4197
  ]
3978
- };
4198
+ });
3979
4199
  }
3980
4200
  case "shadow": {
3981
4201
  const instructions = `List all shadow/elevation tokens in a table format. Use the listTokens tool with category "shadows" and subcategory "elevation". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "shadows.elevation.md" is fine as-is).`;
3982
- return {
4202
+ return withMcpNotice({
3983
4203
  description: "List all shadow/elevation tokens",
3984
4204
  messages: [
3985
4205
  {
@@ -3990,11 +4210,11 @@ The Token Name should be in short format:
3990
4210
  }
3991
4211
  }
3992
4212
  ]
3993
- };
4213
+ });
3994
4214
  }
3995
4215
  case "border": {
3996
4216
  const instructions = `List all border width tokens in a table format. Use the listTokens tool with category "borders" and subcategory "width". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "borders.width.sm" is fine as-is).`;
3997
- return {
4217
+ return withMcpNotice({
3998
4218
  description: "List all border width tokens",
3999
4219
  messages: [
4000
4220
  {
@@ -4005,7 +4225,7 @@ The Token Name should be in short format:
4005
4225
  }
4006
4226
  }
4007
4227
  ]
4008
- };
4228
+ });
4009
4229
  }
4010
4230
  case "sizing": {
4011
4231
  const instructions = `List all sizing tokens in a table format. Call listTokens twice:
@@ -4013,7 +4233,7 @@ The Token Name should be in short format:
4013
4233
  2. category "sizing" and subcategory "icon" for icon sizes
4014
4234
 
4015
4235
  Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (height vs icon) with section headers.`;
4016
- return {
4236
+ return withMcpNotice({
4017
4237
  description: "List all sizing tokens",
4018
4238
  messages: [
4019
4239
  {
@@ -4024,7 +4244,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4024
4244
  }
4025
4245
  }
4026
4246
  ]
4027
- };
4247
+ });
4028
4248
  }
4029
4249
  case "motion": {
4030
4250
  const instructions = `List all motion tokens in a table format. Call listTokens twice:
@@ -4032,7 +4252,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4032
4252
  2. category "motion" and subcategory "easing" for easing tokens
4033
4253
 
4034
4254
  Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (duration vs easing) with section headers.`;
4035
- return {
4255
+ return withMcpNotice({
4036
4256
  description: "List all motion tokens",
4037
4257
  messages: [
4038
4258
  {
@@ -4043,26 +4263,28 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4043
4263
  }
4044
4264
  }
4045
4265
  ]
4046
- };
4266
+ });
4047
4267
  }
4048
4268
  case "sync": {
4049
4269
  const output = args?.output || "./tokens.css";
4050
4270
  const format = args?.format || "css";
4051
- return {
4271
+ const workspaceRoot = args?.workspaceRoot;
4272
+ const rootHint = workspaceRoot ? ` Use workspaceRoot: "${workspaceRoot}" so files are written inside the repo.` : " If the project root is known, pass workspaceRoot with its absolute path so skills and manifest are written inside the repo (committable).";
4273
+ return withMcpNotice({
4052
4274
  description: "Sync tokens, rules, skills, and dependencies manifest",
4053
4275
  messages: [
4054
4276
  {
4055
4277
  role: "user",
4056
4278
  content: {
4057
4279
  type: "text",
4058
- text: `Call the syncAll tool now. Use output="${output}" and format="${format}". This syncs tokens, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Execute immediately - do not search or ask questions.`
4280
+ text: `Call the syncAll tool now. Use output="${output}" and format="${format}".${rootHint} This syncs tokens, AI rules, skills (.cursor/skills/atomix-ds/*), and atomix-dependencies.json. Execute immediately - do not search or ask questions.`
4059
4281
  }
4060
4282
  }
4061
4283
  ]
4062
- };
4284
+ });
4063
4285
  }
4064
4286
  case "sync-to-figma": {
4065
- return {
4287
+ return withMcpNotice({
4066
4288
  description: "Push design system to Figma via MCP tool",
4067
4289
  messages: [
4068
4290
  {
@@ -4073,34 +4295,34 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4073
4295
  }
4074
4296
  }
4075
4297
  ]
4076
- };
4298
+ });
4077
4299
  }
4078
4300
  case "refactor": {
4079
4301
  const refactorOutput = args?.output || "./tokens.css";
4080
4302
  const refactorFormat = args?.format || "css";
4081
- const refactorOutputPath = path2.resolve(process.cwd(), refactorOutput);
4082
- const refactorFileExists = fs2.existsSync(refactorOutputPath);
4303
+ const refactorOutputPath = path.resolve(process.cwd(), refactorOutput);
4304
+ const refactorFileExists = fs.existsSync(refactorOutputPath);
4083
4305
  if (!data) {
4084
- return {
4306
+ return withMcpNotice({
4085
4307
  description: "Refactor codebase for deprecated tokens",
4086
4308
  messages: [{
4087
4309
  role: "user",
4088
4310
  content: { type: "text", text: `Failed to fetch design system from DB. Check your --ds-id and --atomix-token configuration.` }
4089
4311
  }]
4090
- };
4312
+ });
4091
4313
  }
4092
4314
  if (!refactorFileExists) {
4093
- return {
4315
+ return withMcpNotice({
4094
4316
  description: "Refactor codebase for deprecated tokens",
4095
4317
  messages: [{
4096
4318
  role: "user",
4097
4319
  content: { type: "text", text: `No token file found at \`${refactorOutput}\`. Please run \`/--sync\` first to create your token file, then run \`/--refactor\` to scan your codebase for deprecated token usage.` }
4098
4320
  }]
4099
- };
4321
+ });
4100
4322
  }
4101
4323
  const deprecatedTokens = /* @__PURE__ */ new Map();
4102
4324
  if (["css", "scss", "less"].includes(refactorFormat)) {
4103
- const oldContent = fs2.readFileSync(refactorOutputPath, "utf-8");
4325
+ const oldContent = fs.readFileSync(refactorOutputPath, "utf-8");
4104
4326
  const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
4105
4327
  let match;
4106
4328
  while ((match = oldVarPattern.exec(oldContent)) !== null) {
@@ -4112,7 +4334,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4112
4334
  const dsVersion = data.meta.version ?? "?";
4113
4335
  const dsExportedAt = data.meta.exportedAt ? new Date(data.meta.exportedAt).toLocaleString() : "N/A";
4114
4336
  if (deprecatedTokens.size === 0) {
4115
- return {
4337
+ return withMcpNotice({
4116
4338
  description: "Refactor codebase for deprecated tokens",
4117
4339
  messages: [{
4118
4340
  role: "user",
@@ -4120,7 +4342,7 @@ Format the response as a markdown table with columns: Token Name | Value | CSS V
4120
4342
 
4121
4343
  Your token file \`${refactorOutput}\` is aligned with the design system (v${dsVersion}, exported ${dsExportedAt}). No tokens need migration.` }
4122
4344
  }]
4123
- };
4345
+ });
4124
4346
  }
4125
4347
  const format = refactorFormat;
4126
4348
  const isNativeFormat = ["swift", "kotlin", "dart"].includes(format);
@@ -4235,18 +4457,18 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4235
4457
 
4236
4458
  - Run only when the user has said yes (all or specific items).
4237
4459
  - For each approved item:
4238
- - **Skill:** Write the skill content from getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md).
4239
- - **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.
4460
+ - **Skill:** Prefer calling **syncAll** (it writes the skill into the repo). If writing the skill manually, write getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md) under the project root.
4461
+ - **Token file and skills in repo:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css") and **workspaceRoot** set to the absolute path of the current project/workspace root. This ensures .cursor/skills/atomix-ds/SKILL.md and atomix-dependencies.json are written inside the repo so they can be committed. You must call syncAll; do not only suggest the user run it later.
4240
4462
  - **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.
4241
4463
  - **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.
4242
- - **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 (if you created a typeset CSS file, use the full \`<link rel="stylesheet" href="\u2026">\` tag; otherwise empty string), DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1 fallback), FONT_FAMILY_VAR (body fallback), LARGEST_DISPLAY_TYPESET_CLASS (largest display typeset from listTypesets) and BODY_TYPESET_CLASS (e.g. typeset-body-md; leave empty if no typeset file), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4464
+ - **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).
4243
4465
  - Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
4244
4466
  - **After reporting \u2013 styles/theme:**
4245
4467
  - **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014call **listTypesets** and 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 for a 1:1 match. Use the CSS variable names returned by listTypesets. The output must not be only a font import; it must define every typeset with every style detail from the design system.
4246
4468
  - **iOS/Android:** If the project already has theme/style files: recommend how to integrate Atomix tokens. Do not suggest a new global theme. Only if there is **no** theme/style at all, ask once: "There's no theme/style setup yet. Do you want a minimal token-based theme?" and add only if the user says yes.
4247
4469
 
4248
4470
  Create your todo list first, then Phase 1 (resolve platform/stack, call getDependencies, scan, build lists), then Phase 2 (report and ask). Do not perform Phase 3 until the user replies.`;
4249
- return {
4471
+ return withMcpNotice({
4250
4472
  description: "Get started with design system in project (/--get-started). Create todo list; Phase 1 scan, Phase 2 report and ask, Phase 3 create only after user approval.",
4251
4473
  messages: [
4252
4474
  {
@@ -4257,7 +4479,7 @@ Create your todo list first, then Phase 1 (resolve platform/stack, call getDepen
4257
4479
  }
4258
4480
  }
4259
4481
  ]
4260
- };
4482
+ });
4261
4483
  }
4262
4484
  default:
4263
4485
  throw new Error(`Unknown prompt: ${name}`);