@webspire/mcp 0.2.0 → 0.5.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 +35 -21
- package/css/webspire-components.css +481 -0
- package/css/webspire-tokens.css +151 -0
- package/data/registry.json +1309 -214
- package/dist/index.js +1 -1
- package/dist/registration.js +305 -2
- package/dist/types.d.ts +35 -0
- package/package.json +4 -3
- package/dist/tools.d.ts +0 -3
- package/dist/tools.js +0 -151
package/dist/index.js
CHANGED
package/dist/registration.js
CHANGED
|
@@ -58,6 +58,39 @@ function formatSnippetFull(s) {
|
|
|
58
58
|
.filter((line) => line !== '')
|
|
59
59
|
.join('\n');
|
|
60
60
|
}
|
|
61
|
+
function formatTemplateBrief(t) {
|
|
62
|
+
return [
|
|
63
|
+
`**${t.title}** (${t.id})`,
|
|
64
|
+
` ${t.summary}`,
|
|
65
|
+
` Category: ${t.category} | Style: ${t.style}`,
|
|
66
|
+
t.tags.length > 0 ? ` Tags: ${t.tags.join(', ')}` : '',
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join('\n');
|
|
70
|
+
}
|
|
71
|
+
function formatTemplateFull(t) {
|
|
72
|
+
return [
|
|
73
|
+
`# ${t.title}`,
|
|
74
|
+
'',
|
|
75
|
+
t.description ?? t.summary,
|
|
76
|
+
'',
|
|
77
|
+
'## Identity',
|
|
78
|
+
`ID: ${t.id}`,
|
|
79
|
+
`Category: ${t.category}`,
|
|
80
|
+
`Style: ${t.style}`,
|
|
81
|
+
'',
|
|
82
|
+
t.tags.length > 0 ? `## Tags\n${t.tags.join(', ')}` : '',
|
|
83
|
+
t.sections.length > 0 ? `## Sections\n${t.sections.join(', ')}` : '',
|
|
84
|
+
t.patterns.length > 0 ? `## Uses Patterns\n${t.patterns.join(', ')}` : '',
|
|
85
|
+
'',
|
|
86
|
+
'## HTML',
|
|
87
|
+
'```html',
|
|
88
|
+
t.html,
|
|
89
|
+
'```',
|
|
90
|
+
]
|
|
91
|
+
.filter((line) => line !== '')
|
|
92
|
+
.join('\n');
|
|
93
|
+
}
|
|
61
94
|
function formatPatternBrief(p) {
|
|
62
95
|
return [
|
|
63
96
|
`**${p.title}** (${p.id})`,
|
|
@@ -324,7 +357,7 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
324
357
|
],
|
|
325
358
|
};
|
|
326
359
|
});
|
|
327
|
-
server.tool('get_pattern', 'Get full pattern data including HTML (and optional CSS/JS) for a specific pattern.', {
|
|
360
|
+
server.tool('get_pattern', 'Get full pattern data including HTML (and optional CSS/JS) for a specific pattern. Patterns use --ws-* design tokens via component classes — import webspire-tokens.css + webspire-components.css to enable.', {
|
|
328
361
|
id: z.string().describe('Pattern ID, e.g. "hero/base", "hero/with-image"'),
|
|
329
362
|
}, async ({ id }) => {
|
|
330
363
|
const registry = await getRegistry();
|
|
@@ -341,7 +374,229 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
341
374
|
};
|
|
342
375
|
}
|
|
343
376
|
return {
|
|
344
|
-
content: [
|
|
377
|
+
content: [
|
|
378
|
+
{
|
|
379
|
+
type: 'text',
|
|
380
|
+
text: `${formatPatternFull(pattern)}\n\n---\n**Setup:** Import \`webspire-tokens.css\` and \`webspire-components.css\` to enable token support.\nOverride \`--ws-color-primary\` etc. to match your brand. Docs: https://webspire.de/tokens`,
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
server.tool('list_templates', 'List all available page templates grouped by category', {}, async () => {
|
|
386
|
+
const registry = await getRegistry();
|
|
387
|
+
const templates = registry.templates ?? [];
|
|
388
|
+
const counts = {};
|
|
389
|
+
for (const t of templates) {
|
|
390
|
+
counts[t.category] = (counts[t.category] ?? 0) + 1;
|
|
391
|
+
}
|
|
392
|
+
const result = Object.entries(counts)
|
|
393
|
+
.sort(([, a], [, b]) => b - a)
|
|
394
|
+
.map(([cat, count]) => `${cat}: ${count} template${count > 1 ? 's' : ''}`)
|
|
395
|
+
.join('\n');
|
|
396
|
+
return {
|
|
397
|
+
content: [
|
|
398
|
+
{
|
|
399
|
+
type: 'text',
|
|
400
|
+
text: `${templates.length} templates across ${Object.keys(counts).length} categories:\n\n${result}`,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
server.tool('search_templates', 'Search Webspire page templates by keyword, category, or style.', {
|
|
406
|
+
query: z.string().describe('Search query, e.g. "saas landing", "portfolio dark", "shop"'),
|
|
407
|
+
category: z.string().optional().describe('Filter by category, e.g. saas-landing, agency, shop'),
|
|
408
|
+
style: z.string().optional().describe('Filter by style, e.g. modern, bold, minimal, corporate'),
|
|
409
|
+
}, async ({ query, category, style }) => {
|
|
410
|
+
const registry = await getRegistry();
|
|
411
|
+
let templates = registry.templates ?? [];
|
|
412
|
+
if (category)
|
|
413
|
+
templates = templates.filter((t) => t.category === category);
|
|
414
|
+
if (style)
|
|
415
|
+
templates = templates.filter((t) => t.style === style);
|
|
416
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
417
|
+
const results = templates
|
|
418
|
+
.map((t) => {
|
|
419
|
+
const haystack = [t.title, t.summary, t.description ?? '', t.category, t.style, ...t.tags, ...t.sections]
|
|
420
|
+
.join(' ')
|
|
421
|
+
.toLowerCase();
|
|
422
|
+
const score = terms.reduce((acc, term) => acc + (haystack.includes(term) ? 1 : 0), 0);
|
|
423
|
+
return { template: t, score };
|
|
424
|
+
})
|
|
425
|
+
.filter((row) => row.score > 0)
|
|
426
|
+
.sort((a, b) => b.score - a.score)
|
|
427
|
+
.slice(0, 10)
|
|
428
|
+
.map((row) => row.template);
|
|
429
|
+
if (results.length === 0) {
|
|
430
|
+
return {
|
|
431
|
+
content: [{ type: 'text', text: 'No templates found matching your query.' }],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
content: [
|
|
436
|
+
{
|
|
437
|
+
type: 'text',
|
|
438
|
+
text: `Found ${results.length} template${results.length > 1 ? 's' : ''}:\n\n${results.map(formatTemplateBrief).join('\n\n')}`,
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
server.tool('get_template', 'Get full template HTML for a specific page template. Returns standalone HTML ready to use.', {
|
|
444
|
+
id: z.string().describe('Template ID, e.g. "saas-landing/modern", "shop/catalog"'),
|
|
445
|
+
}, async ({ id }) => {
|
|
446
|
+
const registry = await getRegistry();
|
|
447
|
+
const template = (registry.templates ?? []).find((t) => t.id === id);
|
|
448
|
+
if (!template) {
|
|
449
|
+
const available = (registry.templates ?? []).map((t) => t.id).join(', ');
|
|
450
|
+
return {
|
|
451
|
+
content: [
|
|
452
|
+
{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: `Template "${id}" not found. Available templates: ${available}`,
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
content: [{ type: 'text', text: formatTemplateFull(template) }],
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
server.tool('recommend_token_mapping', 'Analyze project design tokens and recommend how to map them to Webspire --ws-* tokens. Provide your existing CSS custom properties or Tailwind theme colors.', {
|
|
464
|
+
project_tokens: z
|
|
465
|
+
.string()
|
|
466
|
+
.describe('Your project\'s design tokens or CSS custom properties, e.g. "--color-brand-500: #2563eb; --color-bg: #fafafa" or "primary: blue-600, background: white"'),
|
|
467
|
+
framework: z
|
|
468
|
+
.enum(['tailwind', 'custom', 'bootstrap', 'chakra', 'shadcn'])
|
|
469
|
+
.optional()
|
|
470
|
+
.describe('CSS framework used in your project'),
|
|
471
|
+
}, async ({ project_tokens, framework }) => {
|
|
472
|
+
const lines = project_tokens.split(/[;\n,]+/).map((s) => s.trim()).filter(Boolean);
|
|
473
|
+
const mappings = [];
|
|
474
|
+
const TOKEN_HINTS = [
|
|
475
|
+
{ patterns: [/brand|primary|main|accent-color/i], wsToken: '--ws-color-primary', description: 'Primary/brand color' },
|
|
476
|
+
{ patterns: [/brand.*hover|primary.*hover|primary.*dark/i], wsToken: '--ws-color-primary-hover', description: 'Primary hover state' },
|
|
477
|
+
{ patterns: [/brand.*light|primary.*light|primary.*50|brand.*50/i], wsToken: '--ws-color-primary-soft', description: 'Primary soft background' },
|
|
478
|
+
{ patterns: [/secondary|accent(?!-color)/i], wsToken: '--ws-color-accent', description: 'Secondary/accent color' },
|
|
479
|
+
{ patterns: [/^bg$|background(?!.*secondary)|surface(?!.*alt)/i], wsToken: '--ws-color-surface', description: 'Page background' },
|
|
480
|
+
{ patterns: [/bg.*secondary|bg.*subtle|bg.*muted|surface.*alt|bg.*light/i], wsToken: '--ws-color-surface-alt', description: 'Subtle background' },
|
|
481
|
+
{ patterns: [/^text$|text.*primary|foreground(?!.*muted)/i], wsToken: '--ws-color-text', description: 'Primary text color' },
|
|
482
|
+
{ patterns: [/text.*secondary|text.*soft|text.*muted|muted.*foreground/i], wsToken: '--ws-color-text-muted', description: 'Muted text color' },
|
|
483
|
+
{ patterns: [/border(?!.*strong)|divider/i], wsToken: '--ws-color-border', description: 'Default border' },
|
|
484
|
+
{ patterns: [/success|green|positive/i], wsToken: '--ws-color-success', description: 'Success color' },
|
|
485
|
+
{ patterns: [/warning|amber|yellow|caution/i], wsToken: '--ws-color-warning', description: 'Warning color' },
|
|
486
|
+
{ patterns: [/danger|error|red|destructive/i], wsToken: '--ws-color-danger', description: 'Danger/error color' },
|
|
487
|
+
{ patterns: [/radius|rounded|corner/i], wsToken: '--ws-radius-md', description: 'Border radius' },
|
|
488
|
+
];
|
|
489
|
+
for (const line of lines) {
|
|
490
|
+
const [rawName] = line.split(':').map((s) => s.trim());
|
|
491
|
+
const name = rawName.replace(/^--color-|^--/, '');
|
|
492
|
+
for (const hint of TOKEN_HINTS) {
|
|
493
|
+
if (hint.patterns.some((p) => p.test(name))) {
|
|
494
|
+
const varRef = rawName.startsWith('--') ? `var(${rawName})` : rawName;
|
|
495
|
+
mappings.push(` ${hint.wsToken}: ${varRef}; /* ${hint.description} ← ${rawName} */`);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const frameworkNote = framework
|
|
501
|
+
? `\nFramework detected: ${framework}. ${framework === 'tailwind'
|
|
502
|
+
? 'Use @theme to register --ws-* tokens, or override in your main CSS.'
|
|
503
|
+
: framework === 'shadcn'
|
|
504
|
+
? 'Map shadcn/ui --* variables to --ws-* in your globals.css.'
|
|
505
|
+
: 'Override --ws-* tokens in your global stylesheet.'}\n`
|
|
506
|
+
: '';
|
|
507
|
+
const css = mappings.length > 0
|
|
508
|
+
? `:root {\n${mappings.join('\n')}\n}`
|
|
509
|
+
: '/* No matching tokens detected. Provide CSS custom properties like:\n --color-brand: #2563eb; --color-bg: #ffffff */';
|
|
510
|
+
return {
|
|
511
|
+
content: [
|
|
512
|
+
{
|
|
513
|
+
type: 'text',
|
|
514
|
+
text: `# Recommended Token Mapping\n${frameworkNote}\n\`\`\`css\n${css}\n\`\`\`\n\nPaste this into your project's CSS, then import webspire-tokens.css.\nAdjust values as needed — these are suggestions based on naming conventions.\n\nFull token reference: https://webspire.de/tokens`,
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
server.tool('setup_tokens', 'Get the WebSpire token CSS files to write into your project. Returns the content of webspire-tokens.css (alias tokens + dark mode) and webspire-components.css (per-component tokens). Write these files to your project and import them in your main CSS.', {
|
|
520
|
+
components: z
|
|
521
|
+
.array(z.string())
|
|
522
|
+
.optional()
|
|
523
|
+
.describe('Only include component tokens for these families (e.g. ["hero", "cta", "pricing"]). If omitted, all component tokens are included.'),
|
|
524
|
+
}, async ({ components }) => {
|
|
525
|
+
const registry = await getRegistry();
|
|
526
|
+
// Read bundled CSS files
|
|
527
|
+
const { readFile } = await import('node:fs/promises');
|
|
528
|
+
const { dirname, resolve } = await import('node:path');
|
|
529
|
+
const { fileURLToPath } = await import('node:url');
|
|
530
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
531
|
+
let tokensCss;
|
|
532
|
+
let componentsCss;
|
|
533
|
+
try {
|
|
534
|
+
tokensCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-tokens.css'), 'utf-8');
|
|
535
|
+
componentsCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-components.css'), 'utf-8');
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return {
|
|
539
|
+
content: [
|
|
540
|
+
{
|
|
541
|
+
type: 'text',
|
|
542
|
+
text: 'Token CSS files not found in package. Update @webspire/mcp to the latest version.',
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
// Filter component tokens if specific families requested
|
|
548
|
+
if (components && components.length > 0) {
|
|
549
|
+
const filtered = ['/* Webspire Component Tokens (filtered) */'];
|
|
550
|
+
const sections = componentsCss.split(/(?=^\.ws-)/m);
|
|
551
|
+
for (const section of sections) {
|
|
552
|
+
const classMatch = section.match(/^\.ws-([a-z0-9-]+)/);
|
|
553
|
+
if (classMatch && components.includes(classMatch[1])) {
|
|
554
|
+
filtered.push(section.trim());
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
componentsCss = filtered.join('\n\n');
|
|
558
|
+
}
|
|
559
|
+
const families = components ? components.join(', ') : 'all';
|
|
560
|
+
const patternCount = registry.patterns?.length ?? 0;
|
|
561
|
+
return {
|
|
562
|
+
content: [
|
|
563
|
+
{
|
|
564
|
+
type: 'text',
|
|
565
|
+
text: [
|
|
566
|
+
`# WebSpire Token Setup (${families} families, ${patternCount} patterns)`,
|
|
567
|
+
'',
|
|
568
|
+
'## 1. Write `webspire-tokens.css` to your project',
|
|
569
|
+
'',
|
|
570
|
+
'```css',
|
|
571
|
+
tokensCss,
|
|
572
|
+
'```',
|
|
573
|
+
'',
|
|
574
|
+
'## 2. Write `webspire-components.css` to your project',
|
|
575
|
+
'',
|
|
576
|
+
'```css',
|
|
577
|
+
componentsCss,
|
|
578
|
+
'```',
|
|
579
|
+
'',
|
|
580
|
+
'## 3. Import in your main CSS',
|
|
581
|
+
'',
|
|
582
|
+
'```css',
|
|
583
|
+
'@import "./webspire-tokens.css";',
|
|
584
|
+
'@import "./webspire-components.css";',
|
|
585
|
+
'```',
|
|
586
|
+
'',
|
|
587
|
+
'## 4. Override to match your brand',
|
|
588
|
+
'',
|
|
589
|
+
'```css',
|
|
590
|
+
':root {',
|
|
591
|
+
' --ws-color-primary: #your-brand-color;',
|
|
592
|
+
' --ws-color-primary-hover: #your-brand-darker;',
|
|
593
|
+
'}',
|
|
594
|
+
'```',
|
|
595
|
+
'',
|
|
596
|
+
'Docs: https://webspire.de/tokens',
|
|
597
|
+
].join('\n'),
|
|
598
|
+
},
|
|
599
|
+
],
|
|
345
600
|
};
|
|
346
601
|
});
|
|
347
602
|
}
|
|
@@ -468,4 +723,52 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
468
723
|
],
|
|
469
724
|
};
|
|
470
725
|
});
|
|
726
|
+
server.resource('templates', 'webspire://templates', { description: 'List all page templates', mimeType: 'application/json' }, async () => {
|
|
727
|
+
const registry = await getRegistry();
|
|
728
|
+
const templates = (registry.templates ?? []).map((t) => ({
|
|
729
|
+
id: t.id,
|
|
730
|
+
title: t.title,
|
|
731
|
+
summary: t.summary,
|
|
732
|
+
category: t.category,
|
|
733
|
+
style: t.style,
|
|
734
|
+
}));
|
|
735
|
+
return {
|
|
736
|
+
contents: [
|
|
737
|
+
{
|
|
738
|
+
uri: 'webspire://templates',
|
|
739
|
+
mimeType: 'application/json',
|
|
740
|
+
text: JSON.stringify(templates, null, 2),
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
server.resource('template', 'webspire://template/{id}', {
|
|
746
|
+
description: 'Get full template data including HTML source',
|
|
747
|
+
mimeType: 'application/json',
|
|
748
|
+
}, async (uri) => {
|
|
749
|
+
const parts = uri.pathname.replace('//', '').split('/');
|
|
750
|
+
const id = parts.slice(1).join('/');
|
|
751
|
+
const registry = await getRegistry();
|
|
752
|
+
const template = (registry.templates ?? []).find((t) => t.id === id);
|
|
753
|
+
if (!template) {
|
|
754
|
+
return {
|
|
755
|
+
contents: [
|
|
756
|
+
{
|
|
757
|
+
uri: uri.href,
|
|
758
|
+
mimeType: 'text/plain',
|
|
759
|
+
text: `Template "${id}" not found`,
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
contents: [
|
|
766
|
+
{
|
|
767
|
+
uri: uri.href,
|
|
768
|
+
mimeType: 'application/json',
|
|
769
|
+
text: JSON.stringify(template, null, 2),
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
};
|
|
773
|
+
});
|
|
471
774
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -106,9 +106,44 @@ export interface PatternEntry {
|
|
|
106
106
|
css: string | null;
|
|
107
107
|
js: string | null;
|
|
108
108
|
}
|
|
109
|
+
export interface TemplateEntry {
|
|
110
|
+
id: string;
|
|
111
|
+
title: string;
|
|
112
|
+
summary: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
category: string;
|
|
115
|
+
style: string;
|
|
116
|
+
tags: string[];
|
|
117
|
+
patterns: string[];
|
|
118
|
+
sections: string[];
|
|
119
|
+
features: {
|
|
120
|
+
responsive: boolean;
|
|
121
|
+
darkMode: boolean;
|
|
122
|
+
animations: boolean;
|
|
123
|
+
formHandling: boolean;
|
|
124
|
+
};
|
|
125
|
+
files: {
|
|
126
|
+
html: string;
|
|
127
|
+
preview: string;
|
|
128
|
+
};
|
|
129
|
+
install: {
|
|
130
|
+
copyPasteReady: boolean;
|
|
131
|
+
tailwindCdn: boolean;
|
|
132
|
+
vanillaJs: boolean;
|
|
133
|
+
notes: string[];
|
|
134
|
+
};
|
|
135
|
+
governance: {
|
|
136
|
+
status: 'draft' | 'review' | 'published' | 'deprecated';
|
|
137
|
+
quality: 'experimental' | 'stable' | 'flagship';
|
|
138
|
+
owner: string;
|
|
139
|
+
updatedAt: string;
|
|
140
|
+
};
|
|
141
|
+
html: string;
|
|
142
|
+
}
|
|
109
143
|
export interface Registry {
|
|
110
144
|
version: string;
|
|
111
145
|
generated: string;
|
|
112
146
|
snippets: SnippetEntry[];
|
|
113
147
|
patterns?: PatternEntry[];
|
|
148
|
+
templates?: TemplateEntry[];
|
|
114
149
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webspire/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Webspire
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "MCP server for Webspire — AI-native discovery of CSS snippets, UI patterns, and page templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./dist/index.js",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
-
"data"
|
|
23
|
+
"data",
|
|
24
|
+
"css"
|
|
24
25
|
],
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"@modelcontextprotocol/sdk": "^1.13.0",
|
package/dist/tools.d.ts
DELETED
package/dist/tools.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { loadRegistry, searchSnippets, suggestSnippets, } from './registry.js';
|
|
3
|
-
function formatSnippetBrief(s) {
|
|
4
|
-
return [
|
|
5
|
-
`**${s.title}** (${s.id})`,
|
|
6
|
-
` ${s.description}`,
|
|
7
|
-
` Category: ${s.category} | Tags: ${s.tags.join(', ')}`,
|
|
8
|
-
s.meta.useCases.length > 0
|
|
9
|
-
? ` Use cases: ${s.meta.useCases.slice(0, 2).join('; ')}`
|
|
10
|
-
: '',
|
|
11
|
-
]
|
|
12
|
-
.filter(Boolean)
|
|
13
|
-
.join('\n');
|
|
14
|
-
}
|
|
15
|
-
function formatSnippetFull(s) {
|
|
16
|
-
const props = s.meta.customProperties
|
|
17
|
-
.map((p) => ` ${p}: ${s.meta.defaultValues[p] ?? 'unset'}`)
|
|
18
|
-
.join('\n');
|
|
19
|
-
return [
|
|
20
|
-
`# ${s.title}`,
|
|
21
|
-
'',
|
|
22
|
-
s.description,
|
|
23
|
-
'',
|
|
24
|
-
'## Usage',
|
|
25
|
-
'```html',
|
|
26
|
-
s.meta.usageExample,
|
|
27
|
-
'```',
|
|
28
|
-
'',
|
|
29
|
-
'## CSS',
|
|
30
|
-
'```css',
|
|
31
|
-
s.css,
|
|
32
|
-
'```',
|
|
33
|
-
'',
|
|
34
|
-
s.meta.customProperties.length > 0 ? `## Custom Properties\n${props}` : '',
|
|
35
|
-
s.meta.classes.length > 0
|
|
36
|
-
? `## Classes\n${s.meta.classes.join(', ')}`
|
|
37
|
-
: '',
|
|
38
|
-
s.meta.accessibility.length > 0
|
|
39
|
-
? `## Accessibility\nRespects: ${s.meta.accessibility.join(', ')}`
|
|
40
|
-
: '',
|
|
41
|
-
'',
|
|
42
|
-
`Browser support: ${s.meta.browser}`,
|
|
43
|
-
`Size: ${s.meta.lines} lines, ${s.meta.bytes} bytes`,
|
|
44
|
-
]
|
|
45
|
-
.filter((line) => line !== '')
|
|
46
|
-
.join('\n');
|
|
47
|
-
}
|
|
48
|
-
export function registerTools(server, registryOptions) {
|
|
49
|
-
server.tool('list_categories', 'List all available snippet categories with snippet counts', {}, async () => {
|
|
50
|
-
const registry = await loadRegistry(registryOptions);
|
|
51
|
-
const counts = {};
|
|
52
|
-
for (const snippet of registry.snippets) {
|
|
53
|
-
counts[snippet.category] = (counts[snippet.category] ?? 0) + 1;
|
|
54
|
-
}
|
|
55
|
-
const result = Object.entries(counts)
|
|
56
|
-
.sort(([, a], [, b]) => b - a)
|
|
57
|
-
.map(([cat, count]) => `${cat}: ${count} snippet${count > 1 ? 's' : ''}`)
|
|
58
|
-
.join('\n');
|
|
59
|
-
return {
|
|
60
|
-
content: [
|
|
61
|
-
{
|
|
62
|
-
type: 'text',
|
|
63
|
-
text: `${registry.snippets.length} snippets across ${Object.keys(counts).length} categories:\n\n${result}`,
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
server.tool('search_snippets', 'Search Webspire CSS snippets by keyword, problem description, or use case. Returns matching snippets with their descriptions and IDs.', {
|
|
69
|
-
query: z
|
|
70
|
-
.string()
|
|
71
|
-
.describe('Search query: problem description, use case, or CSS technique (e.g. "glass card", "fade animation", "blur entrance")'),
|
|
72
|
-
category: z
|
|
73
|
-
.string()
|
|
74
|
-
.optional()
|
|
75
|
-
.describe('Filter by category: glass, animations, easing, scroll, decorative, interactions, text'),
|
|
76
|
-
tags: z.array(z.string()).optional().describe('Filter by tags'),
|
|
77
|
-
}, async ({ query, category, tags }) => {
|
|
78
|
-
const registry = await loadRegistry(registryOptions);
|
|
79
|
-
const results = searchSnippets(registry, query, category, tags);
|
|
80
|
-
if (results.length === 0) {
|
|
81
|
-
return {
|
|
82
|
-
content: [
|
|
83
|
-
{
|
|
84
|
-
type: 'text',
|
|
85
|
-
text: 'No snippets found matching your query. Try broader keywords or check available categories with list_categories.',
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
const text = results.map(formatSnippetBrief).join('\n\n');
|
|
91
|
-
return {
|
|
92
|
-
content: [
|
|
93
|
-
{
|
|
94
|
-
type: 'text',
|
|
95
|
-
text: `Found ${results.length} snippet${results.length > 1 ? 's' : ''}:\n\n${text}`,
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
|
-
server.tool('get_snippet', 'Get full CSS source, metadata, usage example, and custom properties for a specific snippet. Use this after finding a snippet via search_snippets or suggest_snippet.', {
|
|
101
|
-
id: z
|
|
102
|
-
.string()
|
|
103
|
-
.describe('Snippet ID, e.g. "glass/frosted", "animations/fade-in", "easing/tokens"'),
|
|
104
|
-
}, async ({ id }) => {
|
|
105
|
-
const registry = await loadRegistry(registryOptions);
|
|
106
|
-
const snippet = registry.snippets.find((s) => s.id === id);
|
|
107
|
-
if (!snippet) {
|
|
108
|
-
const available = registry.snippets.map((s) => s.id).join(', ');
|
|
109
|
-
return {
|
|
110
|
-
content: [
|
|
111
|
-
{
|
|
112
|
-
type: 'text',
|
|
113
|
-
text: `Snippet "${id}" not found. Available snippets: ${available}`,
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
content: [{ type: 'text', text: formatSnippetFull(snippet) }],
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
server.tool('suggest_snippet', 'Describe a UI problem or desired effect in plain language and get the best matching Webspire snippets. The AI matches your description against each snippet\'s use cases and problem statements.', {
|
|
123
|
-
problem: z
|
|
124
|
-
.string()
|
|
125
|
-
.describe('Describe the UI problem or effect you need (e.g. "I need a blurred card overlay on my hero section", "my list items should animate in one by one")'),
|
|
126
|
-
}, async ({ problem }) => {
|
|
127
|
-
const registry = await loadRegistry(registryOptions);
|
|
128
|
-
const results = suggestSnippets(registry, problem);
|
|
129
|
-
if (results.length === 0) {
|
|
130
|
-
return {
|
|
131
|
-
content: [
|
|
132
|
-
{
|
|
133
|
-
type: 'text',
|
|
134
|
-
text: 'No matching snippet found for your description. Try rephrasing or use search_snippets with specific keywords.',
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const text = results
|
|
140
|
-
.map((s, i) => `${i + 1}. ${formatSnippetBrief(s)}\n Usage: \`${s.meta.usageExample}\``)
|
|
141
|
-
.join('\n\n');
|
|
142
|
-
return {
|
|
143
|
-
content: [
|
|
144
|
-
{
|
|
145
|
-
type: 'text',
|
|
146
|
-
text: `Top ${results.length} suggestion${results.length > 1 ? 's' : ''} for "${problem}":\n\n${text}\n\nUse get_snippet(id) for full CSS and details.`,
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
}
|