@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/dist/index.js CHANGED
@@ -11,7 +11,7 @@ const registryOptions = {
11
11
  };
12
12
  const server = new McpServer({
13
13
  name: 'webspire',
14
- version: '0.1.0',
14
+ version: '0.3.0',
15
15
  });
16
16
  registerTools(server, registryOptions);
17
17
  registerResources(server, registryOptions);
@@ -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, stems: string[]): number;
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
- 'ness',
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
- export function scoreSnippet(snippet, stems) {
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 += stems.filter((w) => lower.includes(w)).length * 3;
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 += stems.filter((w) => lower.includes(w)).length * 2;
82
+ score += queryStems.filter((w) => lower.includes(w)).length * 3;
32
83
  }
33
- const desc = snippet.description.toLowerCase();
34
- score += stems.filter((w) => desc.includes(w)).length;
84
+ // Title important
35
85
  const title = snippet.title.toLowerCase();
36
- score += stems.filter((w) => title.includes(w)).length * 2;
37
- score += stems.filter((w) => snippet.tags.some((t) => t.includes(w))).length;
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 += stems.filter((w) => classes.includes(w)).length;
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.1.0",
4
- "description": "MCP server for Webspire CSS snippets — AI-native snippet discovery and matching",
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": "catalog:",
27
- "zod": "catalog:"
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": "catalog:",
32
- "typescript": "catalog:"
31
+ "tsx": "^4.21.0",
32
+ "typescript": "^5.9.3"
33
33
  },
34
34
  "keywords": [
35
35
  "mcp",