@webspire/mcp 0.3.0 → 0.6.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
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
2
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
7
  import { registerResources, registerTools } from './handlers.js';
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
5
10
  const args = process.argv.slice(2);
6
11
  const urlIdx = args.indexOf('--registry-url');
7
12
  const fileIdx = args.indexOf('--registry-file');
@@ -11,7 +16,7 @@ const registryOptions = {
11
16
  };
12
17
  const server = new McpServer({
13
18
  name: 'webspire',
14
- version: '0.3.0',
19
+ version: pkg.version,
15
20
  });
16
21
  registerTools(server, registryOptions);
17
22
  registerResources(server, registryOptions);
@@ -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: [{ type: 'text', text: formatPatternFull(pattern) }],
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 () => {
@@ -399,8 +404,14 @@ export function registerToolsWithProvider(server, getRegistry) {
399
404
  });
400
405
  server.tool('search_templates', 'Search Webspire page templates by keyword, category, or style.', {
401
406
  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'),
407
+ category: z
408
+ .string()
409
+ .optional()
410
+ .describe('Filter by category, e.g. saas-landing, agency, shop'),
411
+ style: z
412
+ .string()
413
+ .optional()
414
+ .describe('Filter by style, e.g. modern, bold, minimal, corporate'),
404
415
  }, async ({ query, category, style }) => {
405
416
  const registry = await getRegistry();
406
417
  let templates = registry.templates ?? [];
@@ -411,7 +422,15 @@ export function registerToolsWithProvider(server, getRegistry) {
411
422
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
412
423
  const results = templates
413
424
  .map((t) => {
414
- const haystack = [t.title, t.summary, t.description ?? '', t.category, t.style, ...t.tags, ...t.sections]
425
+ const haystack = [
426
+ t.title,
427
+ t.summary,
428
+ t.description ?? '',
429
+ t.category,
430
+ t.style,
431
+ ...t.tags,
432
+ ...t.sections,
433
+ ]
415
434
  .join(' ')
416
435
  .toLowerCase();
417
436
  const score = terms.reduce((acc, term) => acc + (haystack.includes(term) ? 1 : 0), 0);
@@ -455,6 +474,200 @@ export function registerToolsWithProvider(server, getRegistry) {
455
474
  content: [{ type: 'text', text: formatTemplateFull(template) }],
456
475
  };
457
476
  });
477
+ 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.', {
478
+ project_tokens: z
479
+ .string()
480
+ .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"'),
481
+ framework: z
482
+ .enum(['tailwind', 'custom', 'bootstrap', 'chakra', 'shadcn'])
483
+ .optional()
484
+ .describe('CSS framework used in your project'),
485
+ }, async ({ project_tokens, framework }) => {
486
+ const lines = project_tokens
487
+ .split(/[;\n,]+/)
488
+ .map((s) => s.trim())
489
+ .filter(Boolean);
490
+ const mappings = [];
491
+ const TOKEN_HINTS = [
492
+ {
493
+ patterns: [/brand|primary|main|accent-color/i],
494
+ wsToken: '--ws-color-primary',
495
+ description: 'Primary/brand color',
496
+ },
497
+ {
498
+ patterns: [/brand.*hover|primary.*hover|primary.*dark/i],
499
+ wsToken: '--ws-color-primary-hover',
500
+ description: 'Primary hover state',
501
+ },
502
+ {
503
+ patterns: [/brand.*light|primary.*light|primary.*50|brand.*50/i],
504
+ wsToken: '--ws-color-primary-soft',
505
+ description: 'Primary soft background',
506
+ },
507
+ {
508
+ patterns: [/secondary|accent(?!-color)/i],
509
+ wsToken: '--ws-color-accent',
510
+ description: 'Secondary/accent color',
511
+ },
512
+ {
513
+ patterns: [/^bg$|background(?!.*secondary)|surface(?!.*alt)/i],
514
+ wsToken: '--ws-color-surface',
515
+ description: 'Page background',
516
+ },
517
+ {
518
+ patterns: [/bg.*secondary|bg.*subtle|bg.*muted|surface.*alt|bg.*light/i],
519
+ wsToken: '--ws-color-surface-alt',
520
+ description: 'Subtle background',
521
+ },
522
+ {
523
+ patterns: [/^text$|text.*primary|foreground(?!.*muted)/i],
524
+ wsToken: '--ws-color-text',
525
+ description: 'Primary text color',
526
+ },
527
+ {
528
+ patterns: [/text.*secondary|text.*soft|text.*muted|muted.*foreground/i],
529
+ wsToken: '--ws-color-text-muted',
530
+ description: 'Muted text color',
531
+ },
532
+ {
533
+ patterns: [/border(?!.*strong)|divider/i],
534
+ wsToken: '--ws-color-border',
535
+ description: 'Default border',
536
+ },
537
+ {
538
+ patterns: [/success|green|positive/i],
539
+ wsToken: '--ws-color-success',
540
+ description: 'Success color',
541
+ },
542
+ {
543
+ patterns: [/warning|amber|yellow|caution/i],
544
+ wsToken: '--ws-color-warning',
545
+ description: 'Warning color',
546
+ },
547
+ {
548
+ patterns: [/danger|error|red|destructive/i],
549
+ wsToken: '--ws-color-danger',
550
+ description: 'Danger/error color',
551
+ },
552
+ {
553
+ patterns: [/radius|rounded|corner/i],
554
+ wsToken: '--ws-radius-md',
555
+ description: 'Border radius',
556
+ },
557
+ ];
558
+ for (const line of lines) {
559
+ const [rawName] = line.split(':').map((s) => s.trim());
560
+ const name = rawName.replace(/^--color-|^--/, '');
561
+ for (const hint of TOKEN_HINTS) {
562
+ if (hint.patterns.some((p) => p.test(name))) {
563
+ const varRef = rawName.startsWith('--') ? `var(${rawName})` : rawName;
564
+ mappings.push(` ${hint.wsToken}: ${varRef}; /* ${hint.description} ← ${rawName} */`);
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ const frameworkNote = framework
570
+ ? `\nFramework detected: ${framework}. ${framework === 'tailwind'
571
+ ? 'Use @theme to register --ws-* tokens, or override in your main CSS.'
572
+ : framework === 'shadcn'
573
+ ? 'Map shadcn/ui --* variables to --ws-* in your globals.css.'
574
+ : 'Override --ws-* tokens in your global stylesheet.'}\n`
575
+ : '';
576
+ const css = mappings.length > 0
577
+ ? `:root {\n${mappings.join('\n')}\n}`
578
+ : '/* No matching tokens detected. Provide CSS custom properties like:\n --color-brand: #2563eb; --color-bg: #ffffff */';
579
+ return {
580
+ content: [
581
+ {
582
+ type: 'text',
583
+ 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`,
584
+ },
585
+ ],
586
+ };
587
+ });
588
+ 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.', {
589
+ components: z
590
+ .array(z.string())
591
+ .optional()
592
+ .describe('Only include component tokens for these families (e.g. ["hero", "cta", "pricing"]). If omitted, all component tokens are included.'),
593
+ }, async ({ components }) => {
594
+ const registry = await getRegistry();
595
+ // Read bundled CSS files
596
+ const { readFile } = await import('node:fs/promises');
597
+ const { dirname, resolve } = await import('node:path');
598
+ const { fileURLToPath } = await import('node:url');
599
+ const thisDir = dirname(fileURLToPath(import.meta.url));
600
+ let tokensCss;
601
+ let componentsCss;
602
+ try {
603
+ tokensCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-tokens.css'), 'utf-8');
604
+ componentsCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-components.css'), 'utf-8');
605
+ }
606
+ catch {
607
+ return {
608
+ content: [
609
+ {
610
+ type: 'text',
611
+ text: 'Token CSS files not found in package. Update @webspire/mcp to the latest version.',
612
+ },
613
+ ],
614
+ };
615
+ }
616
+ // Filter component tokens if specific families requested
617
+ if (components && components.length > 0) {
618
+ const filtered = ['/* Webspire Component Tokens (filtered) */'];
619
+ const sections = componentsCss.split(/(?=^\.ws-)/m);
620
+ for (const section of sections) {
621
+ const classMatch = section.match(/^\.ws-([a-z0-9-]+)/);
622
+ if (classMatch && components.includes(classMatch[1])) {
623
+ filtered.push(section.trim());
624
+ }
625
+ }
626
+ componentsCss = filtered.join('\n\n');
627
+ }
628
+ const families = components ? components.join(', ') : 'all';
629
+ const patternCount = registry.patterns?.length ?? 0;
630
+ return {
631
+ content: [
632
+ {
633
+ type: 'text',
634
+ text: [
635
+ `# WebSpire Token Setup (${families} families, ${patternCount} patterns)`,
636
+ '',
637
+ '## 1. Write `webspire-tokens.css` to your project',
638
+ '',
639
+ '```css',
640
+ tokensCss,
641
+ '```',
642
+ '',
643
+ '## 2. Write `webspire-components.css` to your project',
644
+ '',
645
+ '```css',
646
+ componentsCss,
647
+ '```',
648
+ '',
649
+ '## 3. Import in your main CSS',
650
+ '',
651
+ '```css',
652
+ '@import "./webspire-tokens.css";',
653
+ '@import "./webspire-components.css";',
654
+ '```',
655
+ '',
656
+ '## 4. Override to match your brand',
657
+ '',
658
+ '```css',
659
+ ':root {',
660
+ ' --ws-color-primary: #your-brand-color;',
661
+ ' --ws-color-primary-hover: #your-brand-darker;',
662
+ '}',
663
+ '```',
664
+ '',
665
+ 'Docs: https://webspire.de/tokens',
666
+ ].join('\n'),
667
+ },
668
+ ],
669
+ };
670
+ });
458
671
  }
459
672
  export function registerResourcesWithProvider(server, getRegistry) {
460
673
  server.resource('categories', 'webspire://categories', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webspire/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.6.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
@@ -1,3 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type RegistryOptions } from './registry.js';
3
- export declare function registerTools(server: McpServer, registryOptions?: RegistryOptions): void;
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
- }