@webspire/mcp 0.3.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 +213 -213
- package/dist/index.js +1 -1
- package/dist/registration.js +146 -2
- package/package.json +3 -2
- package/dist/tools.d.ts +0 -3
- package/dist/tools.js +0 -151
package/dist/index.js
CHANGED
package/dist/registration.js
CHANGED
|
@@ -357,7 +357,7 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
357
357
|
],
|
|
358
358
|
};
|
|
359
359
|
});
|
|
360
|
-
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.', {
|
|
361
361
|
id: z.string().describe('Pattern ID, e.g. "hero/base", "hero/with-image"'),
|
|
362
362
|
}, async ({ id }) => {
|
|
363
363
|
const registry = await getRegistry();
|
|
@@ -374,7 +374,12 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
374
374
|
};
|
|
375
375
|
}
|
|
376
376
|
return {
|
|
377
|
-
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
|
+
],
|
|
378
383
|
};
|
|
379
384
|
});
|
|
380
385
|
server.tool('list_templates', 'List all available page templates grouped by category', {}, async () => {
|
|
@@ -455,6 +460,145 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
455
460
|
content: [{ type: 'text', text: formatTemplateFull(template) }],
|
|
456
461
|
};
|
|
457
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
|
+
],
|
|
600
|
+
};
|
|
601
|
+
});
|
|
458
602
|
}
|
|
459
603
|
export function registerResourcesWithProvider(server, getRegistry) {
|
|
460
604
|
server.resource('categories', 'webspire://categories', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webspire/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for Webspire — AI-native discovery of CSS snippets, UI patterns, and page templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -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
|
-
}
|