@atomixstudio/mcp 0.1.1 → 1.0.1

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/src/index.ts DELETED
@@ -1,1155 +0,0 @@
1
- /**
2
- * Atomix MCP Server
3
- *
4
- * Exposes design tokens to AI tools via the Model Context Protocol.
5
- *
6
- * Tools:
7
- * - getToken: Get a specific token by path
8
- * - listTokens: List all tokens in a category
9
- * - getComponentTokens: Get tokens for a specific component
10
- * - validateUsage: Check if a value follows the design system
11
- * - generateCursorRules: Generate .cursorrules for a project
12
- */
13
-
14
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
- import {
17
- CallToolRequestSchema,
18
- ListToolsRequestSchema,
19
- ListResourcesRequestSchema,
20
- ReadResourceRequestSchema,
21
- } from "@modelcontextprotocol/sdk/types.js";
22
-
23
- import { loadPrimitives } from "./tokens.js";
24
- import {
25
- getTokenByPath,
26
- listTokensInCategory,
27
- flattenTokens,
28
- getTokenCategories,
29
- getCssVariableName,
30
- getTokenMetadata,
31
- isSemanticToken,
32
- type TokenTier,
33
- } from "./utils.js";
34
- import { COMPONENT_TOKENS } from "./component-tokens.js";
35
- import {
36
- parseCLIArgs,
37
- isUserDSMode,
38
- fetchUserDesignSystem,
39
- transformUserTokens,
40
- } from "./user-tokens.js";
41
- import {
42
- generateCursorRules,
43
- generateRulesForTool,
44
- generateRulesForAllTools,
45
- getSupportedAITools,
46
- generateToolSetupGuide,
47
- generateMCPConfig,
48
- generateAllMCPConfigs,
49
- getSetupInstructions,
50
- AI_TOOLS,
51
- type AIToolId,
52
- type MCPConfigToolId,
53
- } from "./ai-rules-generator.js";
54
-
55
- // ============================================
56
- // HELPERS
57
- // ============================================
58
-
59
- /**
60
- * Format a timestamp as a human-readable "time ago" string
61
- */
62
- function formatTimeAgo(timestamp: number): string {
63
- const now = Date.now();
64
- const diffMs = now - timestamp;
65
- const diffSec = Math.floor(diffMs / 1000);
66
- const diffMin = Math.floor(diffSec / 60);
67
- const diffHour = Math.floor(diffMin / 60);
68
- const diffDay = Math.floor(diffHour / 24);
69
-
70
- if (diffSec < 60) return "just now";
71
- if (diffMin < 60) return `${diffMin} minute${diffMin > 1 ? "s" : ""} ago`;
72
- if (diffHour < 24) return `${diffHour} hour${diffHour > 1 ? "s" : ""} ago`;
73
- if (diffDay < 7) return `${diffDay} day${diffDay > 1 ? "s" : ""} ago`;
74
-
75
- // For older dates, show the date
76
- const date = new Date(timestamp);
77
- return date.toLocaleDateString("en-US", {
78
- month: "short",
79
- day: "numeric",
80
- year: date.getFullYear() !== new Date().getFullYear() ? "numeric" : undefined
81
- });
82
- }
83
-
84
- // ============================================
85
- // SERVER SETUP
86
- // ============================================
87
-
88
- const server = new Server(
89
- {
90
- name: "atomix-mcp",
91
- version: "0.1.0",
92
- },
93
- {
94
- capabilities: {
95
- tools: {},
96
- resources: {},
97
- },
98
- }
99
- );
100
-
101
- // ============================================
102
- // TOOL DEFINITIONS
103
- // ============================================
104
-
105
- server.setRequestHandler(ListToolsRequestSchema, async () => {
106
- return {
107
- tools: [
108
- {
109
- name: "getToken",
110
- description: "Get a specific design token by its path. Returns value, CSS variable, and tier metadata. IMPORTANT: Tokens are tiered - 'primitive' tokens are read-only reference values, 'semantic' tokens are the primary API for styling.",
111
- inputSchema: {
112
- type: "object",
113
- properties: {
114
- path: {
115
- type: "string",
116
- description: "Token path in dot notation. Prefer semantic paths (colors.modes.light.bgPage) over primitive paths (colors.scales.green.500)",
117
- },
118
- tenantId: {
119
- type: "string",
120
- description: "Tenant ID for multi-tenant support (optional, defaults to 'default')",
121
- },
122
- },
123
- required: ["path"],
124
- },
125
- },
126
- {
127
- name: "listTokens",
128
- description: "List tokens in a category. Use 'tier' filter to get only semantic tokens (recommended) or primitives (reference only). Semantic tokens are the primary API; primitives are read-only reference values.",
129
- inputSchema: {
130
- type: "object",
131
- properties: {
132
- category: {
133
- type: "string",
134
- enum: ["colors", "typography", "spacing", "sizing", "shadows", "radius", "motion", "zIndex", "borders"],
135
- description: "Token category to list",
136
- },
137
- subcategory: {
138
- type: "string",
139
- description: "Optional subcategory, e.g., 'modes.light' for semantic colors, 'scale' for primitive spacing",
140
- },
141
- tier: {
142
- type: "string",
143
- enum: ["semantic", "primitive", "all"],
144
- description: "Filter by token tier. 'semantic' (recommended) returns purpose-driven tokens for styling. 'primitive' returns raw reference values. Default: 'all'",
145
- },
146
- tenantId: {
147
- type: "string",
148
- description: "Tenant ID for multi-tenant support (optional, defaults to 'default')",
149
- },
150
- },
151
- required: ["category"],
152
- },
153
- },
154
- {
155
- name: "getComponentTokens",
156
- description: "Get all design tokens used by a specific component (Button, Card, Dialog, etc.)",
157
- inputSchema: {
158
- type: "object",
159
- properties: {
160
- component: {
161
- type: "string",
162
- enum: ["button", "card", "dialog", "input", "select", "heading", "checkbox", "radio", "toggle", "selectionControls"],
163
- description: "Component name",
164
- },
165
- variant: {
166
- type: "string",
167
- description: "Optional variant name (e.g., 'primary', 'outline', 'ghost')",
168
- },
169
- size: {
170
- type: "string",
171
- description: "Optional size name (e.g., 'sm', 'md', 'lg')",
172
- },
173
- tenantId: {
174
- type: "string",
175
- description: "Tenant ID for multi-tenant support (optional, defaults to 'default')",
176
- },
177
- },
178
- required: ["component"],
179
- },
180
- },
181
- {
182
- name: "validateUsage",
183
- description: "Check if a CSS value follows the Atomix design system. Detects arbitrary values that should use tokens.",
184
- inputSchema: {
185
- type: "object",
186
- properties: {
187
- value: {
188
- type: "string",
189
- description: "CSS value to validate, e.g., '#ff0000', '16px', 'rgb(0,112,97)'",
190
- },
191
- context: {
192
- type: "string",
193
- enum: ["color", "spacing", "radius", "shadow", "typography", "any"],
194
- description: "Context of the value (helps find the right token)",
195
- },
196
- },
197
- required: ["value"],
198
- },
199
- },
200
- {
201
- name: "generateCursorRules",
202
- description: "[DEPRECATED] Use 'getAIToolRules' instead. Generate .cursorrules content for a project.",
203
- inputSchema: {
204
- type: "object",
205
- properties: {
206
- projectName: {
207
- type: "string",
208
- description: "Name of the project",
209
- },
210
- strict: {
211
- type: "boolean",
212
- description: "Whether to enforce strict token usage (no arbitrary values)",
213
- },
214
- },
215
- required: [],
216
- },
217
- },
218
- {
219
- name: "getAIToolRules",
220
- description: "Generate design system rules for AI coding tools. Supports Cursor, GitHub Copilot, Windsurf, Cline, Continue, Zed, and more.",
221
- inputSchema: {
222
- type: "object",
223
- properties: {
224
- tool: {
225
- type: "string",
226
- enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic", "all"],
227
- description: "AI tool to generate rules for. Use 'all' to generate for all supported tools.",
228
- },
229
- projectName: {
230
- type: "string",
231
- description: "Name of the project (default: 'My Project')",
232
- },
233
- strict: {
234
- type: "boolean",
235
- description: "Enforce strict token usage - no arbitrary values allowed (default: true)",
236
- },
237
- },
238
- required: ["tool"],
239
- },
240
- },
241
- {
242
- name: "listAITools",
243
- description: "List all supported AI coding tools and their rules file conventions.",
244
- inputSchema: {
245
- type: "object",
246
- properties: {},
247
- required: [],
248
- },
249
- },
250
- {
251
- name: "getAIToolSetupGuide",
252
- description: "Get a complete setup guide for integrating Atomix with AI coding tools.",
253
- inputSchema: {
254
- type: "object",
255
- properties: {
256
- projectName: {
257
- type: "string",
258
- description: "Name of the project",
259
- },
260
- },
261
- required: [],
262
- },
263
- },
264
- {
265
- name: "exportMCPConfig",
266
- description: "Generate MCP configuration file for AI tools. Creates ready-to-use config for Cursor (.cursor/mcp.json), Claude Desktop, Windsurf, Continue, or VS Code.",
267
- inputSchema: {
268
- type: "object",
269
- properties: {
270
- tool: {
271
- type: "string",
272
- enum: ["cursor", "claude-desktop", "windsurf", "continue", "vscode", "all"],
273
- description: "AI tool to generate MCP config for. Use 'all' to generate for all supported tools.",
274
- },
275
- tenantId: {
276
- type: "string",
277
- description: "Tenant ID for multi-tenant setups (optional, defaults to 'default')",
278
- },
279
- projectName: {
280
- type: "string",
281
- description: "Project name for identification (optional)",
282
- },
283
- useNpx: {
284
- type: "boolean",
285
- description: "Use npx to run the server (default: true). Set to false for local development.",
286
- },
287
- },
288
- required: ["tool"],
289
- },
290
- },
291
- {
292
- name: "getSetupInstructions",
293
- description: "Get detailed step-by-step setup instructions for a specific AI tool. Includes MCP configuration, rules file setup, and verification steps.",
294
- inputSchema: {
295
- type: "object",
296
- properties: {
297
- tool: {
298
- type: "string",
299
- enum: ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "claude-desktop", "generic"],
300
- description: "AI tool to get setup instructions for.",
301
- },
302
- },
303
- required: ["tool"],
304
- },
305
- },
306
- {
307
- name: "searchTokens",
308
- description: "Search for tokens by name or value. Results include tier metadata - prefer using 'semantic' tier tokens in code, use 'primitive' tier only for reference/validation.",
309
- inputSchema: {
310
- type: "object",
311
- properties: {
312
- query: {
313
- type: "string",
314
- description: "Search query (matches token paths or values)",
315
- },
316
- tier: {
317
- type: "string",
318
- enum: ["semantic", "primitive", "all"],
319
- description: "Filter results by tier. 'semantic' recommended for styling. Default: 'all'",
320
- },
321
- },
322
- required: ["query"],
323
- },
324
- },
325
- ],
326
- };
327
- });
328
-
329
- // ============================================
330
- // TOOL HANDLERS
331
- // ============================================
332
-
333
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
334
- const { name, arguments: args } = request.params;
335
-
336
- switch (name) {
337
- case "getToken": {
338
- const path = args?.path as string;
339
- const value = getTokenByPath(primitives, path);
340
-
341
- if (value === undefined) {
342
- return {
343
- content: [
344
- {
345
- type: "text",
346
- text: JSON.stringify({
347
- error: `Token not found: ${path}`,
348
- suggestion: `Use listTokens to see available tokens. Available categories: ${getTokenCategories().join(", ")}`,
349
- }, null, 2),
350
- },
351
- ],
352
- };
353
- }
354
-
355
- const cssVar = getCssVariableName(path);
356
- const metadata = getTokenMetadata(path);
357
-
358
- return {
359
- content: [
360
- {
361
- type: "text",
362
- text: JSON.stringify({
363
- path,
364
- value,
365
- cssVariable: cssVar,
366
- // Token tier classification
367
- tier: metadata.tier,
368
- mutable: metadata.mutable,
369
- editVia: metadata.editVia || null,
370
- guidance: metadata.guidance,
371
- // Usage examples
372
- usage: metadata.tier === "semantic" ? {
373
- css: `var(${cssVar})`,
374
- tailwind: getTailwindClass(path, value),
375
- recommendation: "Use this token in your components.",
376
- } : {
377
- css: `var(${cssVar})`,
378
- tailwind: getTailwindClass(path, value),
379
- recommendation: "This is a primitive (read-only reference). Consider using a semantic token instead.",
380
- semanticAlternatives: getSuggestedSemanticTokens(path),
381
- },
382
- }, null, 2),
383
- },
384
- ],
385
- };
386
- }
387
-
388
- case "listTokens": {
389
- const category = args?.category as string;
390
- const subcategory = args?.subcategory as string | undefined;
391
- const tierFilter = (args?.tier as TokenTier | "all") || "all";
392
-
393
- const rawTokens = listTokensInCategory(primitives, category, subcategory);
394
-
395
- // Apply tier filter and add metadata
396
- const tokensWithMetadata: Record<string, {
397
- value: unknown;
398
- tier: TokenTier;
399
- mutable: boolean;
400
- guidance: string;
401
- }> = {};
402
-
403
- for (const [tokenPath, value] of Object.entries(rawTokens)) {
404
- const fullPath = subcategory
405
- ? `${category}.${subcategory}.${tokenPath}`
406
- : `${category}.${tokenPath}`;
407
- const metadata = getTokenMetadata(fullPath);
408
-
409
- // Apply tier filter
410
- if (tierFilter !== "all" && metadata.tier !== tierFilter) {
411
- continue;
412
- }
413
-
414
- tokensWithMetadata[tokenPath] = {
415
- value,
416
- tier: metadata.tier,
417
- mutable: metadata.mutable,
418
- guidance: metadata.guidance,
419
- };
420
- }
421
-
422
- return {
423
- content: [
424
- {
425
- type: "text",
426
- text: JSON.stringify({
427
- category,
428
- subcategory: subcategory || null,
429
- tierFilter,
430
- count: Object.keys(tokensWithMetadata).length,
431
- note: tierFilter === "all"
432
- ? "Results include both primitive (read-only) and semantic (usable) tokens. Use tier='semantic' for styling recommendations."
433
- : tierFilter === "semantic"
434
- ? "Showing semantic tokens - these are the recommended tokens for styling."
435
- : "Showing primitive tokens - use these for reference/validation only, not direct styling.",
436
- tokens: tokensWithMetadata,
437
- }, null, 2),
438
- },
439
- ],
440
- };
441
- }
442
-
443
- case "getComponentTokens": {
444
- const component = args?.component as string;
445
- const variant = args?.variant as string | undefined;
446
- const size = args?.size as string | undefined;
447
-
448
- const componentKey = component.toLowerCase();
449
- const componentData = COMPONENT_TOKENS[componentKey];
450
-
451
- if (!componentData) {
452
- return {
453
- content: [
454
- {
455
- type: "text",
456
- text: JSON.stringify({
457
- error: `Component not found: ${component}`,
458
- available: Object.keys(COMPONENT_TOKENS),
459
- }, null, 2),
460
- },
461
- ],
462
- };
463
- }
464
-
465
- let result = { ...componentData };
466
-
467
- // Filter by variant if specified
468
- if (variant && result.variants) {
469
- result.variants = { [variant]: result.variants[variant] };
470
- }
471
-
472
- // Filter by size if specified
473
- if (size && result.sizes) {
474
- result.sizes = { [size]: result.sizes[size] };
475
- }
476
-
477
- return {
478
- content: [
479
- {
480
- type: "text",
481
- text: JSON.stringify(result, null, 2),
482
- },
483
- ],
484
- };
485
- }
486
-
487
- case "validateUsage": {
488
- const value = args?.value as string;
489
- const context = (args?.context as string) || "any";
490
-
491
- const validation = validateValue(value, context);
492
-
493
- return {
494
- content: [
495
- {
496
- type: "text",
497
- text: JSON.stringify(validation, null, 2),
498
- },
499
- ],
500
- };
501
- }
502
-
503
- case "generateCursorRules": {
504
- // DEPRECATED: Forward to getAIToolRules
505
- const projectName = (args?.projectName as string) || "My Project";
506
- const strict = (args?.strict as boolean) ?? true;
507
-
508
- const rules = generateCursorRules(projectName, strict);
509
-
510
- return {
511
- content: [
512
- {
513
- type: "text",
514
- text: `<!-- DEPRECATED: Use getAIToolRules instead -->\n${rules}`,
515
- },
516
- ],
517
- };
518
- }
519
-
520
- case "getAIToolRules": {
521
- const tool = args?.tool as string;
522
- const projectName = (args?.projectName as string) || "My Project";
523
- const strict = (args?.strict as boolean) ?? true;
524
-
525
- if (tool === "all") {
526
- // Generate for all tools
527
- const allRules = generateRulesForAllTools(projectName, strict);
528
-
529
- return {
530
- content: [
531
- {
532
- type: "text",
533
- text: JSON.stringify({
534
- message: `Generated rules for ${allRules.length} AI tools`,
535
- tools: allRules.map(r => ({
536
- tool: r.tool.name,
537
- filename: r.filename,
538
- path: r.path,
539
- description: r.tool.description,
540
- })),
541
- files: allRules.map(r => ({
542
- path: r.path,
543
- content: r.content,
544
- })),
545
- }, null, 2),
546
- },
547
- ],
548
- };
549
- }
550
-
551
- // Generate for specific tool
552
- try {
553
- const result = generateRulesForTool(tool as AIToolId, projectName, strict);
554
-
555
- return {
556
- content: [
557
- {
558
- type: "text",
559
- text: JSON.stringify({
560
- tool: result.tool.name,
561
- filename: result.filename,
562
- path: result.path,
563
- description: result.tool.description,
564
- content: result.content,
565
- }, null, 2),
566
- },
567
- ],
568
- };
569
- } catch (error) {
570
- return {
571
- content: [
572
- {
573
- type: "text",
574
- text: JSON.stringify({
575
- error: String(error),
576
- availableTools: Object.keys(AI_TOOLS),
577
- }, null, 2),
578
- },
579
- ],
580
- };
581
- }
582
- }
583
-
584
- case "listAITools": {
585
- const tools = getSupportedAITools();
586
-
587
- return {
588
- content: [
589
- {
590
- type: "text",
591
- text: JSON.stringify({
592
- count: tools.length,
593
- tools: tools.map(t => ({
594
- id: t.id,
595
- name: t.name,
596
- rulesFile: t.rulesPath,
597
- description: t.description,
598
- })),
599
- note: "Use getAIToolRules({ tool: 'toolId' }) to generate rules for a specific tool.",
600
- }, null, 2),
601
- },
602
- ],
603
- };
604
- }
605
-
606
- case "getAIToolSetupGuide": {
607
- const projectName = (args?.projectName as string) || "My Project";
608
- const guide = generateToolSetupGuide(projectName);
609
-
610
- return {
611
- content: [
612
- {
613
- type: "text",
614
- text: guide,
615
- },
616
- ],
617
- };
618
- }
619
-
620
- case "exportMCPConfig": {
621
- const tool = args?.tool as string;
622
- const tenantId = (args?.tenantId as string) || "default";
623
- const projectName = (args?.projectName as string) || "my-project";
624
- const useNpx = (args?.useNpx as boolean) ?? true;
625
-
626
- const options = { tenantId, projectName, useNpx };
627
-
628
- if (tool === "all") {
629
- // Generate for all tools
630
- const allConfigs = generateAllMCPConfigs(options);
631
-
632
- return {
633
- content: [
634
- {
635
- type: "text",
636
- text: JSON.stringify({
637
- message: `Generated MCP configs for ${allConfigs.length} AI tools`,
638
- configs: allConfigs.map(c => ({
639
- tool: c.tool,
640
- path: c.path,
641
- filename: c.filename,
642
- })),
643
- files: allConfigs.map(c => ({
644
- tool: c.tool,
645
- path: c.path,
646
- content: c.content,
647
- instructions: c.instructions,
648
- })),
649
- }, null, 2),
650
- },
651
- ],
652
- };
653
- }
654
-
655
- // Generate for specific tool
656
- try {
657
- const config = generateMCPConfig(tool as MCPConfigToolId, options);
658
-
659
- return {
660
- content: [
661
- {
662
- type: "text",
663
- text: JSON.stringify({
664
- tool: config.tool,
665
- path: config.path,
666
- filename: config.filename,
667
- content: config.content,
668
- instructions: config.instructions,
669
- }, null, 2),
670
- },
671
- ],
672
- };
673
- } catch (error) {
674
- return {
675
- content: [
676
- {
677
- type: "text",
678
- text: JSON.stringify({
679
- error: String(error),
680
- availableTools: ["cursor", "claude-desktop", "windsurf", "continue", "vscode", "all"],
681
- }, null, 2),
682
- },
683
- ],
684
- };
685
- }
686
- }
687
-
688
- case "getSetupInstructions": {
689
- const toolId = args?.tool as AIToolId;
690
-
691
- try {
692
- const instructions = getSetupInstructions(toolId);
693
-
694
- return {
695
- content: [
696
- {
697
- type: "text",
698
- text: instructions,
699
- },
700
- ],
701
- };
702
- } catch (error) {
703
- return {
704
- content: [
705
- {
706
- type: "text",
707
- text: JSON.stringify({
708
- error: String(error),
709
- availableTools: Object.keys(AI_TOOLS),
710
- }, null, 2),
711
- },
712
- ],
713
- };
714
- }
715
- }
716
-
717
- case "searchTokens": {
718
- const query = (args?.query as string).toLowerCase();
719
- const tierFilter = (args?.tier as TokenTier | "all") || "all";
720
- const allTokens = flattenTokens(primitives);
721
-
722
- const matches = Object.entries(allTokens)
723
- .filter(([path, value]) => {
724
- const pathLower = path.toLowerCase();
725
- const valueStr = String(value).toLowerCase();
726
- const matchesQuery = pathLower.includes(query) || valueStr.includes(query);
727
-
728
- if (!matchesQuery) return false;
729
-
730
- // Apply tier filter
731
- if (tierFilter !== "all") {
732
- const metadata = getTokenMetadata(path);
733
- if (metadata.tier !== tierFilter) return false;
734
- }
735
-
736
- return true;
737
- })
738
- .slice(0, 50) // Limit results
739
- .map(([path, value]) => {
740
- const metadata = getTokenMetadata(path);
741
- return {
742
- path,
743
- value,
744
- cssVariable: getCssVariableName(path),
745
- tier: metadata.tier,
746
- mutable: metadata.mutable,
747
- guidance: metadata.guidance,
748
- };
749
- });
750
-
751
- // Sort semantic tokens first
752
- matches.sort((a, b) => {
753
- if (a.tier === "semantic" && b.tier !== "semantic") return -1;
754
- if (a.tier !== "semantic" && b.tier === "semantic") return 1;
755
- return 0;
756
- });
757
-
758
- return {
759
- content: [
760
- {
761
- type: "text",
762
- text: JSON.stringify({
763
- query,
764
- tierFilter,
765
- count: matches.length,
766
- note: "Semantic tokens are listed first. Use semantic tokens for styling; primitives are for reference only.",
767
- matches,
768
- }, null, 2),
769
- },
770
- ],
771
- };
772
- }
773
-
774
- default:
775
- return {
776
- content: [
777
- {
778
- type: "text",
779
- text: JSON.stringify({ error: `Unknown tool: ${name}` }),
780
- },
781
- ],
782
- };
783
- }
784
- });
785
-
786
- // ============================================
787
- // RESOURCE DEFINITIONS
788
- // ============================================
789
-
790
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
791
- return {
792
- resources: [
793
- {
794
- uri: "atomix://tokens/all",
795
- name: "All Atomix Tokens",
796
- description: "Complete design token reference",
797
- mimeType: "application/json",
798
- },
799
- {
800
- uri: "atomix://tokens/colors",
801
- name: "Color Tokens",
802
- description: "All color tokens (static, scales, modes)",
803
- mimeType: "application/json",
804
- },
805
- {
806
- uri: "atomix://tokens/typography",
807
- name: "Typography Tokens",
808
- description: "Font families, sizes, weights, line heights",
809
- mimeType: "application/json",
810
- },
811
- {
812
- uri: "atomix://tokens/spacing",
813
- name: "Spacing Tokens",
814
- description: "Spacing scale, insets, gaps",
815
- mimeType: "application/json",
816
- },
817
- {
818
- uri: "atomix://components",
819
- name: "Component Tokens",
820
- description: "Token mappings for all components",
821
- mimeType: "application/json",
822
- },
823
- ],
824
- };
825
- });
826
-
827
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
828
- const uri = request.params.uri;
829
-
830
- switch (uri) {
831
- case "atomix://tokens/all":
832
- return {
833
- contents: [
834
- {
835
- uri,
836
- mimeType: "application/json",
837
- text: JSON.stringify(primitives, null, 2),
838
- },
839
- ],
840
- };
841
-
842
- case "atomix://tokens/colors":
843
- return {
844
- contents: [
845
- {
846
- uri,
847
- mimeType: "application/json",
848
- text: JSON.stringify(primitives.colors, null, 2),
849
- },
850
- ],
851
- };
852
-
853
- case "atomix://tokens/typography":
854
- return {
855
- contents: [
856
- {
857
- uri,
858
- mimeType: "application/json",
859
- text: JSON.stringify(primitives.typography, null, 2),
860
- },
861
- ],
862
- };
863
-
864
- case "atomix://tokens/spacing":
865
- return {
866
- contents: [
867
- {
868
- uri,
869
- mimeType: "application/json",
870
- text: JSON.stringify(primitives.spacing, null, 2),
871
- },
872
- ],
873
- };
874
-
875
- case "atomix://components":
876
- return {
877
- contents: [
878
- {
879
- uri,
880
- mimeType: "application/json",
881
- text: JSON.stringify(COMPONENT_TOKENS, null, 2),
882
- },
883
- ],
884
- };
885
-
886
- default:
887
- throw new Error(`Unknown resource: ${uri}`);
888
- }
889
- });
890
-
891
- // ============================================
892
- // HELPER FUNCTIONS
893
- // ============================================
894
-
895
- function getTailwindClass(path: string, value: unknown): string | null {
896
- // Map common token paths to Tailwind classes
897
- if (path.startsWith("colors.static.brand.primary")) return "bg-brand text-brand";
898
- // Spacing: flat structure (spacing.xs, spacing.md, etc.)
899
- if (path.startsWith("spacing.") && !path.includes(".inset")) {
900
- const size = path.split(".").pop();
901
- return `p-${size} m-${size} gap-${size}`;
902
- }
903
- // Radius: flat structure (radius.xs, radius.md, etc.)
904
- if (path.startsWith("radius.")) {
905
- const size = path.split(".").pop();
906
- return `rounded-${size}`;
907
- }
908
- return null;
909
- }
910
-
911
- /**
912
- * Suggest semantic token alternatives for a primitive token path
913
- */
914
- function getSuggestedSemanticTokens(primitivePath: string): string[] {
915
- const suggestions: string[] = [];
916
-
917
- // Color scale → Mode colors
918
- if (primitivePath.startsWith("colors.scales.")) {
919
- suggestions.push(
920
- "colors.modes.light.bgPage (page background)",
921
- "colors.modes.light.bgSurface (card/panel background)",
922
- "colors.modes.light.textPrimary (main text)",
923
- "colors.modes.light.borderPrimary (borders)",
924
- "colors.modes.light.actionPrimary (interactive elements)"
925
- );
926
- }
927
-
928
- // Spacing → Use flat spacing tokens (spacing.xs, spacing.md, etc.)
929
- if (primitivePath.startsWith("spacing.")) {
930
- suggestions.push(
931
- "Use spacing tokens (spacing.xs, spacing.sm, spacing.md, spacing.lg, etc.)",
932
- "These resolve to CSS variables automatically"
933
- );
934
- }
935
-
936
- // Typography fontSize → TypeSets
937
- if (primitivePath.startsWith("typography.fontSize.")) {
938
- suggestions.push(
939
- "typography.typeSets.title-lg (large titles)",
940
- "typography.typeSets.title-md (medium titles)",
941
- "typography.typeSets.text-normal-regular (body text)",
942
- "typography.typeSets.label-md (form labels)"
943
- );
944
- }
945
-
946
- // Radius → Use flat radius tokens (radius.sm, radius.md, etc.)
947
- if (primitivePath.startsWith("radius.")) {
948
- suggestions.push(
949
- "Use radius tokens (radius.sm, radius.md, radius.lg, radius.full, etc.)",
950
- "Describe semantic usage in AI guidance (e.g., 'Buttons use @radius.md')"
951
- );
952
- }
953
-
954
- // Shadow elevation → Use elevation keys
955
- if (primitivePath.startsWith("shadows.elevation.")) {
956
- suggestions.push(
957
- "Use elevation keys (none, sm, md, lg, xl) in component defaults",
958
- "These resolve to CSS variables automatically"
959
- );
960
- }
961
-
962
- return suggestions;
963
- }
964
-
965
- function validateValue(value: string, context: string): {
966
- valid: boolean;
967
- issue?: string;
968
- suggestion?: string;
969
- matchingTokens?: Array<{ path: string; value: unknown; cssVariable: string }>;
970
- } {
971
- // Check for hex colors
972
- if (/^#[0-9A-Fa-f]{3,8}$/.test(value)) {
973
- const allTokens = flattenTokens(primitives);
974
- const matches = Object.entries(allTokens)
975
- .filter(([path, v]) => {
976
- if (typeof v !== "string") return false;
977
- return v.toLowerCase() === value.toLowerCase();
978
- })
979
- .map(([path, v]) => ({
980
- path,
981
- value: v,
982
- cssVariable: getCssVariableName(path),
983
- }));
984
-
985
- if (matches.length > 0) {
986
- return {
987
- valid: false,
988
- issue: "Hardcoded hex color detected",
989
- suggestion: `Use token instead: ${matches[0].cssVariable}`,
990
- matchingTokens: matches,
991
- };
992
- }
993
-
994
- return {
995
- valid: false,
996
- issue: "Hardcoded hex color with no matching token",
997
- suggestion: "Check colors.static, colors.scales, or colors.modes for appropriate tokens",
998
- };
999
- }
1000
-
1001
- // Check for rgb/rgba/hsl colors
1002
- if (/^(rgb|rgba|hsl|hsla)\(/.test(value)) {
1003
- return {
1004
- valid: false,
1005
- issue: "Hardcoded color function detected",
1006
- suggestion: "Use CSS variable: var(--atomix-colors-*) or primitives.colors.*",
1007
- };
1008
- }
1009
-
1010
- // Check for pixel values in spacing context
1011
- if (context === "spacing" && /^\d+px$/.test(value)) {
1012
- return {
1013
- valid: false,
1014
- issue: "Hardcoded pixel value for spacing",
1015
- suggestion: "Use spacing.* tokens (e.g., @spacing.md, @spacing.lg)",
1016
- };
1017
- }
1018
-
1019
- // Check for pixel values in any context
1020
- if (/^\d+px$/.test(value)) {
1021
- return {
1022
- valid: false,
1023
- issue: "Hardcoded pixel value detected",
1024
- suggestion: "Consider using design tokens from spacing, sizing, or typography",
1025
- };
1026
- }
1027
-
1028
- // Check for Tailwind arbitrary values
1029
- if (/\[.*\]/.test(value)) {
1030
- return {
1031
- valid: false,
1032
- issue: "Tailwind arbitrary value detected",
1033
- suggestion: "Replace with token-based class or CSS variable",
1034
- };
1035
- }
1036
-
1037
- return { valid: true };
1038
- }
1039
-
1040
- // ============================================
1041
- // START SERVER
1042
- // ============================================
1043
-
1044
- // Global primitives reference (loaded on startup)
1045
- let primitives: Record<string, unknown> = {};
1046
-
1047
- // Global governance reference (for user DS mode)
1048
- let userGovernance: { rules: string[]; categories?: Record<string, string[]> } | undefined;
1049
-
1050
- // Global DS mode flag
1051
- let isUserMode = false;
1052
- let userDsId: string | undefined;
1053
-
1054
- async function main() {
1055
- // Parse CLI arguments
1056
- const cliArgs = parseCLIArgs();
1057
-
1058
- if (isUserDSMode(cliArgs)) {
1059
- // User DS mode: fetch tokens from Atomix API
1060
- console.error(`[atomix-mcp] User DS mode: loading design system ${cliArgs.dsId}`);
1061
- isUserMode = true;
1062
- userDsId = cliArgs.dsId;
1063
-
1064
- const result = await fetchUserDesignSystem({
1065
- dsId: cliArgs.dsId!,
1066
- apiKey: cliArgs.apiKey,
1067
- baseUrl: cliArgs.baseUrl,
1068
- });
1069
-
1070
- if (!result.success || !result.tokens) {
1071
- console.error(`[atomix-mcp] Failed to load user DS: ${result.error}`);
1072
- console.error("[atomix-mcp] Falling back to Atomix internal tokens");
1073
-
1074
- // Fallback to internal tokens
1075
- primitives = await loadPrimitives();
1076
- } else {
1077
- // Transform user tokens to MCP-compatible format
1078
- primitives = transformUserTokens(result.tokens);
1079
- userGovernance = result.governance;
1080
-
1081
- // Display startup summary
1082
- const meta = result.meta;
1083
- const dsName = meta?.name || cliArgs.dsId;
1084
- const tokenCategories = Object.keys(primitives);
1085
- const rulesCount = userGovernance?.rules?.length || 0;
1086
-
1087
- console.error("");
1088
- console.error(`[atomix-mcp] ========================================`);
1089
- console.error(`[atomix-mcp] Design System: ${dsName}`);
1090
- console.error(`[atomix-mcp] ----------------------------------------`);
1091
-
1092
- // Show publish info
1093
- if (meta?.publishedAt) {
1094
- const publishedDate = new Date(meta.publishedAt);
1095
- const timeAgo = formatTimeAgo(meta.publishedAt);
1096
- console.error(`[atomix-mcp] Published: ${timeAgo}`);
1097
- console.error(`[atomix-mcp] (${publishedDate.toLocaleString()})`);
1098
- }
1099
- if (meta?.version) {
1100
- console.error(`[atomix-mcp] Version: ${meta.version}`);
1101
- }
1102
-
1103
- console.error(`[atomix-mcp] ----------------------------------------`);
1104
- console.error(`[atomix-mcp] Tokens: ${tokenCategories.length} categories`);
1105
- console.error(`[atomix-mcp] ${tokenCategories.join(", ")}`);
1106
- console.error(`[atomix-mcp] Rules: ${rulesCount} governance rules`);
1107
- console.error(`[atomix-mcp] ========================================`);
1108
- console.error("");
1109
- }
1110
- } else {
1111
- // Standard mode: load Atomix internal primitives
1112
- console.error("[atomix-mcp] Loading Atomix internal primitives...");
1113
- primitives = await loadPrimitives();
1114
- console.error(`[atomix-mcp] Loaded ${Object.keys(primitives).length} token categories`);
1115
- }
1116
-
1117
- const transport = new StdioServerTransport();
1118
- await server.connect(transport);
1119
-
1120
- console.error("[atomix-mcp] Atomix MCP server started");
1121
- if (isUserMode) {
1122
- console.error(`[atomix-mcp] Serving design system: ${userDsId}`);
1123
- }
1124
- }
1125
-
1126
- main().catch((error) => {
1127
- console.error("[atomix-mcp] Fatal error:", error);
1128
- process.exit(1);
1129
- });
1130
-
1131
- // ============================================
1132
- // EXPORTS (for programmatic use)
1133
- // ============================================
1134
-
1135
- export {
1136
- // AI rules generator functions
1137
- generateCursorRules,
1138
- generateRulesForTool,
1139
- generateRulesForAllTools,
1140
- getSupportedAITools,
1141
- generateToolSetupGuide,
1142
- generateMCPConfig,
1143
- generateAllMCPConfigs,
1144
- getSetupInstructions,
1145
- AI_TOOLS,
1146
- // User tokens functions
1147
- parseCLIArgs,
1148
- isUserDSMode,
1149
- fetchUserDesignSystem,
1150
- transformUserTokens,
1151
- // Types
1152
- type AIToolId,
1153
- type MCPConfigToolId,
1154
- };
1155
-