@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/README.md +2 -2
- package/dist/index.js +498 -276
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
1996
|
-
import * as
|
|
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((
|
|
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:
|
|
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 (!
|
|
2166
|
+
if (!fs.existsSync(outputPath)) {
|
|
2222
2167
|
return { path: outputPath, status: "FAIL", detail: "File not found after write." };
|
|
2223
2168
|
}
|
|
2224
|
-
const content =
|
|
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 :
|
|
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:
|
|
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: "
|
|
2445
|
-
description: "
|
|
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
|
-
|
|
2402
|
+
topic: {
|
|
2450
2403
|
type: "string",
|
|
2451
|
-
enum: ["
|
|
2452
|
-
description: "
|
|
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.
|
|
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 =
|
|
2564
|
-
const fileExists =
|
|
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 =
|
|
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 =
|
|
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
|
-
"
|
|
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 =
|
|
2684
|
-
if (!
|
|
2685
|
-
|
|
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
|
-
|
|
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
|
|
2729
|
-
const value = getTokenByPath(data.tokens,
|
|
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: ${
|
|
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-${
|
|
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:
|
|
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:
|
|
2776
|
-
const fullPath = subcategory ? `${category}.${subcategory}.${
|
|
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-${
|
|
2739
|
+
cssVar = data.cssVariables[`--atmx-color-brand-${path2}`];
|
|
2780
2740
|
} else if (category === "colors" && subcategory?.startsWith("modes.")) {
|
|
2781
|
-
cssVar = data.cssVariables[`--atmx-color-${
|
|
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 "
|
|
2891
|
-
const
|
|
2892
|
-
const
|
|
2893
|
-
|
|
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(`[
|
|
2859
|
+
console.error(`[getRules] Response status: ${response.status}`);
|
|
2899
2860
|
if (!response.ok) {
|
|
2900
2861
|
const errorText = await response.text();
|
|
2901
|
-
console.error(`[
|
|
2862
|
+
console.error(`[getRules] Error response: ${errorText}`);
|
|
2902
2863
|
throw new Error(`Failed to fetch rules: ${response.status} - ${errorText}`);
|
|
2903
2864
|
}
|
|
2904
|
-
const
|
|
2905
|
-
|
|
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(`[
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
3082
|
-
2.
|
|
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 =
|
|
3137
|
-
const skillPath1 =
|
|
3138
|
-
const manifestPath =
|
|
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 (!
|
|
3141
|
+
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
3150
3142
|
const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
|
|
3151
|
-
|
|
3152
|
-
allValidation.push({ path: skillPath1, status:
|
|
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
|
-
|
|
3188
|
-
allValidation.push({ path: manifestPath, status:
|
|
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: '
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
-
|
|
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
|
-
|
|
3570
|
+
Call \`getRules\` for the full mapping of semantic tokens to primitives in each theme.
|
|
3543
3571
|
|
|
3544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:**
|
|
3565
|
-
- **
|
|
3566
|
-
- **
|
|
3567
|
-
- **
|
|
3568
|
-
- **
|
|
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:
|
|
3585
|
-
color:
|
|
3649
|
+
background: var(--atmx-color-bg-page);
|
|
3650
|
+
color: var(--atmx-color-text-primary);
|
|
3586
3651
|
min-height: 100vh;
|
|
3587
|
-
padding:
|
|
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
|
-
.
|
|
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
|
-
|
|
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 {
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
.
|
|
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="
|
|
3612
|
-
<
|
|
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:
|
|
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
|
|
3687
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
3878
|
-
const rulesUrl = `${apiBase}/api/ds/${dsId}/rules?format
|
|
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
|
-
|
|
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
|
-
|
|
3886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}"
|
|
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 =
|
|
4082
|
-
const refactorFileExists =
|
|
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 =
|
|
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:**
|
|
4239
|
-
- **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css").
|
|
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
|
|
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}`);
|