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