fractalstyler2 0.0.2 → 0.3.0

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.
Files changed (59) hide show
  1. package/README.md +60 -45
  2. package/dist/cli.js +54 -11
  3. package/dist/index.d.ts +32 -1
  4. package/dist/index.js +73 -4
  5. package/dist/mcp/export.d.ts +13 -0
  6. package/dist/mcp/export.js +76 -0
  7. package/dist/mcp/schemas/compile_fractals.json +20 -0
  8. package/dist/mcp/schemas/css_to_fractals.json +16 -0
  9. package/dist/mcp/schemas/generate_component.json +45 -0
  10. package/dist/mcp/schemas/get_design_tokens.json +23 -0
  11. package/dist/mcp/schemas/instructions.md +18 -0
  12. package/dist/mcp/schemas/list_fractals.json +20 -0
  13. package/dist/mcp/schemas/snap_to_tokens.json +25 -0
  14. package/dist/mcp/schemas/validate_recipe.json +16 -0
  15. package/dist/mcp/server.d.ts +7 -0
  16. package/dist/mcp/server.js +837 -0
  17. package/dist/styles/_00_tokens.sass +177 -0
  18. package/{templates/_config.sass → dist/styles/_01_config.sass} +11 -5
  19. package/dist/styles/_02_fonts.sass +13 -0
  20. package/dist/styles/_03_responsive.sass +31 -0
  21. package/dist/styles/{_atoms.sass → _04_atoms.sass} +4 -4
  22. package/{templates/_molecules.sass → dist/styles/_05_molecules.sass} +16 -12
  23. package/dist/styles/_06_recipes.sass +189 -0
  24. package/dist/styles/{_base.sass → _07_base.sass} +3 -2
  25. package/dist/styles/_08_blocks.sass +433 -0
  26. package/dist/styles/{_utilities.sass → _09_utilities.sass} +96 -16
  27. package/dist/styles/_10_layouts.sass +373 -0
  28. package/dist/styles/_11_own.sass +21 -0
  29. package/dist/styles/_fractals.sass +7 -6
  30. package/dist/styles/index.sass +14 -16
  31. package/mcp.json +11 -0
  32. package/package.json +14 -6
  33. package/plugin.json +22 -0
  34. package/skills/fractal-styler/SKILL.md +83 -0
  35. package/skills/fractal-styler/references/fractals.md +28 -0
  36. package/skills/fractal-styler/references/tokens.md +36 -0
  37. package/skills/style-migration/SKILL.md +45 -0
  38. package/templates/_00_tokens.sass +177 -0
  39. package/{dist/styles/_config.sass → templates/_01_config.sass} +11 -5
  40. package/templates/_02_fonts.sass +13 -0
  41. package/templates/_03_responsive.sass +31 -0
  42. package/templates/{_atoms.sass → _04_atoms.sass} +4 -4
  43. package/{dist/styles/_molecules.sass → templates/_05_molecules.sass} +16 -12
  44. package/templates/_06_recipes.sass +189 -0
  45. package/templates/{_base.sass → _07_base.sass} +3 -2
  46. package/templates/_08_blocks.sass +433 -0
  47. package/templates/{_utilities.sass → _09_utilities.sass} +96 -16
  48. package/templates/_10_layouts.sass +373 -0
  49. package/templates/_11_own.sass +21 -0
  50. package/templates/_fractals.sass +7 -6
  51. package/templates/index.sass +14 -16
  52. package/dist/styles/_blocks.sass +0 -88
  53. package/dist/styles/_layouts.sass +0 -108
  54. package/dist/styles/_responsive.sass +0 -43
  55. package/dist/styles/_tokens.sass +0 -127
  56. package/templates/_blocks.sass +0 -88
  57. package/templates/_layouts.sass +0 -108
  58. package/templates/_responsive.sass +0 -43
  59. package/templates/_tokens.sass +0 -127
@@ -0,0 +1,837 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fractalstyler2 MCP Server
4
+ * Model Context Protocol server exposing design tokens, SASS mixin compilation,
5
+ * token snapping, component generation, and linting for OpenDesign, Claude Desktop, Cursor, etc.
6
+ */
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
10
+ import * as sass from 'sass';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ const VERSION = '0.3.0';
15
+ // Resolve styles directory for SASS compiler loadPaths
16
+ function getStylesDir() {
17
+ const HERE = dirname(fileURLToPath(import.meta.url));
18
+ const candidates = [
19
+ join(HERE, '..', 'styles'),
20
+ join(HERE, '..', '..', 'templates'),
21
+ join(HERE, 'styles'),
22
+ join(process.cwd(), 'src', 'lib', 'styles'),
23
+ join(process.cwd(), 'templates')
24
+ ];
25
+ for (const candidate of candidates) {
26
+ if (existsSync(candidate) && existsSync(join(candidate, '_00_tokens.sass'))) {
27
+ return candidate;
28
+ }
29
+ }
30
+ // Fallback to current working directory
31
+ return process.cwd();
32
+ }
33
+ // Design Token Catalog
34
+ const DESIGN_TOKENS = {
35
+ version: VERSION,
36
+ breakpoints: {
37
+ sm: '640px',
38
+ md: '768px',
39
+ lg: '1024px',
40
+ xl: '1240px'
41
+ },
42
+ space: {
43
+ scale: ['3xs', '2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', '3xl', 's-l'],
44
+ clamp: {
45
+ '3xs': 'clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem)', // ~5px
46
+ '2xs': 'clamp(0.5625rem, 0.5369rem + 0.1136vw, 0.625rem)', // ~9-10px
47
+ xs: 'clamp(0.875rem, 0.8494rem + 0.1136vw, 0.9375rem)', // ~14-15px
48
+ s: 'clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem)', // ~18-20px
49
+ m: 'clamp(1.6875rem, 1.6108rem + 0.3409vw, 1.875rem)', // ~27-30px
50
+ l: 'clamp(2.25rem, 2.1477rem + 0.4545vw, 2.5rem)', // ~36-40px
51
+ xl: 'clamp(3.375rem, 3.2216rem + 0.6818vw, 3.75rem)', // ~54-60px
52
+ '2xl': 'clamp(4.5rem, 4.2955rem + 0.9091vw, 5rem)', // ~72-80px
53
+ '3xl': 'clamp(6.75rem, 6.4432rem + 1.3636vw, 7.5rem)', // ~108-120px
54
+ 's-l': 'clamp(1.125rem, 0.5625rem + 2.5vw, 2.5rem)' // fluid s to l
55
+ },
56
+ approxPx: {
57
+ '3xs': 5,
58
+ '2xs': 9,
59
+ xs: 14,
60
+ s: 18,
61
+ m: 27,
62
+ l: 36,
63
+ xl: 54,
64
+ '2xl': 72,
65
+ '3xl': 108
66
+ }
67
+ },
68
+ typography: {
69
+ scale: ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl'],
70
+ clamp: {
71
+ xs: '0.75rem',
72
+ sm: 'clamp(0.9375rem, 0.9119rem + 0.1136vw, 1rem)',
73
+ md: 'clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem)',
74
+ lg: 'clamp(1.35rem, 1.2631rem + 0.3864vw, 1.5625rem)',
75
+ xl: 'clamp(1.62rem, 1.4837rem + 0.6057vw, 1.9531rem)',
76
+ '2xl': 'clamp(1.944rem, 1.7405rem + 0.9044vw, 2.4414rem)',
77
+ '3xl': 'clamp(2.3328rem, 2.0387rem + 1.3072vw, 3.0518rem)',
78
+ '4xl': 'clamp(2.7994rem, 2.384rem + 1.8461vw, 3.8147rem)'
79
+ },
80
+ approxPx: {
81
+ xs: 12,
82
+ sm: 15,
83
+ md: 18,
84
+ lg: 22,
85
+ xl: 26,
86
+ '2xl': 31,
87
+ '3xl': 38,
88
+ '4xl': 45
89
+ },
90
+ fonts: {
91
+ sans: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
92
+ mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
93
+ }
94
+ },
95
+ radius: {
96
+ steps: ['0', '2', '3', '4', '6', '8', '12', '16', '24', 'full'],
97
+ values: {
98
+ '0': '0px',
99
+ '2': '2px',
100
+ '3': '3px',
101
+ '4': '4px',
102
+ '6': '6px',
103
+ '8': '8px',
104
+ '12': '12px',
105
+ '16': '16px',
106
+ '24': '24px',
107
+ full: '9999px'
108
+ }
109
+ },
110
+ shadows: {
111
+ sm: '0 1px 2px rgba(15, 23, 42, 0.06)',
112
+ md: '0 4px 12px rgba(15, 23, 42, 0.08)',
113
+ lg: '0 12px 32px rgba(15, 23, 42, 0.12)'
114
+ },
115
+ surfaces: {
116
+ bg: 'var(--bg)',
117
+ surface: 'var(--bg-surface)',
118
+ raised: 'var(--bg-raised)',
119
+ panel: 'var(--bg-panel)',
120
+ footer: 'var(--bg-footer)',
121
+ popover: 'var(--bg-popover)',
122
+ dialog: 'var(--bg-dialog)',
123
+ terminal: 'var(--bg-terminal)',
124
+ input: 'var(--bg-input)',
125
+ canvas: 'var(--bg-canvas)'
126
+ },
127
+ ink: {
128
+ primary: 'var(--text-primary)',
129
+ secondary: 'var(--text-secondary)',
130
+ muted: 'var(--text-muted)',
131
+ inverse: 'var(--text-inverse)',
132
+ themeColor: 'var(--theme-color)',
133
+ themeColorAlt: 'var(--theme-color-alt)'
134
+ },
135
+ brand: {
136
+ theme: 'var(--theme)',
137
+ themeHover: 'var(--theme-hover)',
138
+ themeActive: 'var(--theme-active)',
139
+ ring: 'var(--ring)'
140
+ },
141
+ layering: {
142
+ '--z-base': 0,
143
+ '--z-raised': 10,
144
+ '--z-sticky': 100,
145
+ '--z-modal': 200,
146
+ '--z-toast': 300
147
+ }
148
+ };
149
+ // Catalog of Fractals (Mixins)
150
+ const FRACTAL_CATALOG = {
151
+ atoms: [
152
+ { name: 'box', signature: '+box($x: null, $y: null)', description: 'Flex column layout with optional cross/main axis alignment.' },
153
+ { name: 'row', signature: '+row($x: null, $y: null)', description: 'Flex row layout with optional main/cross axis alignment.' },
154
+ { name: 'wrap', signature: '+wrap', description: 'Enables flex-wrap: wrap on container.' },
155
+ { name: 'grid', signature: '+grid($cols: 1)', description: 'CSS grid with fixed column count repeat($cols, minmax(0, 1fr)).' },
156
+ { name: 'auto-grid', signature: '+auto-grid($min: 15rem, $gap: s)', description: 'Intrinsic auto-fit CSS grid without breakpoints.' },
157
+ { name: 'center', signature: '+center', description: 'Dead-center anything using display: grid; place-items: center.' },
158
+ { name: 'gap', signature: '+gap($v: s)', description: 'Applies gap from space token or raw px value.' },
159
+ { name: 'pad', signature: '+pad($v: s)', description: 'Applies padding on all sides from space token or raw px.' },
160
+ { name: 'px', signature: '+px($v: s)', description: 'Applies inline padding (left/right) from space token or raw px.' },
161
+ { name: 'py', signature: '+py($v: s)', description: 'Applies block padding (top/bottom) from space token or raw px.' },
162
+ { name: 'mx-auto', signature: '+mx-auto', description: 'Sets margin-inline: auto for horizontal centering.' },
163
+ { name: 'my-auto', signature: '+my-auto', description: 'Sets margin-block: auto for vertical centering.' },
164
+ { name: 'w', signature: '+w($v: 100%)', description: 'Sets width.' },
165
+ { name: 'h', signature: '+h($v: 100%)', description: 'Sets height.' },
166
+ { name: 'full', signature: '+full', description: 'Sets width: 100% and height: 100%.' },
167
+ { name: 'square', signature: '+square($v)', description: 'Sets equal width and height.' },
168
+ { name: 'grow', signature: '+grow($n: 1)', description: 'Sets flex-grow.' },
169
+ { name: 'shrink', signature: '+shrink($n: 0)', description: 'Sets flex-shrink.' },
170
+ { name: 'min0', signature: '+min0', description: 'Sets min-width: 0 and min-height: 0 to prevent overflow.' },
171
+ { name: 'bg', signature: '+bg($role: surface)', description: 'Sets background-color to any of the 21 surface tokens.' },
172
+ { name: 'ink', signature: '+ink($role: primary)', description: 'Sets text color to primary, secondary, muted, inverse, theme-color, theme-color-alt.' },
173
+ { name: 'border', signature: '+border($side: all, $color: var(--border))', description: 'Applies 1px solid border on all sides or a specific side.' },
174
+ { name: 'radius', signature: '+radius($v: 6)', description: 'Applies border-radius from radius token or raw px.' },
175
+ { name: 'shadow', signature: '+shadow($v: md)', description: 'Applies box-shadow from shadow scale (sm, md, lg).' },
176
+ { name: 'type', signature: '+type($v)', description: 'Applies font-size from fluid type scale (xs..4xl).' },
177
+ { name: 'weight', signature: '+weight($w: 500)', description: 'Sets font-weight.' },
178
+ { name: 'leading', signature: '+leading($lh: 1.5)', description: 'Sets line-height.' },
179
+ { name: 'truncate', signature: '+truncate', description: 'Single-line text truncation with ellipsis.' },
180
+ { name: 'clamp-lines', signature: '+clamp-lines($n: 2)', description: 'Multi-line clamp using -webkit-line-clamp.' },
181
+ { name: 'transition', signature: '+transition($props: all, $dur: 150ms, $ease: ease)', description: 'Smooth CSS transition.' },
182
+ { name: 'ring', signature: '+ring($color: var(--ring))', description: 'Focus outline ring with 1px offset.' }
183
+ ],
184
+ molecules: [
185
+ { name: 'stack', signature: '+stack($gap: xs, $x: null)', description: 'Vertical rhythm: flex column + gap.' },
186
+ { name: 'cluster', signature: '+cluster($gap: xs, $x: start, $y: center)', description: 'Wrapping row for tags/buttons/chips.' },
187
+ { name: 'center-column', signature: '+center-column($max: var(--measure, 60ch), $pad: s)', description: 'Bounded reading column with max-width measure.' },
188
+ { name: 'cover', signature: '+cover($min: 100vh, $pad: s)', description: 'Full-height container with vertically centered focal child.' },
189
+ { name: 'frame', signature: '+frame($ratio: 16 / 9)', description: 'Aspect-ratio container for media (images, video, iframe).' },
190
+ { name: 'reel', signature: '+reel($gap: xs)', description: 'Horizontal scroll-snap rail.' },
191
+ { name: 'with-sidebar', signature: '+with-sidebar($rail: 240px, $gap: s, $min: 60%)', description: 'Intrinsic sidebar and fluid main content.' },
192
+ { name: 'surface', signature: '+surface($bg: surface, $pad: null, $radius: 6, $elevation: none)', description: 'All-in-one material fractal: skin, radius, pad, and elevation.' },
193
+ { name: 'cols', signature: '+cols($map, $gap: s)', description: 'Responsive column grid mapped across breakpoints, e.g. (base: 1, sm: 2, lg: 3).' }
194
+ ],
195
+ recipes: [
196
+ { name: 'card', signature: '+card($bg: surface, $pad: null, $radius: 6, $elevation: none)', description: 'Vertical card container recipe with optional pad and elevation.' },
197
+ { name: 'control', signature: '+control($size: md, $radius: 4)', description: 'Universal interactive control recipe (buttons, triggers, inputs).' },
198
+ { name: 'select', signature: '+select($size: md, $radius: 4)', description: 'Select input recipe with embedded SVG chevron.' },
199
+ { name: 'badge', signature: '+badge($radius: 4)', description: 'Compact status badge recipe.' }
200
+ ],
201
+ layouts: [
202
+ { name: 'grid-3', class: '.grid-3', description: 'Responsive 1 → 2 → 3 column reflow.' },
203
+ { name: 'card-grid', class: '.card-grid', description: 'Intrinsic auto-fit grid for cards.' },
204
+ { name: 'hero', class: '.hero', description: 'Full viewport cover with centered hero message.' },
205
+ { name: 'holy-grail', class: '.holy-grail', description: 'Responsive header / (nav · main · aside) / footer.' },
206
+ { name: 'docs', class: '.docs', description: 'Docs template with sidebar nav, center reading column, and right TOC.' },
207
+ { name: 'app-shell', class: '.app-shell', description: 'Sticky header, fluid body, and footer application frame.' }
208
+ ]
209
+ };
210
+ // Guidelines & Golden Rules
211
+ const GUIDELINES = `
212
+ # fractalstyler2 Design System Rules
213
+
214
+ 1. Never hardcode a value that a token covers (+gap(m), +radius(6), +bg(surface)).
215
+ 2. Compose fractals (+surface, +stack, +cluster, +card); write raw CSS only for genuinely unique lines.
216
+ 3. Express component state on data-* / aria-* attributes, never modifier classes (e.g. &[data-elevated], &[data-variant='ghost']).
217
+ 4. Markup stays thin and semantic: prefer clean tags (<article class="card">) over utility class soup.
218
+ 5. Mobile-first: define base styles first, then grow with +at(md/lg/xl) or +cols().
219
+ 6. No legacy v1 classes (e.g. no gap8, pad16, w100, .stack as markup class).
220
+ `.trim();
221
+ // Snapping helper: find closest token
222
+ function findNearestToken(val, scaleMap) {
223
+ let bestToken = Object.keys(scaleMap)[0];
224
+ let bestDiff = Math.abs(val - scaleMap[bestToken]);
225
+ for (const [token, px] of Object.entries(scaleMap)) {
226
+ const diff = Math.abs(val - px);
227
+ if (diff < bestDiff) {
228
+ bestDiff = diff;
229
+ bestToken = token;
230
+ }
231
+ }
232
+ return { token: bestToken, approxPx: scaleMap[bestToken], diff: bestDiff };
233
+ }
234
+ // Create MCP Server
235
+ const server = new Server({
236
+ name: 'fractalstyler2',
237
+ version: VERSION
238
+ }, {
239
+ capabilities: {
240
+ tools: {},
241
+ resources: {},
242
+ prompts: {}
243
+ }
244
+ });
245
+ // -----------------------------------------------------------------------------
246
+ // LIST TOOLS
247
+ // -----------------------------------------------------------------------------
248
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
249
+ return {
250
+ tools: [
251
+ {
252
+ name: 'compile_fractals',
253
+ description: 'Compiles indented SASS fractal mixins into CSS. Useful for live preview in OpenDesign or web apps.',
254
+ inputSchema: {
255
+ type: 'object',
256
+ properties: {
257
+ sassCode: {
258
+ type: 'string',
259
+ description: 'Indented SASS code. Can use any fractal mixin (+surface, +stack, +gap, etc.).'
260
+ },
261
+ className: {
262
+ type: 'string',
263
+ description: 'Optional CSS class name to wrap the mixins under (e.g. "preview-card"). Defaults to "element".'
264
+ }
265
+ },
266
+ required: ['sassCode']
267
+ }
268
+ },
269
+ {
270
+ name: 'get_design_tokens',
271
+ description: 'Returns the complete structured JSON design tokens (space, typography, radius, shadows, colors, breakpoints).',
272
+ inputSchema: {
273
+ type: 'object',
274
+ properties: {
275
+ category: {
276
+ type: 'string',
277
+ enum: ['all', 'space', 'typography', 'radius', 'shadows', 'surfaces', 'ink', 'breakpoints'],
278
+ description: 'Optional category filter. Defaults to "all".'
279
+ }
280
+ }
281
+ }
282
+ },
283
+ {
284
+ name: 'snap_to_tokens',
285
+ description: 'Takes raw pixel values (e.g. from canvas elements in OpenDesign) and snaps them to the nearest fractalstyler2 design tokens.',
286
+ inputSchema: {
287
+ type: 'object',
288
+ properties: {
289
+ gap: { type: 'number', description: 'Gap in pixels (e.g. 16)' },
290
+ padding: { type: 'number', description: 'Padding in pixels (e.g. 24)' },
291
+ radius: { type: 'number', description: 'Border radius in pixels (e.g. 10)' },
292
+ fontSize: { type: 'number', description: 'Font size in pixels (e.g. 18)' }
293
+ }
294
+ }
295
+ },
296
+ {
297
+ name: 'css_to_fractals',
298
+ description: 'Converts raw CSS declarations (from Figma/OpenDesign inspection) into idiomatic fractalstyler2 SASS mixins.',
299
+ inputSchema: {
300
+ type: 'object',
301
+ properties: {
302
+ css: {
303
+ type: 'string',
304
+ description: 'Raw CSS block or declaration lines (e.g. "display: flex; flex-direction: column; gap: 16px; padding: 20px; border-radius: 12px; background: #ffffff;")'
305
+ }
306
+ },
307
+ required: ['css']
308
+ }
309
+ },
310
+ {
311
+ name: 'generate_component',
312
+ description: 'Generates a production-ready Svelte 5 component with runes and scoped SASS fractal mixins.',
313
+ inputSchema: {
314
+ type: 'object',
315
+ properties: {
316
+ name: { type: 'string', description: 'Component name (e.g. PricingCard, UserAvatar, HeroBanner)' },
317
+ type: {
318
+ type: 'string',
319
+ enum: ['card', 'panel', 'button', 'badge', 'modal', 'hero', 'nav', 'custom'],
320
+ description: 'Type of component recipe.'
321
+ },
322
+ elevation: {
323
+ type: 'string',
324
+ enum: ['none', 'sm', 'md', 'lg'],
325
+ description: 'Surface elevation.'
326
+ },
327
+ description: { type: 'string', description: 'Detailed description of component purpose and props.' }
328
+ },
329
+ required: ['name', 'type']
330
+ }
331
+ },
332
+ {
333
+ name: 'validate_recipe',
334
+ description: 'Lints a SASS snippet or Svelte component against fractalstyler2 golden rules (flags legacy classes, unmapped pixels, etc.).',
335
+ inputSchema: {
336
+ type: 'object',
337
+ properties: {
338
+ code: { type: 'string', description: 'The SASS or Svelte code to validate.' }
339
+ },
340
+ required: ['code']
341
+ }
342
+ },
343
+ {
344
+ name: 'list_fractals',
345
+ description: 'Returns the catalog of all available atom & molecule mixins with their signatures and descriptions.',
346
+ inputSchema: {
347
+ type: 'object',
348
+ properties: {
349
+ tier: {
350
+ type: 'string',
351
+ enum: ['all', 'atoms', 'molecules', 'recipes', 'layouts'],
352
+ description: 'Filter by fractal tier.'
353
+ }
354
+ }
355
+ }
356
+ }
357
+ ]
358
+ };
359
+ });
360
+ // -----------------------------------------------------------------------------
361
+ // CALL TOOL
362
+ // -----------------------------------------------------------------------------
363
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
364
+ const { name, arguments: args = {} } = request.params;
365
+ switch (name) {
366
+ case 'compile_fractals': {
367
+ const rawSass = args.sassCode || '';
368
+ const className = args.className || 'element';
369
+ const stylesDir = getStylesDir();
370
+ // Indent code lines under selector
371
+ const indented = rawSass
372
+ .split('\n')
373
+ .map((line) => (line.trim() ? `\t${line}` : ''))
374
+ .join('\n');
375
+ const fullSass = `
376
+ @use 'tokens'
377
+ @use 'base'
378
+ @use 'fractals' as *
379
+
380
+ .${className}
381
+ ${indented}
382
+ `;
383
+ try {
384
+ const result = sass.compileString(fullSass, {
385
+ syntax: 'indented',
386
+ loadPaths: [stylesDir]
387
+ });
388
+ return {
389
+ content: [
390
+ {
391
+ type: 'text',
392
+ text: result.css
393
+ }
394
+ ]
395
+ };
396
+ }
397
+ catch (err) {
398
+ return {
399
+ isError: true,
400
+ content: [
401
+ {
402
+ type: 'text',
403
+ text: `SASS Compilation Error:\n${err?.message || String(err)}`
404
+ }
405
+ ]
406
+ };
407
+ }
408
+ }
409
+ case 'get_design_tokens': {
410
+ const category = args.category || 'all';
411
+ if (category === 'all') {
412
+ return {
413
+ content: [{ type: 'text', text: JSON.stringify(DESIGN_TOKENS, null, 2) }]
414
+ };
415
+ }
416
+ const filtered = DESIGN_TOKENS[category] || null;
417
+ return {
418
+ content: [{ type: 'text', text: JSON.stringify({ [category]: filtered }, null, 2) }]
419
+ };
420
+ }
421
+ case 'snap_to_tokens': {
422
+ const results = {};
423
+ if (typeof args.gap === 'number') {
424
+ const match = findNearestToken(args.gap, DESIGN_TOKENS.space.approxPx);
425
+ results.gap = {
426
+ inputPx: args.gap,
427
+ nearestToken: match.token,
428
+ cssVar: `var(--space-${match.token})`,
429
+ suggestedMixin: `+gap(${match.token})`,
430
+ utilityClass: `.gap-${match.token}`
431
+ };
432
+ }
433
+ if (typeof args.padding === 'number') {
434
+ const match = findNearestToken(args.padding, DESIGN_TOKENS.space.approxPx);
435
+ results.padding = {
436
+ inputPx: args.padding,
437
+ nearestToken: match.token,
438
+ cssVar: `var(--space-${match.token})`,
439
+ suggestedMixin: `+pad(${match.token})`,
440
+ utilityClass: `.pad-${match.token}`
441
+ };
442
+ }
443
+ if (typeof args.radius === 'number') {
444
+ const radiusPxMap = { '0': 0, '2': 2, '4': 4, '6': 6, '8': 8, '12': 12, '16': 16, '24': 24, full: 9999 };
445
+ const match = findNearestToken(args.radius, radiusPxMap);
446
+ results.radius = {
447
+ inputPx: args.radius,
448
+ nearestToken: match.token,
449
+ cssVar: `var(--radius-${match.token})`,
450
+ suggestedMixin: `+radius(${match.token})`,
451
+ utilityClass: `.radius-${match.token}`
452
+ };
453
+ }
454
+ if (typeof args.fontSize === 'number') {
455
+ const match = findNearestToken(args.fontSize, DESIGN_TOKENS.typography.approxPx);
456
+ results.fontSize = {
457
+ inputPx: args.fontSize,
458
+ nearestToken: match.token,
459
+ cssVar: `var(--text-${match.token})`,
460
+ suggestedMixin: `+type(${match.token})`,
461
+ utilityClass: `.text-${match.token}`
462
+ };
463
+ }
464
+ return {
465
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
466
+ };
467
+ }
468
+ case 'css_to_fractals': {
469
+ const rawCss = args.css || '';
470
+ const lines = rawCss.split(/[;\n]/).map((l) => l.trim()).filter(Boolean);
471
+ const mixins = [];
472
+ let hasFlexCol = false;
473
+ let hasFlexRow = false;
474
+ let gapVal = null;
475
+ let padVal = null;
476
+ let radiusVal = null;
477
+ let bgVal = null;
478
+ let elevationVal = null;
479
+ for (const line of lines) {
480
+ const [prop, val] = line.split(':').map((s) => s?.trim());
481
+ if (!prop || !val)
482
+ continue;
483
+ if (prop === 'display' && val === 'flex') {
484
+ // wait for flex-direction
485
+ }
486
+ else if (prop === 'flex-direction' && val === 'column') {
487
+ hasFlexCol = true;
488
+ }
489
+ else if (prop === 'flex-direction' && val === 'row') {
490
+ hasFlexRow = true;
491
+ }
492
+ else if (prop === 'gap') {
493
+ const num = parseInt(val, 10);
494
+ if (!isNaN(num)) {
495
+ gapVal = findNearestToken(num, DESIGN_TOKENS.space.approxPx).token;
496
+ }
497
+ }
498
+ else if (prop === 'padding') {
499
+ const num = parseInt(val, 10);
500
+ if (!isNaN(num)) {
501
+ padVal = findNearestToken(num, DESIGN_TOKENS.space.approxPx).token;
502
+ }
503
+ }
504
+ else if (prop === 'border-radius') {
505
+ const num = parseInt(val, 10);
506
+ if (!isNaN(num)) {
507
+ radiusVal = String(num);
508
+ }
509
+ }
510
+ else if (prop === 'background-color' || prop === 'background') {
511
+ if (val.includes('raised'))
512
+ bgVal = 'raised';
513
+ else if (val.includes('surface') || val === '#ffffff' || val === '#fff')
514
+ bgVal = 'surface';
515
+ else
516
+ bgVal = 'bg';
517
+ }
518
+ else if (prop === 'box-shadow') {
519
+ elevationVal = 'md';
520
+ }
521
+ }
522
+ // If surface combination
523
+ if (bgVal || padVal || radiusVal) {
524
+ mixins.push(`+surface(${bgVal || 'surface'}, ${padVal || 's'}, ${radiusVal || '12'}${elevationVal ? `, ${elevationVal}` : ''})`);
525
+ }
526
+ if (hasFlexCol) {
527
+ mixins.push(`+stack(${gapVal || 's'})`);
528
+ }
529
+ else if (hasFlexRow) {
530
+ mixins.push(`+cluster(${gapVal || 'xs'})`);
531
+ }
532
+ else if (gapVal && !hasFlexCol && !hasFlexRow) {
533
+ mixins.push(`+gap(${gapVal})`);
534
+ }
535
+ const output = mixins.length > 0 ? mixins.join('\n') : '// No direct fractal match; use atoms:\n+box\n+gap(s)';
536
+ return {
537
+ content: [
538
+ {
539
+ type: 'text',
540
+ text: `Suggested fractalstyler2 recipe:\n\n${output}`
541
+ }
542
+ ]
543
+ };
544
+ }
545
+ case 'generate_component': {
546
+ const compName = args.name || 'CustomCard';
547
+ const type = args.type || 'card';
548
+ const elevation = args.elevation || 'none';
549
+ const desc = args.description || '';
550
+ let template = '';
551
+ let sassBlock = '';
552
+ switch (type) {
553
+ case 'card':
554
+ template = `<script lang="ts">
555
+ let { title = 'Card Title', description = '${desc || 'Card summary text'}', children } = $props();
556
+ </script>
557
+
558
+ <article class="card"${elevation !== 'none' ? ' data-elevated' : ''}>
559
+ <div class="row ycenter xbetween">
560
+ <h3 class="text-lg">{title}</h3>
561
+ <span class="badge">Active</span>
562
+ </div>
563
+ <p class="body muted">{description}</p>
564
+ {#if children}
565
+ {@render children()}
566
+ {/if}
567
+ </article>
568
+
569
+ <style lang="sass">
570
+ @use '$lib/styles/fractals' as *
571
+ </style>`;
572
+ break;
573
+ case 'panel':
574
+ template = `<script lang="ts">
575
+ let { heading = 'Panel Heading', children } = $props();
576
+ </script>
577
+
578
+ <section class="panel">
579
+ <header class="row ycenter xbetween">
580
+ <h2 class="text-xl">{heading}</h2>
581
+ </header>
582
+ <div class="box gap-s">
583
+ {#if children}
584
+ {@render children()}
585
+ {/if}
586
+ </div>
587
+ </section>
588
+
589
+ <style lang="sass">
590
+ @use '$lib/styles/fractals' as *
591
+ </style>`;
592
+ break;
593
+ case 'button':
594
+ template = `<script lang="ts">
595
+ let { variant = 'primary', disabled = false, onclick, children } = $props();
596
+ </script>
597
+
598
+ <button class="button" data-variant={variant} {disabled} {onclick}>
599
+ {#if children}
600
+ {@render children()}
601
+ {:else}
602
+ Action
603
+ {/if}
604
+ </button>
605
+
606
+ <style lang="sass">
607
+ @use '$lib/styles/fractals' as *
608
+ </style>`;
609
+ break;
610
+ case 'badge':
611
+ template = `<script lang="ts">
612
+ let { label = 'Badge', variant = 'default' } = $props();
613
+ </script>
614
+
615
+ <span class="badge" data-variant={variant}>
616
+ {label}
617
+ </span>
618
+
619
+ <style lang="sass">
620
+ @use '$lib/styles/fractals' as *
621
+ </style>`;
622
+ break;
623
+ default:
624
+ template = `<script lang="ts">
625
+ let { children } = $props();
626
+ </script>
627
+
628
+ <div class="custom-container">
629
+ {#if children}
630
+ {@render children()}
631
+ {/if}
632
+ </div>
633
+
634
+ <style lang="sass">
635
+ @use '$lib/styles/fractals' as *
636
+
637
+ .custom-container
638
+ +surface(surface, m, 16)
639
+ +stack(s)
640
+ </style>`;
641
+ break;
642
+ }
643
+ return {
644
+ content: [
645
+ {
646
+ type: 'text',
647
+ text: `// Component: ${compName}.svelte\n\n${template}`
648
+ }
649
+ ]
650
+ };
651
+ }
652
+ case 'validate_recipe': {
653
+ const code = args.code || '';
654
+ const diagnostics = [];
655
+ const legacyClassPatterns = [
656
+ { regex: /\bgap\d+\b/g, name: 'gapN (e.g. gap8)', replacement: '.gap-xs / .gap-s or +gap(N)' },
657
+ { regex: /\bpad\d+\b/g, name: 'padN (e.g. pad16)', replacement: '.pad-s or +pad(N)' },
658
+ { regex: /\bw100\b/g, name: 'w100', replacement: '.wfull or +w(100%)' },
659
+ { regex: /\bh100\b/g, name: 'h100', replacement: '.hfull or +h(100%)' },
660
+ { regex: /\bmin-w-0\b/g, name: 'min-w-0', replacement: '.min0' },
661
+ { regex: /\bclass="[^"]*\bstack\b[^"]*"/g, name: 'class="stack"', replacement: '+stack() in SASS (no markup class)' },
662
+ { regex: /\bclass="[^"]*\bcluster\b[^"]*"/g, name: 'class="cluster"', replacement: '+cluster() in SASS (no markup class)' },
663
+ { regex: /\bclass="[^"]*\bappshell\b[^"]*"/g, name: 'class="appshell"', replacement: '.app-shell' }
664
+ ];
665
+ for (const p of legacyClassPatterns) {
666
+ if (p.regex.test(code)) {
667
+ diagnostics.push({
668
+ severity: 'error',
669
+ message: `Found legacy v1 pattern "${p.name}". In fractalstyler2 use: ${p.replacement}.`
670
+ });
671
+ }
672
+ }
673
+ if (/padding:\s*\d+px/i.test(code) || /gap:\s*\d+px/i.test(code)) {
674
+ diagnostics.push({
675
+ severity: 'warning',
676
+ message: 'Hardcoded px values detected in CSS. Prefer token resolvers like +gap(s) or +pad(m).'
677
+ });
678
+ }
679
+ if (code.includes('.is-active') || code.includes('--active')) {
680
+ diagnostics.push({
681
+ severity: 'warning',
682
+ message: 'State expressed as modifier class. In fractalstyler2, state should live on data-* or aria-* attributes (e.g. &[data-active]).'
683
+ });
684
+ }
685
+ if (diagnostics.length === 0) {
686
+ diagnostics.push({
687
+ severity: 'info',
688
+ message: 'Code conforms perfectly to fractalstyler2 design system rules.'
689
+ });
690
+ }
691
+ return {
692
+ content: [{ type: 'text', text: JSON.stringify({ diagnostics, valid: !diagnostics.some((d) => d.severity === 'error') }, null, 2) }]
693
+ };
694
+ }
695
+ case 'list_fractals': {
696
+ const tier = args.tier || 'all';
697
+ if (tier === 'all') {
698
+ return { content: [{ type: 'text', text: JSON.stringify(FRACTAL_CATALOG, null, 2) }] };
699
+ }
700
+ return {
701
+ content: [{ type: 'text', text: JSON.stringify({ [tier]: FRACTAL_CATALOG[tier] || [] }, null, 2) }]
702
+ };
703
+ }
704
+ default:
705
+ throw new Error(`Unknown tool: ${name}`);
706
+ }
707
+ });
708
+ // -----------------------------------------------------------------------------
709
+ // LIST RESOURCES
710
+ // -----------------------------------------------------------------------------
711
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
712
+ return {
713
+ resources: [
714
+ {
715
+ uri: 'fractalstyler2://tokens',
716
+ name: 'Design Tokens',
717
+ description: 'Live JSON map of all Utopia space scales, fluid typography, radii, shadows, and color roles.',
718
+ mimeType: 'application/json'
719
+ },
720
+ {
721
+ uri: 'fractalstyler2://fractals',
722
+ name: 'Fractal Mixin Catalog',
723
+ description: 'Catalog of atom and molecule mixins, signatures, and descriptions.',
724
+ mimeType: 'application/json'
725
+ },
726
+ {
727
+ uri: 'fractalstyler2://guidelines',
728
+ name: 'Design System Guidelines',
729
+ description: 'Golden rules for AI assistants generating UI with fractalstyler2.',
730
+ mimeType: 'text/markdown'
731
+ }
732
+ ]
733
+ };
734
+ });
735
+ // -----------------------------------------------------------------------------
736
+ // READ RESOURCE
737
+ // -----------------------------------------------------------------------------
738
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
739
+ const uri = request.params.uri;
740
+ if (uri === 'fractalstyler2://tokens') {
741
+ return {
742
+ contents: [
743
+ {
744
+ uri,
745
+ mimeType: 'application/json',
746
+ text: JSON.stringify(DESIGN_TOKENS, null, 2)
747
+ }
748
+ ]
749
+ };
750
+ }
751
+ if (uri === 'fractalstyler2://fractals') {
752
+ return {
753
+ contents: [
754
+ {
755
+ uri,
756
+ mimeType: 'application/json',
757
+ text: JSON.stringify(FRACTAL_CATALOG, null, 2)
758
+ }
759
+ ]
760
+ };
761
+ }
762
+ if (uri === 'fractalstyler2://guidelines') {
763
+ return {
764
+ contents: [
765
+ {
766
+ uri,
767
+ mimeType: 'text/markdown',
768
+ text: GUIDELINES
769
+ }
770
+ ]
771
+ };
772
+ }
773
+ throw new Error(`Resource not found: ${uri}`);
774
+ });
775
+ // -----------------------------------------------------------------------------
776
+ // LIST PROMPTS
777
+ // -----------------------------------------------------------------------------
778
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
779
+ return {
780
+ prompts: [
781
+ {
782
+ name: 'design_system_review',
783
+ description: 'Audit and refactor a component or screen to follow fractalstyler2 SASS mixin rules.'
784
+ },
785
+ {
786
+ name: 'generate_ui',
787
+ description: 'Generate a complete responsive UI page or component using fractalstyler2 and Svelte 5.'
788
+ }
789
+ ]
790
+ };
791
+ });
792
+ // -----------------------------------------------------------------------------
793
+ // GET PROMPT
794
+ // -----------------------------------------------------------------------------
795
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
796
+ const { name } = request.params;
797
+ if (name === 'design_system_review') {
798
+ return {
799
+ description: 'Refactor UI code to fractalstyler2',
800
+ messages: [
801
+ {
802
+ role: 'user',
803
+ content: {
804
+ type: 'text',
805
+ text: `Please review the following code and refactor it into idiomatic fractalstyler2 Svelte 5 + indented SASS (.sass):\n\n${GUIDELINES}`
806
+ }
807
+ }
808
+ ]
809
+ };
810
+ }
811
+ if (name === 'generate_ui') {
812
+ return {
813
+ description: 'Generate responsive UI with fractalstyler2',
814
+ messages: [
815
+ {
816
+ role: 'user',
817
+ content: {
818
+ type: 'text',
819
+ text: `Generate a responsive UI component using Svelte 5 runes ($props, $state) and scoped indented SASS with fractal mixins (+surface, +stack, +cluster, +cols). Adhere to the golden rules:\n\n${GUIDELINES}`
820
+ }
821
+ }
822
+ ]
823
+ };
824
+ }
825
+ throw new Error(`Prompt not found: ${name}`);
826
+ });
827
+ // -----------------------------------------------------------------------------
828
+ // START SERVER
829
+ // -----------------------------------------------------------------------------
830
+ async function main() {
831
+ const transport = new StdioServerTransport();
832
+ await server.connect(transport);
833
+ }
834
+ main().catch((error) => {
835
+ console.error('Fatal MCP server error:', error);
836
+ process.exit(1);
837
+ });