@webspire/mcp 0.1.0 → 0.3.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 +92 -0
- package/data/registry.json +1097 -2
- package/dist/index.js +1 -1
- package/dist/registration.js +159 -0
- package/dist/search.d.ts +1 -1
- package/dist/search.js +77 -12
- package/dist/types.d.ts +35 -0
- package/package.json +6 -6
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})`,
|
|
@@ -344,6 +377,84 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
344
377
|
content: [{ type: 'text', text: formatPatternFull(pattern) }],
|
|
345
378
|
};
|
|
346
379
|
});
|
|
380
|
+
server.tool('list_templates', 'List all available page templates grouped by category', {}, async () => {
|
|
381
|
+
const registry = await getRegistry();
|
|
382
|
+
const templates = registry.templates ?? [];
|
|
383
|
+
const counts = {};
|
|
384
|
+
for (const t of templates) {
|
|
385
|
+
counts[t.category] = (counts[t.category] ?? 0) + 1;
|
|
386
|
+
}
|
|
387
|
+
const result = Object.entries(counts)
|
|
388
|
+
.sort(([, a], [, b]) => b - a)
|
|
389
|
+
.map(([cat, count]) => `${cat}: ${count} template${count > 1 ? 's' : ''}`)
|
|
390
|
+
.join('\n');
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: 'text',
|
|
395
|
+
text: `${templates.length} templates across ${Object.keys(counts).length} categories:\n\n${result}`,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
server.tool('search_templates', 'Search Webspire page templates by keyword, category, or style.', {
|
|
401
|
+
query: z.string().describe('Search query, e.g. "saas landing", "portfolio dark", "shop"'),
|
|
402
|
+
category: z.string().optional().describe('Filter by category, e.g. saas-landing, agency, shop'),
|
|
403
|
+
style: z.string().optional().describe('Filter by style, e.g. modern, bold, minimal, corporate'),
|
|
404
|
+
}, async ({ query, category, style }) => {
|
|
405
|
+
const registry = await getRegistry();
|
|
406
|
+
let templates = registry.templates ?? [];
|
|
407
|
+
if (category)
|
|
408
|
+
templates = templates.filter((t) => t.category === category);
|
|
409
|
+
if (style)
|
|
410
|
+
templates = templates.filter((t) => t.style === style);
|
|
411
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
412
|
+
const results = templates
|
|
413
|
+
.map((t) => {
|
|
414
|
+
const haystack = [t.title, t.summary, t.description ?? '', t.category, t.style, ...t.tags, ...t.sections]
|
|
415
|
+
.join(' ')
|
|
416
|
+
.toLowerCase();
|
|
417
|
+
const score = terms.reduce((acc, term) => acc + (haystack.includes(term) ? 1 : 0), 0);
|
|
418
|
+
return { template: t, score };
|
|
419
|
+
})
|
|
420
|
+
.filter((row) => row.score > 0)
|
|
421
|
+
.sort((a, b) => b.score - a.score)
|
|
422
|
+
.slice(0, 10)
|
|
423
|
+
.map((row) => row.template);
|
|
424
|
+
if (results.length === 0) {
|
|
425
|
+
return {
|
|
426
|
+
content: [{ type: 'text', text: 'No templates found matching your query.' }],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: 'text',
|
|
433
|
+
text: `Found ${results.length} template${results.length > 1 ? 's' : ''}:\n\n${results.map(formatTemplateBrief).join('\n\n')}`,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
server.tool('get_template', 'Get full template HTML for a specific page template. Returns standalone HTML ready to use.', {
|
|
439
|
+
id: z.string().describe('Template ID, e.g. "saas-landing/modern", "shop/catalog"'),
|
|
440
|
+
}, async ({ id }) => {
|
|
441
|
+
const registry = await getRegistry();
|
|
442
|
+
const template = (registry.templates ?? []).find((t) => t.id === id);
|
|
443
|
+
if (!template) {
|
|
444
|
+
const available = (registry.templates ?? []).map((t) => t.id).join(', ');
|
|
445
|
+
return {
|
|
446
|
+
content: [
|
|
447
|
+
{
|
|
448
|
+
type: 'text',
|
|
449
|
+
text: `Template "${id}" not found. Available templates: ${available}`,
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
content: [{ type: 'text', text: formatTemplateFull(template) }],
|
|
456
|
+
};
|
|
457
|
+
});
|
|
347
458
|
}
|
|
348
459
|
export function registerResourcesWithProvider(server, getRegistry) {
|
|
349
460
|
server.resource('categories', 'webspire://categories', {
|
|
@@ -468,4 +579,52 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
468
579
|
],
|
|
469
580
|
};
|
|
470
581
|
});
|
|
582
|
+
server.resource('templates', 'webspire://templates', { description: 'List all page templates', mimeType: 'application/json' }, async () => {
|
|
583
|
+
const registry = await getRegistry();
|
|
584
|
+
const templates = (registry.templates ?? []).map((t) => ({
|
|
585
|
+
id: t.id,
|
|
586
|
+
title: t.title,
|
|
587
|
+
summary: t.summary,
|
|
588
|
+
category: t.category,
|
|
589
|
+
style: t.style,
|
|
590
|
+
}));
|
|
591
|
+
return {
|
|
592
|
+
contents: [
|
|
593
|
+
{
|
|
594
|
+
uri: 'webspire://templates',
|
|
595
|
+
mimeType: 'application/json',
|
|
596
|
+
text: JSON.stringify(templates, null, 2),
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
});
|
|
601
|
+
server.resource('template', 'webspire://template/{id}', {
|
|
602
|
+
description: 'Get full template data including HTML source',
|
|
603
|
+
mimeType: 'application/json',
|
|
604
|
+
}, async (uri) => {
|
|
605
|
+
const parts = uri.pathname.replace('//', '').split('/');
|
|
606
|
+
const id = parts.slice(1).join('/');
|
|
607
|
+
const registry = await getRegistry();
|
|
608
|
+
const template = (registry.templates ?? []).find((t) => t.id === id);
|
|
609
|
+
if (!template) {
|
|
610
|
+
return {
|
|
611
|
+
contents: [
|
|
612
|
+
{
|
|
613
|
+
uri: uri.href,
|
|
614
|
+
mimeType: 'text/plain',
|
|
615
|
+
text: `Template "${id}" not found`,
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
contents: [
|
|
622
|
+
{
|
|
623
|
+
uri: uri.href,
|
|
624
|
+
mimeType: 'application/json',
|
|
625
|
+
text: JSON.stringify(template, null, 2),
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
};
|
|
629
|
+
});
|
|
471
630
|
}
|
package/dist/search.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export interface SnippetSearchOptions {
|
|
|
8
8
|
limit?: number;
|
|
9
9
|
}
|
|
10
10
|
export declare function stem(word: string): string;
|
|
11
|
-
export declare function scoreSnippet(snippet: SnippetEntry,
|
|
11
|
+
export declare function scoreSnippet(snippet: SnippetEntry, queryStems: string[], expandedTerms: string[]): number;
|
|
12
12
|
export declare function searchSnippets(snippets: SnippetEntry[], options: SnippetSearchOptions): SnippetEntry[];
|
package/dist/search.js
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
const STEM_SUFFIXES = [
|
|
2
|
-
'ions',
|
|
3
|
-
'ing',
|
|
4
2
|
'tion',
|
|
5
|
-
'
|
|
3
|
+
'sion',
|
|
4
|
+
'ions',
|
|
6
5
|
'ment',
|
|
6
|
+
'ness',
|
|
7
7
|
'able',
|
|
8
8
|
'ible',
|
|
9
9
|
'ous',
|
|
10
10
|
'ive',
|
|
11
|
+
'ing',
|
|
11
12
|
'ed',
|
|
12
13
|
'es',
|
|
14
|
+
'ly',
|
|
13
15
|
's',
|
|
14
16
|
];
|
|
17
|
+
/** Common synonyms for CSS/UI terms — maps alternative words to canonical terms */
|
|
18
|
+
const SYNONYMS = {
|
|
19
|
+
blur: ['glass', 'frosted', 'glassmorphism', 'backdrop'],
|
|
20
|
+
glass: ['blur', 'frosted', 'glassmorphism', 'transparent'],
|
|
21
|
+
fade: ['opacity', 'appear', 'entrance', 'reveal'],
|
|
22
|
+
slide: ['translate', 'move', 'enter', 'entrance'],
|
|
23
|
+
hover: ['mouse', 'interaction', 'lift', 'pointer'],
|
|
24
|
+
card: ['panel', 'surface', 'container', 'box'],
|
|
25
|
+
animation: ['animate', 'motion', 'transition', 'keyframe'],
|
|
26
|
+
glow: ['neon', 'light', 'shine', 'luminous'],
|
|
27
|
+
scroll: ['viewport', 'intersection', 'parallax', 'progress'],
|
|
28
|
+
text: ['font', 'typography', 'heading', 'title'],
|
|
29
|
+
gradient: ['color', 'blend', 'mesh', 'aurora'],
|
|
30
|
+
dark: ['darkmode', 'theme', 'scheme', 'night'],
|
|
31
|
+
button: ['btn', 'cta', 'action', 'click'],
|
|
32
|
+
responsive: ['mobile', 'fluid', 'adaptive', 'clamp'],
|
|
33
|
+
focus: ['keyboard', 'a11y', 'accessibility', 'ring'],
|
|
34
|
+
stagger: ['cascade', 'sequence', 'delay', 'children'],
|
|
35
|
+
reveal: ['show', 'appear', 'unhide', 'visible', 'fade'],
|
|
36
|
+
shimmer: ['skeleton', 'loading', 'placeholder', 'pulse'],
|
|
37
|
+
ripple: ['click', 'material', 'touch', 'feedback'],
|
|
38
|
+
};
|
|
15
39
|
export function stem(word) {
|
|
16
40
|
for (const suffix of STEM_SUFFIXES) {
|
|
17
41
|
if (word.length > suffix.length + 2 && word.endsWith(suffix)) {
|
|
@@ -20,29 +44,70 @@ export function stem(word) {
|
|
|
20
44
|
}
|
|
21
45
|
return word;
|
|
22
46
|
}
|
|
23
|
-
|
|
47
|
+
/** Expand query terms with synonyms for broader matching */
|
|
48
|
+
function expandWithSynonyms(terms) {
|
|
49
|
+
const expanded = new Set(terms);
|
|
50
|
+
for (const term of terms) {
|
|
51
|
+
// Direct synonym lookup
|
|
52
|
+
if (SYNONYMS[term]) {
|
|
53
|
+
for (const syn of SYNONYMS[term]) {
|
|
54
|
+
expanded.add(syn);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Reverse lookup: if term appears as a synonym value
|
|
58
|
+
for (const [key, values] of Object.entries(SYNONYMS)) {
|
|
59
|
+
if (values.includes(term)) {
|
|
60
|
+
expanded.add(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...expanded];
|
|
65
|
+
}
|
|
66
|
+
export function scoreSnippet(snippet, queryStems, expandedTerms) {
|
|
24
67
|
let score = 0;
|
|
68
|
+
// Exact ID match — highest priority
|
|
69
|
+
const idLower = snippet.id.toLowerCase();
|
|
70
|
+
for (const term of queryStems) {
|
|
71
|
+
if (idLower.includes(term))
|
|
72
|
+
score += 5;
|
|
73
|
+
}
|
|
74
|
+
// solves field — primary intent match
|
|
25
75
|
for (const solve of snippet.meta.solves) {
|
|
26
76
|
const lower = solve.toLowerCase();
|
|
27
|
-
score +=
|
|
77
|
+
score += queryStems.filter((w) => lower.includes(w)).length * 4;
|
|
28
78
|
}
|
|
79
|
+
// useCases — strong signal
|
|
29
80
|
for (const useCase of snippet.meta.useCases) {
|
|
30
81
|
const lower = useCase.toLowerCase();
|
|
31
|
-
score +=
|
|
82
|
+
score += queryStems.filter((w) => lower.includes(w)).length * 3;
|
|
32
83
|
}
|
|
33
|
-
|
|
34
|
-
score += stems.filter((w) => desc.includes(w)).length;
|
|
84
|
+
// Title — important
|
|
35
85
|
const title = snippet.title.toLowerCase();
|
|
36
|
-
score +=
|
|
37
|
-
|
|
86
|
+
score += queryStems.filter((w) => title.includes(w)).length * 3;
|
|
87
|
+
// Description
|
|
88
|
+
const desc = snippet.description.toLowerCase();
|
|
89
|
+
score += queryStems.filter((w) => desc.includes(w)).length * 2;
|
|
90
|
+
// Tags
|
|
91
|
+
const tagStr = snippet.tags.join(' ').toLowerCase();
|
|
92
|
+
score += queryStems.filter((w) => tagStr.includes(w)).length * 2;
|
|
93
|
+
// Classes
|
|
38
94
|
const classes = snippet.meta.classes.join(' ').toLowerCase();
|
|
39
|
-
score +=
|
|
95
|
+
score += queryStems.filter((w) => classes.includes(w)).length;
|
|
96
|
+
// Synonym-expanded terms — lower weight to avoid noise
|
|
97
|
+
const synonymOnly = expandedTerms.filter((t) => !queryStems.includes(t));
|
|
98
|
+
if (synonymOnly.length > 0) {
|
|
99
|
+
const allText = [title, desc, tagStr, ...snippet.meta.useCases, ...snippet.meta.solves]
|
|
100
|
+
.join(' ')
|
|
101
|
+
.toLowerCase();
|
|
102
|
+
score += synonymOnly.filter((w) => allText.includes(w)).length;
|
|
103
|
+
}
|
|
40
104
|
return score;
|
|
41
105
|
}
|
|
42
106
|
export function searchSnippets(snippets, options) {
|
|
43
107
|
const { query, category, tags, darkMode, responsive, limit = 10 } = options;
|
|
44
108
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
45
109
|
const stems = words.map(stem);
|
|
110
|
+
const expanded = expandWithSynonyms(words);
|
|
46
111
|
if (stems.length === 0) {
|
|
47
112
|
return [];
|
|
48
113
|
}
|
|
@@ -58,7 +123,7 @@ export function searchSnippets(snippets, options) {
|
|
|
58
123
|
return false;
|
|
59
124
|
return true;
|
|
60
125
|
})
|
|
61
|
-
.map((snippet) => ({ snippet, score: scoreSnippet(snippet, stems) }))
|
|
126
|
+
.map((snippet) => ({ snippet, score: scoreSnippet(snippet, stems, expanded) }))
|
|
62
127
|
.filter(({ score }) => score > 0)
|
|
63
128
|
.sort((a, b) => b.score - a.score);
|
|
64
129
|
return scored.slice(0, limit).map(({ snippet }) => snippet);
|
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.3.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",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"data"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "
|
|
27
|
-
"zod": "
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
27
|
+
"zod": "^4.2.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^22.0.0",
|
|
31
|
-
"tsx": "
|
|
32
|
-
"typescript": "
|
|
31
|
+
"tsx": "^4.21.0",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"mcp",
|