@csszyx/mcp-server 0.9.9
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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +785 -0
- package/llms-full.txt +3701 -0
- package/package.json +58 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { SPECIAL_VARIANTS, KNOWN_VARIANTS, PROPERTY_MAP, transform, SUGGESTION_MAP, BOOLEAN_SHORTHANDS } from '@csszyx/compiler';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { migrateSource, classNameToSzObject } from '@csszyx/cli';
|
|
12
|
+
import { parseThemeBlocks, hasTokens } from '@csszyx/unplugin';
|
|
13
|
+
|
|
14
|
+
function listPrompts() {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
name: "migrate_component",
|
|
18
|
+
description: "Migrate a Tailwind CSS component to use csszyx sz props. Paste your component code and get a transformed version with sz syntax.",
|
|
19
|
+
arguments: [
|
|
20
|
+
{
|
|
21
|
+
name: "code",
|
|
22
|
+
description: "The JSX/TSX component code to migrate from className to sz props",
|
|
23
|
+
required: true
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "create_component",
|
|
29
|
+
description: "Create a UI component using csszyx sz props. Describe the component you want and get production-ready code with proper sz syntax.",
|
|
30
|
+
arguments: [
|
|
31
|
+
{
|
|
32
|
+
name: "description",
|
|
33
|
+
description: 'Description of the component to create (e.g., "a responsive card with hover effects")',
|
|
34
|
+
required: true
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
function getPrompt(name, args) {
|
|
41
|
+
switch (name) {
|
|
42
|
+
case "migrate_component":
|
|
43
|
+
return {
|
|
44
|
+
messages: [
|
|
45
|
+
{
|
|
46
|
+
role: "user",
|
|
47
|
+
content: {
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `I want to migrate this Tailwind CSS component to use csszyx sz props.
|
|
50
|
+
|
|
51
|
+
Here is the component code:
|
|
52
|
+
|
|
53
|
+
\`\`\`tsx
|
|
54
|
+
${args.code ?? "// paste your component here"}
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
Please:
|
|
58
|
+
1. Use the csszyx_migrate tool to transform the className attributes to sz props
|
|
59
|
+
2. Review the result for any unrecognized classes
|
|
60
|
+
3. Explain any changes and suggest improvements
|
|
61
|
+
4. If there are dynamic className patterns (clsx, ternary, template literals), explain how sz handles them
|
|
62
|
+
|
|
63
|
+
Key csszyx rules:
|
|
64
|
+
- Numbers are bare: { p: 4 } \u2192 p-4
|
|
65
|
+
- Colors are strings: { bg: 'blue-500' } \u2192 bg-blue-500
|
|
66
|
+
- Color + opacity is an object: { bg: { color: 'blue-500', op: 50 } } \u2192 bg-blue-500/50
|
|
67
|
+
- Variants nest: { hover: { bg: 'blue-700' } } \u2192 hover:bg-blue-700
|
|
68
|
+
- Boolean sugar: { flex: true } \u2192 flex
|
|
69
|
+
- Fractions stay native: { w: '1/2' } \u2192 w-1/2
|
|
70
|
+
- Arbitrary values: { p: '5px' } \u2192 p-[5px]
|
|
71
|
+
- CSS variables: { p: '--my-var' } \u2192 p-(--my-var)
|
|
72
|
+
- Scope variants: { group: { hover: { bg: 'blue-700' } } } \u2192 group-hover:bg-blue-700 (also peer)
|
|
73
|
+
- Conditional/feature variants: { has: { checked: { ... } } } \u2192 has-[:checked]:\u2026, { supports: { 'display:grid': { ... } } } \u2192 supports-[display:grid]:\u2026 (also not/data/aria)`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
};
|
|
78
|
+
case "create_component":
|
|
79
|
+
return {
|
|
80
|
+
messages: [
|
|
81
|
+
{
|
|
82
|
+
role: "user",
|
|
83
|
+
content: {
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `I want to create a React component using csszyx sz props.
|
|
86
|
+
|
|
87
|
+
Component description: ${args.description ?? "a responsive card component"}
|
|
88
|
+
|
|
89
|
+
Please:
|
|
90
|
+
1. Generate a complete React component using sz props (NOT className/Tailwind)
|
|
91
|
+
2. Use the csszyx_lookup tool to verify prop names for any CSS properties
|
|
92
|
+
3. Include responsive breakpoints using nested variants: { md: { ... } }
|
|
93
|
+
4. Include hover/focus states using nested variants: { hover: { ... } }
|
|
94
|
+
5. Include dark mode support: { dark: { ... } }
|
|
95
|
+
6. Use the csszyx_expand tool to verify the generated sz objects
|
|
96
|
+
|
|
97
|
+
Key csszyx syntax:
|
|
98
|
+
- <div sz={{ p: 4, bg: 'blue-500', hover: { bg: 'blue-700' } }} />
|
|
99
|
+
- <div sz={{ flex: true, items: 'center', gap: 4 }} />
|
|
100
|
+
- <div sz={{ md: { gridCols: 3 }, gridCols: 1, gap: 6 }} />
|
|
101
|
+
- <div sz={{ dark: { bg: 'gray-900', color: 'white' } }} />
|
|
102
|
+
- <div sz={{ bg: { color: 'blue-500', op: 50 }, w: '1/2' }} /> (color opacity + fraction)
|
|
103
|
+
- <div sz={{ group: { hover: { bg: 'blue-700' } } }} /> (group/peer scope variants)`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const packageRoot = path.resolve(fileURLToPath(import.meta.url), "../../..");
|
|
114
|
+
const LLMS_FULL_PATH = path.join(packageRoot, "llms-full.txt");
|
|
115
|
+
const SETUP_GUIDE = `# csszyx setup
|
|
116
|
+
|
|
117
|
+
Fastest path: run \`csszyx init\` (it does everything below). Manual steps:
|
|
118
|
+
|
|
119
|
+
## 1. Install
|
|
120
|
+
\`\`\`bash
|
|
121
|
+
pnpm add csszyx @csszyx/runtime
|
|
122
|
+
pnpm add -D @csszyx/types @csszyx/cli tailwindcss
|
|
123
|
+
\`\`\`
|
|
124
|
+
\`@csszyx/runtime\` and \`@csszyx/types\` MUST be DIRECT dependencies: the
|
|
125
|
+
transform injects a bare \`import { _szMerge } from '@csszyx/runtime'\`, and
|
|
126
|
+
\`@csszyx/types\` provides the \`sz\` JSX types. Strict package managers (pnpm) do
|
|
127
|
+
not resolve them transitively.
|
|
128
|
+
|
|
129
|
+
## 2. Bundler config
|
|
130
|
+
### Vite
|
|
131
|
+
\`\`\`ts
|
|
132
|
+
import csszyx from 'csszyx/vite';
|
|
133
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
134
|
+
export default defineConfig({ plugins: [...csszyx(), tailwindcss(), react()] });
|
|
135
|
+
// Order matters: csszyx BEFORE tailwindcss BEFORE react.
|
|
136
|
+
\`\`\`
|
|
137
|
+
### Next.js \u2014 webpack (full parity, recommended)
|
|
138
|
+
\`\`\`ts
|
|
139
|
+
// next.config.ts
|
|
140
|
+
export default {
|
|
141
|
+
webpack: (config) => {
|
|
142
|
+
config.plugins.push(require('@csszyx/unplugin/webpack').default());
|
|
143
|
+
return config;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
// Run: next dev --webpack / next build --webpack
|
|
147
|
+
\`\`\`
|
|
148
|
+
### Next.js \u2014 Turbopack (experimental; no production class/CSS-var mangling)
|
|
149
|
+
\`\`\`js
|
|
150
|
+
// next.config.mjs
|
|
151
|
+
import { csszyxTurbopack } from '@csszyx/unplugin/next';
|
|
152
|
+
export default {
|
|
153
|
+
turbopack: csszyxTurbopack({}, { safelistOutputFile: '.csszyx/next-loader-classes.html' }),
|
|
154
|
+
};
|
|
155
|
+
\`\`\`
|
|
156
|
+
- Do NOT set an \`as\` field on the loader rule \u2014 the helper omits it; an \`as\`
|
|
157
|
+
makes the loader output self-match into \`./X.tsx.tsx\` (Module not found).
|
|
158
|
+
- Keep the safelist fresh: \`csszyx next watch\` (dev) and \`csszyx next prebuild\`
|
|
159
|
+
before \`next build --turbopack\`. Point Tailwind \`@source\` at the safelist file.
|
|
160
|
+
|
|
161
|
+
## 3. Tailwind v4 entry
|
|
162
|
+
\`\`\`css
|
|
163
|
+
/* src/index.css */
|
|
164
|
+
@import "tailwindcss";
|
|
165
|
+
\`\`\`
|
|
166
|
+
Import it from your app entry.
|
|
167
|
+
|
|
168
|
+
## 4. TypeScript \u2014 enable the \`sz\` prop types
|
|
169
|
+
Create \`csszyx-env.d.ts\` at the project root (kept in tsconfig \`include\`):
|
|
170
|
+
\`\`\`ts
|
|
171
|
+
/// <reference types="@csszyx/types/jsx" />
|
|
172
|
+
\`\`\`
|
|
173
|
+
Without it: \`Property 'sz' does not exist on type 'DetailedHTMLProps<...>'\`.
|
|
174
|
+
|
|
175
|
+
## Usage
|
|
176
|
+
\`\`\`tsx
|
|
177
|
+
<div sz={{ p: 4, bg: 'blue-500', hover: { bg: 'blue-700' } }} />
|
|
178
|
+
\`\`\`
|
|
179
|
+
`;
|
|
180
|
+
function listResources() {
|
|
181
|
+
return [
|
|
182
|
+
{
|
|
183
|
+
uri: "csszyx://reference",
|
|
184
|
+
name: "CSSzyx Full Reference",
|
|
185
|
+
description: "Complete sz prop API reference \u2014 all property mappings, variant syntax, and examples",
|
|
186
|
+
mimeType: "text/plain"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
uri: "csszyx://property-map",
|
|
190
|
+
name: "CSSzyx Property Map",
|
|
191
|
+
description: "JSON mapping of sz keys to Tailwind CSS utility prefixes (PROPERTY_MAP)",
|
|
192
|
+
mimeType: "application/json"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
uri: "csszyx://variants",
|
|
196
|
+
name: "CSSzyx Variant List",
|
|
197
|
+
description: "JSON with `standard` variants (hover, focus, dark, sm, md, \u2026) and `parametric` scope variants (group, peer, has, not, data, aria, supports) that take a nested target",
|
|
198
|
+
mimeType: "application/json"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
uri: "csszyx://setup",
|
|
202
|
+
name: "CSSzyx Setup Guide",
|
|
203
|
+
description: "How to install and wire csszyx into a project (Vite / Next.js webpack / Next.js Turbopack) \u2014 direct @csszyx/runtime + @csszyx/types deps, the sz JSX types (csszyx-env.d.ts), and the Turbopack no-`as` rule",
|
|
204
|
+
mimeType: "text/markdown"
|
|
205
|
+
}
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
function readResource(uri) {
|
|
209
|
+
switch (uri) {
|
|
210
|
+
case "csszyx://reference": {
|
|
211
|
+
let content = "CSSzyx Full Reference \u2014 llms-full.txt not found. Use csszyx_lookup tool instead.";
|
|
212
|
+
try {
|
|
213
|
+
content = fs.readFileSync(LLMS_FULL_PATH, "utf-8");
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
contents: [{ uri, mimeType: "text/plain", text: content }]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "csszyx://property-map":
|
|
221
|
+
return {
|
|
222
|
+
contents: [
|
|
223
|
+
{
|
|
224
|
+
uri,
|
|
225
|
+
mimeType: "application/json",
|
|
226
|
+
text: JSON.stringify(PROPERTY_MAP, null, 2)
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
case "csszyx://variants":
|
|
231
|
+
return {
|
|
232
|
+
contents: [
|
|
233
|
+
{
|
|
234
|
+
uri,
|
|
235
|
+
mimeType: "application/json",
|
|
236
|
+
text: JSON.stringify(
|
|
237
|
+
{
|
|
238
|
+
standard: [...KNOWN_VARIANTS].sort(),
|
|
239
|
+
parametric: [...SPECIAL_VARIANTS].sort()
|
|
240
|
+
},
|
|
241
|
+
null,
|
|
242
|
+
2
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
case "csszyx://setup":
|
|
248
|
+
return {
|
|
249
|
+
contents: [{ uri, mimeType: "text/markdown", text: SETUP_GUIDE }]
|
|
250
|
+
};
|
|
251
|
+
default:
|
|
252
|
+
throw new Error(`Unknown resource URI: ${uri}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const batchSchema = z.object({
|
|
257
|
+
items: z.array(z.record(z.any())).describe(
|
|
258
|
+
"Array of sz prop objects to expand. Example: [{ p: 4 }, { m: 2, bg: 'red-500' }]"
|
|
259
|
+
)
|
|
260
|
+
});
|
|
261
|
+
function handleBatch(input) {
|
|
262
|
+
const results = input.items.map((sz, index) => {
|
|
263
|
+
try {
|
|
264
|
+
const result = transform(sz);
|
|
265
|
+
return {
|
|
266
|
+
index,
|
|
267
|
+
className: result.className,
|
|
268
|
+
attributes: Object.keys(result.attributes).length > 0 ? result.attributes : void 0
|
|
269
|
+
};
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return {
|
|
272
|
+
index,
|
|
273
|
+
error: err instanceof Error ? err.message : String(err)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: JSON.stringify(
|
|
282
|
+
{
|
|
283
|
+
results,
|
|
284
|
+
totalItems: input.items.length,
|
|
285
|
+
successful: results.filter((r) => !("error" in r)).length
|
|
286
|
+
},
|
|
287
|
+
null,
|
|
288
|
+
2
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const expandSchema = z.object({
|
|
296
|
+
sz: z.record(z.any()).describe(
|
|
297
|
+
"The sz prop object to expand. Example: { p: 4, bg: 'blue-500', hover: { bg: 'blue-700' } }"
|
|
298
|
+
)
|
|
299
|
+
});
|
|
300
|
+
function handleExpand(input) {
|
|
301
|
+
const result = transform(input.sz);
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: JSON.stringify(
|
|
307
|
+
{
|
|
308
|
+
className: result.className,
|
|
309
|
+
attributes: result.attributes,
|
|
310
|
+
classCount: result.className.split(/\s+/).filter(Boolean).length
|
|
311
|
+
},
|
|
312
|
+
null,
|
|
313
|
+
2
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const SEARCH_INDEX = /* @__PURE__ */ new Map();
|
|
321
|
+
function addSearch(term, szKey) {
|
|
322
|
+
const lower = term.toLowerCase();
|
|
323
|
+
const existing = SEARCH_INDEX.get(lower) ?? [];
|
|
324
|
+
if (!existing.includes(szKey)) {
|
|
325
|
+
existing.push(szKey);
|
|
326
|
+
SEARCH_INDEX.set(lower, existing);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function toKebab(camel) {
|
|
330
|
+
return camel.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
331
|
+
}
|
|
332
|
+
for (const szKey of Object.keys(PROPERTY_MAP)) {
|
|
333
|
+
addSearch(szKey, szKey);
|
|
334
|
+
addSearch(toKebab(szKey), szKey);
|
|
335
|
+
}
|
|
336
|
+
for (const [cssAlias, szTarget] of Object.entries(SUGGESTION_MAP)) {
|
|
337
|
+
const primaryKey = szTarget.split(/[\s/(]/)[0];
|
|
338
|
+
if (primaryKey && (PROPERTY_MAP[primaryKey] !== void 0 || BOOLEAN_SHORTHANDS.has(primaryKey))) {
|
|
339
|
+
addSearch(cssAlias, primaryKey);
|
|
340
|
+
addSearch(toKebab(cssAlias), primaryKey);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const key of BOOLEAN_SHORTHANDS) {
|
|
344
|
+
addSearch(key, key);
|
|
345
|
+
addSearch(toKebab(key), key);
|
|
346
|
+
}
|
|
347
|
+
const EXAMPLES = {
|
|
348
|
+
bg: ["{ bg: 'blue-500' }", "{ bg: { color: 'blue-500', op: 50 } }"],
|
|
349
|
+
p: ["{ p: 4 }", "{ p: '5px' }"],
|
|
350
|
+
m: ["{ m: 4 }", "{ m: 'auto' }"],
|
|
351
|
+
px: ["{ px: 4 }"],
|
|
352
|
+
py: ["{ py: 2 }"],
|
|
353
|
+
mx: ["{ mx: 'auto' }"],
|
|
354
|
+
my: ["{ my: 4 }"],
|
|
355
|
+
w: ["{ w: 64 }", "{ w: 'full' }"],
|
|
356
|
+
h: ["{ h: 16 }", "{ h: 'screen' }"],
|
|
357
|
+
text: ["{ text: 'lg' }", "{ text: '2xl' }"],
|
|
358
|
+
color: ["{ color: 'white' }", "{ color: 'slate-500' }"],
|
|
359
|
+
fontWeight: ["{ fontWeight: 'bold' }", "{ fontWeight: 700 }"],
|
|
360
|
+
fontFamily: ["{ fontFamily: 'sans' }", "{ fontFamily: 'mono' }"],
|
|
361
|
+
flex: ["{ flex: true }", "{ flex: 'auto' }", "{ flex: 1 }"],
|
|
362
|
+
flexDir: ["{ flexDir: 'col' }", "{ flexDir: 'row' }"],
|
|
363
|
+
grid: ["{ grid: true }"],
|
|
364
|
+
gridCols: ["{ gridCols: 3 }", "{ gridCols: 'none' }"],
|
|
365
|
+
gap: ["{ gap: 4 }"],
|
|
366
|
+
items: ["{ items: 'center' }", "{ items: 'start' }"],
|
|
367
|
+
justify: ["{ justify: 'center' }", "{ justify: 'between' }"],
|
|
368
|
+
rounded: ["{ rounded: 'md' }", "{ rounded: 'full' }"],
|
|
369
|
+
shadow: ["{ shadow: 'md' }"],
|
|
370
|
+
border: ["{ border: true }", "{ border: 2 }"],
|
|
371
|
+
opacity: ["{ opacity: 50 }"],
|
|
372
|
+
z: ["{ z: 10 }", "{ z: 'auto' }"],
|
|
373
|
+
overflow: ["{ overflow: 'hidden' }"],
|
|
374
|
+
cursor: ["{ cursor: 'pointer' }"],
|
|
375
|
+
transition: ["{ transition: 'all' }", "{ transition: 'colors' }"],
|
|
376
|
+
duration: ["{ duration: 300 }"],
|
|
377
|
+
scale: ["{ scale: 150 }", "{ scale: 75 }"],
|
|
378
|
+
rotate: ["{ rotate: 45 }"],
|
|
379
|
+
leading: ["{ leading: 'tight' }", "{ leading: 7 }"],
|
|
380
|
+
tracking: ["{ tracking: 'wide' }"],
|
|
381
|
+
textAlign: ["{ textAlign: 'center' }"],
|
|
382
|
+
aspect: ["{ aspect: 'video' }", "{ aspect: 'square' }"],
|
|
383
|
+
blur: ["{ blur: true }", "{ blur: 'sm' }"],
|
|
384
|
+
grow: ["{ grow: true }"],
|
|
385
|
+
shrink: ["{ shrink: true }"]
|
|
386
|
+
};
|
|
387
|
+
const lookupSchema = z.object({
|
|
388
|
+
query: z.string().describe(
|
|
389
|
+
'CSS property name, sz key, or search term. Examples: "background-color", "bg", "padding", "flex-direction"'
|
|
390
|
+
)
|
|
391
|
+
});
|
|
392
|
+
function handleLookup(input) {
|
|
393
|
+
const query = input.query.trim().toLowerCase();
|
|
394
|
+
const seen = /* @__PURE__ */ new Set();
|
|
395
|
+
const results = [];
|
|
396
|
+
function pushResult(szKey) {
|
|
397
|
+
if (seen.has(szKey)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
seen.add(szKey);
|
|
401
|
+
results.push({
|
|
402
|
+
szKey,
|
|
403
|
+
tailwindPrefix: PROPERTY_MAP[szKey] ?? szKey,
|
|
404
|
+
examples: (EXAMPLES[szKey] ?? []).slice(0, 2)
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const exact = SEARCH_INDEX.get(query);
|
|
408
|
+
if (exact) {
|
|
409
|
+
exact.forEach(pushResult);
|
|
410
|
+
}
|
|
411
|
+
if (results.length === 0) {
|
|
412
|
+
for (const [term, szKeys] of SEARCH_INDEX.entries()) {
|
|
413
|
+
if (results.length >= 5) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
if (term.includes(query)) {
|
|
417
|
+
szKeys.forEach(pushResult);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: "text",
|
|
425
|
+
text: results.length > 0 ? JSON.stringify(
|
|
426
|
+
{ query: input.query, results: results.slice(0, 8) },
|
|
427
|
+
null,
|
|
428
|
+
2
|
|
429
|
+
) : JSON.stringify(
|
|
430
|
+
{
|
|
431
|
+
query: input.query,
|
|
432
|
+
results: [],
|
|
433
|
+
message: `No mapping found for "${input.query}". Try a CSS property name (e.g. "padding") or sz key (e.g. "p").`
|
|
434
|
+
},
|
|
435
|
+
null,
|
|
436
|
+
2
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const migrateSchema = z.object({
|
|
444
|
+
code: z.string().describe(
|
|
445
|
+
`JSX/TSX code snippet containing className attributes to transform. Example: '<div className="p-4 bg-blue-500" />'`
|
|
446
|
+
),
|
|
447
|
+
customMap: z.record(z.unknown()).optional().describe(
|
|
448
|
+
'Parsed .csszyx-todo.json map. Values: sz object, Tailwind string, "sz:keep", "sz:remove", "sz:todo", null, or false.'
|
|
449
|
+
),
|
|
450
|
+
injectTodos: z.boolean().optional().describe(
|
|
451
|
+
"If true, inserts {/* @sz-todo: ... */} comments above elements with unrecognized classes."
|
|
452
|
+
)
|
|
453
|
+
});
|
|
454
|
+
function handleMigrate(input) {
|
|
455
|
+
const result = migrateSource(input.code, "snippet.tsx", {
|
|
456
|
+
injectTodos: input.injectTodos,
|
|
457
|
+
customMap: input.customMap
|
|
458
|
+
});
|
|
459
|
+
return {
|
|
460
|
+
content: [
|
|
461
|
+
{
|
|
462
|
+
type: "text",
|
|
463
|
+
text: JSON.stringify(
|
|
464
|
+
{
|
|
465
|
+
code: result.code,
|
|
466
|
+
changed: result.changed,
|
|
467
|
+
stats: {
|
|
468
|
+
classNamesTransformed: result.stats.classNamesTransformed,
|
|
469
|
+
classNamesSkipped: result.stats.classNamesSkipped,
|
|
470
|
+
unrecognized: result.stats.classesUnrecognized.length > 0 ? result.stats.classesUnrecognized : void 0
|
|
471
|
+
},
|
|
472
|
+
warnings: result.warnings.length > 0 ? result.warnings : void 0,
|
|
473
|
+
potentiallyUnusedImports: result.potentiallyUnusedImports.length > 0 ? result.potentiallyUnusedImports : void 0
|
|
474
|
+
},
|
|
475
|
+
null,
|
|
476
|
+
2
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
]
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const reverseSchema = z.object({
|
|
484
|
+
classes: z.string().describe(
|
|
485
|
+
'Tailwind CSS class string to convert. Example: "p-4 bg-blue-500 hover:text-white"'
|
|
486
|
+
)
|
|
487
|
+
});
|
|
488
|
+
function handleReverse(input) {
|
|
489
|
+
const { szObject, unrecognized } = classNameToSzObject(input.classes.trim());
|
|
490
|
+
return {
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: JSON.stringify(
|
|
495
|
+
{
|
|
496
|
+
szObject,
|
|
497
|
+
unrecognized: unrecognized.length > 0 ? unrecognized : void 0,
|
|
498
|
+
totalRecognized: Object.keys(szObject).length,
|
|
499
|
+
totalUnrecognized: unrecognized.length
|
|
500
|
+
},
|
|
501
|
+
null,
|
|
502
|
+
2
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const themeSchema = z.object({
|
|
510
|
+
css: z.string().describe(
|
|
511
|
+
'CSS file content containing @theme blocks. Example: "@theme { --color-brand: #ff6b35; --font-display: \\"Cal Sans\\"; }"'
|
|
512
|
+
)
|
|
513
|
+
});
|
|
514
|
+
function handleTheme(input) {
|
|
515
|
+
const theme = parseThemeBlocks(input.css);
|
|
516
|
+
const totalVars = theme.colors.length + theme.spacings.length + theme.fonts.length + theme.radii.length + theme.shadows.length;
|
|
517
|
+
return {
|
|
518
|
+
content: [
|
|
519
|
+
{
|
|
520
|
+
type: "text",
|
|
521
|
+
text: JSON.stringify(
|
|
522
|
+
{
|
|
523
|
+
theme,
|
|
524
|
+
totalVariables: totalVars,
|
|
525
|
+
usage: hasTokens(theme) ? "Use these custom values in sz props. Example: { bg: 'brand' } for --color-brand-500" : "No @theme block found in the CSS content. Make sure your CSS contains @theme { --color-*: ...; }"
|
|
526
|
+
},
|
|
527
|
+
null,
|
|
528
|
+
2
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const validateSchema = z.object({
|
|
536
|
+
sz: z.record(z.any()).describe('The sz prop object to validate. Example: { padding: 4, bg: "blue-500" }')
|
|
537
|
+
});
|
|
538
|
+
function handleValidate(input) {
|
|
539
|
+
const errors = [];
|
|
540
|
+
const warnings = [];
|
|
541
|
+
for (const key of Object.keys(input.sz)) {
|
|
542
|
+
if (SUGGESTION_MAP[key]) {
|
|
543
|
+
errors.push({
|
|
544
|
+
key,
|
|
545
|
+
message: `Unknown prop '${key}'. This is a CSS property name, not an sz key.`,
|
|
546
|
+
suggestion: `Use '${SUGGESTION_MAP[key]}' instead. Example: { ${SUGGESTION_MAP[key].split(/[\s/(]/)[0]}: ${JSON.stringify(input.sz[key])} }`
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const isProperty = key in PROPERTY_MAP;
|
|
551
|
+
const isBoolean = BOOLEAN_SHORTHANDS.has(key);
|
|
552
|
+
const isVariant = KNOWN_VARIANTS.has(key);
|
|
553
|
+
const isSpecial = ["css", "@container", "*"].includes(key) || key.startsWith("@") || key.startsWith("[");
|
|
554
|
+
if (!isProperty && !isBoolean && !isVariant && !isSpecial) {
|
|
555
|
+
errors.push({
|
|
556
|
+
key,
|
|
557
|
+
message: `Unknown prop '${key}'. Not a valid sz key, variant, or special prop.`
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
let transformResult = null;
|
|
562
|
+
let transformError = null;
|
|
563
|
+
try {
|
|
564
|
+
transformResult = transform(input.sz);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
transformError = err instanceof Error ? err.message : String(err);
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
content: [
|
|
570
|
+
{
|
|
571
|
+
type: "text",
|
|
572
|
+
text: JSON.stringify(
|
|
573
|
+
{
|
|
574
|
+
valid: errors.length === 0 && !transformError,
|
|
575
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
576
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
577
|
+
transformResult: transformResult ? {
|
|
578
|
+
className: transformResult.className,
|
|
579
|
+
classCount: transformResult.className.split(/\s+/).filter(Boolean).length
|
|
580
|
+
} : void 0,
|
|
581
|
+
transformError: transformError ?? void 0
|
|
582
|
+
},
|
|
583
|
+
null,
|
|
584
|
+
2
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const _require = createRequire(import.meta.url);
|
|
592
|
+
const packageJson = _require(
|
|
593
|
+
path.resolve(fileURLToPath(import.meta.url), "../../package.json")
|
|
594
|
+
);
|
|
595
|
+
const VERSION = packageJson.version;
|
|
596
|
+
const server = new Server(
|
|
597
|
+
{
|
|
598
|
+
name: "csszyx-mcp-server",
|
|
599
|
+
version: VERSION
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
capabilities: {
|
|
603
|
+
tools: {},
|
|
604
|
+
resources: {},
|
|
605
|
+
prompts: {}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
const TOOLS = [
|
|
610
|
+
{
|
|
611
|
+
name: "csszyx_expand",
|
|
612
|
+
description: "Expand ONE csszyx sz object into a Tailwind CSS className string. Use this for a single component or quick lookup. For multiple objects at once, use csszyx_batch instead.",
|
|
613
|
+
inputSchema: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: {
|
|
616
|
+
sz: {
|
|
617
|
+
type: "object",
|
|
618
|
+
description: "The sz prop object. Example: { p: 4, bg: 'blue-500', hover: { bg: 'blue-700' } }"
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
required: ["sz"]
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "csszyx_reverse",
|
|
626
|
+
description: "Convert a Tailwind CSS class string into a csszyx sz object. Input: class string. Output: structured sz object + unrecognized classes.",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
type: "object",
|
|
629
|
+
properties: {
|
|
630
|
+
classes: {
|
|
631
|
+
type: "string",
|
|
632
|
+
description: 'Tailwind CSS class string. Example: "p-4 bg-blue-500 hover:text-white"'
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
required: ["classes"]
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "csszyx_validate",
|
|
640
|
+
description: "Validate a csszyx sz object for correctness. Detects unknown props, wrong types, deprecated CSS property names, and suggests fixes.",
|
|
641
|
+
inputSchema: {
|
|
642
|
+
type: "object",
|
|
643
|
+
properties: {
|
|
644
|
+
sz: {
|
|
645
|
+
type: "object",
|
|
646
|
+
description: 'The sz prop object to validate. Example: { padding: 4, bg: "blue-500" }'
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
required: ["sz"]
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "csszyx_lookup",
|
|
654
|
+
description: "Look up how a CSS property maps to csszyx sz syntax. Search by CSS property name, sz key, or keyword. Returns sz key, Tailwind prefix, and usage examples.",
|
|
655
|
+
inputSchema: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
query: {
|
|
659
|
+
type: "string",
|
|
660
|
+
description: 'CSS property name, sz key, or search term. Examples: "background-color", "bg", "padding", "flex-direction"'
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
required: ["query"]
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "csszyx_migrate",
|
|
668
|
+
description: "Transform JSX/TSX code from Tailwind className attributes to csszyx sz props. Handles static strings, clsx/cn calls, ternary expressions, and template literals. Optionally pass a customMap (parsed .csszyx-todo.json) to resolve custom class names, and injectTodos to annotate unrecognized classes.",
|
|
669
|
+
inputSchema: {
|
|
670
|
+
type: "object",
|
|
671
|
+
properties: {
|
|
672
|
+
code: {
|
|
673
|
+
type: "string",
|
|
674
|
+
description: `JSX/TSX code snippet. Example: '<div className="p-4 bg-blue-500" />'`
|
|
675
|
+
},
|
|
676
|
+
customMap: {
|
|
677
|
+
type: "object",
|
|
678
|
+
description: 'Parsed .csszyx-todo.json map. Values per entry: sz object | Tailwind string | "sz:keep" | "sz:remove" | "sz:todo" | null | false.'
|
|
679
|
+
},
|
|
680
|
+
injectTodos: {
|
|
681
|
+
type: "boolean",
|
|
682
|
+
description: "If true, inserts {/* @sz-todo: ... */} comments above elements with unrecognized classes."
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
required: ["code"]
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "csszyx_batch",
|
|
690
|
+
description: "Expand MULTIPLE sz objects into Tailwind class strings in one call. Use when you need to process an array of components. For a single object, use csszyx_expand instead.",
|
|
691
|
+
inputSchema: {
|
|
692
|
+
type: "object",
|
|
693
|
+
properties: {
|
|
694
|
+
items: {
|
|
695
|
+
type: "array",
|
|
696
|
+
items: { type: "object" },
|
|
697
|
+
description: "Array of sz objects. Example: [{ p: 4 }, { m: 2, bg: 'red-500' }]"
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
required: ["items"]
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
name: "csszyx_theme",
|
|
705
|
+
description: "Extract @theme CSS custom properties from Tailwind CSS file content. Categorizes variables by type: colors, fonts, spacing, breakpoints.",
|
|
706
|
+
inputSchema: {
|
|
707
|
+
type: "object",
|
|
708
|
+
properties: {
|
|
709
|
+
css: {
|
|
710
|
+
type: "string",
|
|
711
|
+
description: "CSS file content containing @theme blocks"
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
required: ["css"]
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
];
|
|
718
|
+
const TOOL_HANDLERS = {
|
|
719
|
+
csszyx_expand: { schema: expandSchema, handler: handleExpand },
|
|
720
|
+
csszyx_reverse: { schema: reverseSchema, handler: handleReverse },
|
|
721
|
+
csszyx_validate: { schema: validateSchema, handler: handleValidate },
|
|
722
|
+
csszyx_lookup: { schema: lookupSchema, handler: handleLookup },
|
|
723
|
+
csszyx_migrate: { schema: migrateSchema, handler: handleMigrate },
|
|
724
|
+
csszyx_batch: { schema: batchSchema, handler: handleBatch },
|
|
725
|
+
csszyx_theme: { schema: themeSchema, handler: handleTheme }
|
|
726
|
+
};
|
|
727
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
728
|
+
return { tools: TOOLS };
|
|
729
|
+
});
|
|
730
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
731
|
+
const { name, arguments: args } = request.params;
|
|
732
|
+
const entry = TOOL_HANDLERS[name];
|
|
733
|
+
if (!entry) {
|
|
734
|
+
throw new Error(
|
|
735
|
+
`Unknown tool: ${name}. Available: ${Object.keys(TOOL_HANDLERS).join(", ")}`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
const validated = entry.schema.parse(args);
|
|
740
|
+
return entry.handler(validated);
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
743
|
+
return {
|
|
744
|
+
content: [{ type: "text", text: `Error in ${name}: ${message}` }],
|
|
745
|
+
isError: true
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
750
|
+
return { resources: listResources() };
|
|
751
|
+
});
|
|
752
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
753
|
+
try {
|
|
754
|
+
return readResource(request.params.uri);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
757
|
+
throw new Error(`Resource error: ${message}`);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
761
|
+
return { prompts: listPrompts() };
|
|
762
|
+
});
|
|
763
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
764
|
+
try {
|
|
765
|
+
return getPrompt(
|
|
766
|
+
request.params.name,
|
|
767
|
+
request.params.arguments ?? {}
|
|
768
|
+
);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
771
|
+
throw new Error(`Prompt error: ${message}`);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
async function main() {
|
|
775
|
+
const transport = new StdioServerTransport();
|
|
776
|
+
await server.connect(transport);
|
|
777
|
+
console.error(`csszyx MCP Server v${VERSION} running on stdio`);
|
|
778
|
+
console.error(
|
|
779
|
+
` Tools: ${TOOLS.length} | Resources: ${listResources().length} | Prompts: ${listPrompts().length}`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
main().catch((error) => {
|
|
783
|
+
console.error("Server error:", error);
|
|
784
|
+
process.exit(1);
|
|
785
|
+
});
|