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 +14 -1
- package/dist/cli.js +12 -7
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +821 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -110,8 +110,9 @@ setMode('dark'); // force mode
|
|
|
110
110
|
|
|
111
111
|
---
|
|
112
112
|
|
|
113
|
-
## CLI
|
|
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
|
-
|
|
27
|
-
dest
|
|
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
|
|
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 &&
|
|
96
|
-
if (command &&
|
|
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 &&
|
|
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,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
|
|
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
|
}
|