@webspire/mcp 0.8.1 → 0.9.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 +11 -0
- package/data/fonts.json +582 -0
- package/data/registry.json +8939 -434
- package/dist/registration.d.ts +2 -2
- package/dist/registration.js +439 -304
- package/dist/registry.d.ts +5 -1
- package/dist/registry.js +17 -0
- package/dist/search.d.ts +11 -2
- package/dist/search.js +397 -266
- package/dist/types.d.ts +77 -0
- package/package.json +2 -2
package/dist/registration.js
CHANGED
|
@@ -1,146 +1,140 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { searchSnippets } from
|
|
3
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadFonts, searchSnippets } from './registry.js';
|
|
3
|
+
import { CONTENT_NEED_KEYS, CONTENT_NEED_LABEL, composePage, DOMAIN_KEYS, DOMAIN_LABEL, normalizeContentNeed, recommendFonts, searchCanvasEffects, searchPatterns, TONE_KEYS, TONE_LABEL, UX_GOAL_KEYS, UX_GOAL_LABEL, } from './search.js';
|
|
4
4
|
function formatSnippetBrief(s) {
|
|
5
5
|
return [
|
|
6
6
|
`**${s.title}** (${s.id})`,
|
|
7
7
|
` ${s.description}`,
|
|
8
|
-
` Category: ${s.category} | Tags: ${s.tags.join(
|
|
9
|
-
s.darkMode ?
|
|
10
|
-
s.responsive ?
|
|
11
|
-
s.meta.useCases.length > 0
|
|
12
|
-
? ` Use cases: ${s.meta.useCases.slice(0, 2).join("; ")}`
|
|
13
|
-
: "",
|
|
8
|
+
` Category: ${s.category} | Tags: ${s.tags.join(', ')}`,
|
|
9
|
+
s.darkMode ? ' Dark mode: yes' : '',
|
|
10
|
+
s.responsive ? ' Responsive: yes' : '',
|
|
11
|
+
s.meta.useCases.length > 0 ? ` Use cases: ${s.meta.useCases.slice(0, 2).join('; ')}` : '',
|
|
14
12
|
]
|
|
15
13
|
.filter(Boolean)
|
|
16
|
-
.join(
|
|
14
|
+
.join('\n');
|
|
17
15
|
}
|
|
18
16
|
function formatAccessibility(s) {
|
|
19
17
|
const a11y = s.meta.accessibility;
|
|
20
18
|
const features = [];
|
|
21
19
|
if (a11y.prefersReducedMotion)
|
|
22
|
-
features.push(
|
|
20
|
+
features.push('prefers-reduced-motion');
|
|
23
21
|
if (a11y.prefersColorScheme)
|
|
24
|
-
features.push(
|
|
22
|
+
features.push('prefers-color-scheme');
|
|
25
23
|
if (a11y.supportsCheck)
|
|
26
|
-
features.push(
|
|
24
|
+
features.push('@supports fallback');
|
|
27
25
|
if (a11y.ariaNotes?.length)
|
|
28
26
|
features.push(...a11y.ariaNotes);
|
|
29
|
-
return features.length > 0
|
|
30
|
-
? `## Accessibility\nRespects: ${features.join(", ")}`
|
|
31
|
-
: "";
|
|
27
|
+
return features.length > 0 ? `## Accessibility\nRespects: ${features.join(', ')}` : '';
|
|
32
28
|
}
|
|
33
29
|
function formatSnippetFull(s) {
|
|
34
30
|
const props = s.meta.customProperties
|
|
35
|
-
.map((p) => ` ${p}: ${s.meta.defaultValues?.[p] ??
|
|
36
|
-
.join(
|
|
31
|
+
.map((p) => ` ${p}: ${s.meta.defaultValues?.[p] ?? 'unset'}`)
|
|
32
|
+
.join('\n');
|
|
37
33
|
return [
|
|
38
34
|
`# ${s.title}`,
|
|
39
|
-
|
|
35
|
+
'',
|
|
40
36
|
s.description,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
'',
|
|
38
|
+
'## Usage',
|
|
39
|
+
'```html',
|
|
44
40
|
s.meta.usageExample,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
'```',
|
|
42
|
+
'',
|
|
43
|
+
'## CSS',
|
|
44
|
+
'```css',
|
|
49
45
|
s.css,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
s.meta.customProperties.length > 0 ? `## Custom Properties\n${props}` :
|
|
53
|
-
s.meta.classes.length > 0 ? `## Classes\n${s.meta.classes.join(
|
|
46
|
+
'```',
|
|
47
|
+
'',
|
|
48
|
+
s.meta.customProperties.length > 0 ? `## Custom Properties\n${props}` : '',
|
|
49
|
+
s.meta.classes.length > 0 ? `## Classes\n${s.meta.classes.join(', ')}` : '',
|
|
54
50
|
formatAccessibility(s),
|
|
55
|
-
s.dependencies?.length
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
s.variants?.length ? `## Variants\n${s.variants.join(", ")}` : "",
|
|
59
|
-
"",
|
|
51
|
+
s.dependencies?.length ? `## Dependencies\n${s.dependencies.join(', ')}` : '',
|
|
52
|
+
s.variants?.length ? `## Variants\n${s.variants.join(', ')}` : '',
|
|
53
|
+
'',
|
|
60
54
|
`Browser support: ${s.meta.browser}`,
|
|
61
55
|
`Size: ${s.meta.lines} lines, ${s.meta.bytes} bytes`,
|
|
62
|
-
s.darkMode ?
|
|
63
|
-
s.responsive ?
|
|
56
|
+
s.darkMode ? 'Dark mode: supported' : '',
|
|
57
|
+
s.responsive ? 'Responsive: yes' : '',
|
|
64
58
|
]
|
|
65
|
-
.filter((line) => line !==
|
|
66
|
-
.join(
|
|
59
|
+
.filter((line) => line !== '')
|
|
60
|
+
.join('\n');
|
|
67
61
|
}
|
|
68
62
|
function formatTemplateBrief(t) {
|
|
69
63
|
return [
|
|
70
64
|
`**${t.title}** (${t.id})`,
|
|
71
65
|
` ${t.summary}`,
|
|
72
66
|
` Category: ${t.category} | Style: ${t.style}`,
|
|
73
|
-
t.tags.length > 0 ? ` Tags: ${t.tags.join(
|
|
67
|
+
t.tags.length > 0 ? ` Tags: ${t.tags.join(', ')}` : '',
|
|
74
68
|
]
|
|
75
69
|
.filter(Boolean)
|
|
76
|
-
.join(
|
|
70
|
+
.join('\n');
|
|
77
71
|
}
|
|
78
72
|
function formatTemplateFull(t) {
|
|
79
73
|
return [
|
|
80
74
|
`# ${t.title}`,
|
|
81
|
-
|
|
75
|
+
'',
|
|
82
76
|
t.description ?? t.summary,
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
'',
|
|
78
|
+
'## Identity',
|
|
85
79
|
`ID: ${t.id}`,
|
|
86
80
|
`Category: ${t.category}`,
|
|
87
81
|
`Style: ${t.style}`,
|
|
88
|
-
|
|
89
|
-
t.tags.length > 0 ? `## Tags\n${t.tags.join(
|
|
90
|
-
t.sections.length > 0 ? `## Sections\n${t.sections.join(
|
|
91
|
-
t.patterns.length > 0 ? `## Uses Patterns\n${t.patterns.join(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
'',
|
|
83
|
+
t.tags.length > 0 ? `## Tags\n${t.tags.join(', ')}` : '',
|
|
84
|
+
t.sections.length > 0 ? `## Sections\n${t.sections.join(', ')}` : '',
|
|
85
|
+
t.patterns.length > 0 ? `## Uses Patterns\n${t.patterns.join(', ')}` : '',
|
|
86
|
+
'',
|
|
87
|
+
'## HTML',
|
|
88
|
+
'```html',
|
|
95
89
|
t.html,
|
|
96
|
-
|
|
90
|
+
'```',
|
|
97
91
|
]
|
|
98
|
-
.filter((line) => line !==
|
|
99
|
-
.join(
|
|
92
|
+
.filter((line) => line !== '')
|
|
93
|
+
.join('\n');
|
|
100
94
|
}
|
|
101
95
|
function formatPatternBrief(p) {
|
|
102
96
|
return [
|
|
103
97
|
`**${p.title}** (${p.id})`,
|
|
104
98
|
` ${p.summary}`,
|
|
105
99
|
` Family: ${p.family} | Tier: ${p.tier} | Kind: ${p.kind}`,
|
|
106
|
-
p.tags.length > 0 ? ` Tags: ${p.tags.join(
|
|
107
|
-
p.extends ? ` Extends: ${p.extends}` :
|
|
100
|
+
p.tags.length > 0 ? ` Tags: ${p.tags.join(', ')}` : '',
|
|
101
|
+
p.extends ? ` Extends: ${p.extends}` : '',
|
|
108
102
|
]
|
|
109
103
|
.filter(Boolean)
|
|
110
|
-
.join(
|
|
104
|
+
.join('\n');
|
|
111
105
|
}
|
|
112
106
|
function formatPatternFull(p) {
|
|
113
107
|
return [
|
|
114
108
|
`# ${p.title}`,
|
|
115
|
-
|
|
109
|
+
'',
|
|
116
110
|
p.description ?? p.summary,
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
'',
|
|
112
|
+
'## Identity',
|
|
119
113
|
`ID: ${p.id}`,
|
|
120
114
|
`Family: ${p.family}`,
|
|
121
115
|
`Tier: ${p.tier}`,
|
|
122
116
|
`Kind: ${p.kind}`,
|
|
123
|
-
p.extends ? `Extends: ${p.extends}` :
|
|
124
|
-
|
|
125
|
-
p.tags.length > 0 ? `## Tags\n${p.tags.join(
|
|
117
|
+
p.extends ? `Extends: ${p.extends}` : '',
|
|
118
|
+
'',
|
|
119
|
+
p.tags.length > 0 ? `## Tags\n${p.tags.join(', ')}` : '',
|
|
126
120
|
p.slots.length > 0
|
|
127
|
-
? `## Slots\n${p.slots.map((s) => `- ${s.name}${s.required ?
|
|
128
|
-
:
|
|
121
|
+
? `## Slots\n${p.slots.map((s) => `- ${s.name}${s.required ? ' (required)' : ''}: ${s.description}`).join('\n')}`
|
|
122
|
+
: '',
|
|
129
123
|
p.props.length > 0
|
|
130
|
-
? `## Props\n${p.props.map((prop) => `- ${prop.name} (${prop.type}) default=${String(prop.default)}`).join(
|
|
131
|
-
:
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
? `## Props\n${p.props.map((prop) => `- ${prop.name} (${prop.type}) default=${String(prop.default)}`).join('\n')}`
|
|
125
|
+
: '',
|
|
126
|
+
'## HTML',
|
|
127
|
+
'```html',
|
|
134
128
|
p.html,
|
|
135
|
-
|
|
136
|
-
p.css ? `## CSS\n\`\`\`css\n${p.css}\n\`\`\`` :
|
|
137
|
-
p.js ? `## JS\n\`\`\`js\n${p.js}\n\`\`\`` :
|
|
129
|
+
'```',
|
|
130
|
+
p.css ? `## CSS\n\`\`\`css\n${p.css}\n\`\`\`` : '',
|
|
131
|
+
p.js ? `## JS\n\`\`\`js\n${p.js}\n\`\`\`` : '',
|
|
138
132
|
]
|
|
139
|
-
.filter((line) => line !==
|
|
140
|
-
.join(
|
|
133
|
+
.filter((line) => line !== '')
|
|
134
|
+
.join('\n');
|
|
141
135
|
}
|
|
142
136
|
export function registerToolsWithProvider(server, getRegistry) {
|
|
143
|
-
server.tool(
|
|
137
|
+
server.tool('list_categories', 'List all available snippet categories with snippet counts', {}, async () => {
|
|
144
138
|
const registry = await getRegistry();
|
|
145
139
|
const counts = {};
|
|
146
140
|
for (const snippet of registry.snippets) {
|
|
@@ -148,31 +142,28 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
148
142
|
}
|
|
149
143
|
const result = Object.entries(counts)
|
|
150
144
|
.sort(([, a], [, b]) => b - a)
|
|
151
|
-
.map(([cat, count]) => `${cat}: ${count} snippet${count > 1 ?
|
|
152
|
-
.join(
|
|
145
|
+
.map(([cat, count]) => `${cat}: ${count} snippet${count > 1 ? 's' : ''}`)
|
|
146
|
+
.join('\n');
|
|
153
147
|
return {
|
|
154
148
|
content: [
|
|
155
149
|
{
|
|
156
|
-
type:
|
|
150
|
+
type: 'text',
|
|
157
151
|
text: `${registry.snippets.length} snippets across ${Object.keys(counts).length} categories:\n\n${result}`,
|
|
158
152
|
},
|
|
159
153
|
],
|
|
160
154
|
};
|
|
161
155
|
});
|
|
162
|
-
server.tool(
|
|
156
|
+
server.tool('search_snippets', 'Search Webspire CSS snippets by keyword, problem description, or use case. Returns matching snippets with their descriptions and IDs.', {
|
|
163
157
|
query: z
|
|
164
158
|
.string()
|
|
165
159
|
.describe('Search query: problem description, use case, or CSS technique (e.g. "glass card", "fade animation", "blur entrance")'),
|
|
166
160
|
category: z
|
|
167
161
|
.string()
|
|
168
162
|
.optional()
|
|
169
|
-
.describe(
|
|
170
|
-
tags: z.array(z.string()).optional().describe(
|
|
171
|
-
darkMode: z.boolean().optional().describe(
|
|
172
|
-
responsive: z
|
|
173
|
-
.boolean()
|
|
174
|
-
.optional()
|
|
175
|
-
.describe("Filter for responsive support"),
|
|
163
|
+
.describe('Filter by category: glass, animations, easing, scroll, decorative, interactions, text'),
|
|
164
|
+
tags: z.array(z.string()).optional().describe('Filter by tags'),
|
|
165
|
+
darkMode: z.boolean().optional().describe('Filter for dark mode support'),
|
|
166
|
+
responsive: z.boolean().optional().describe('Filter for responsive support'),
|
|
176
167
|
}, async ({ query, category, tags, darkMode, responsive }) => {
|
|
177
168
|
const registry = await getRegistry();
|
|
178
169
|
const results = searchSnippets(registry, {
|
|
@@ -186,23 +177,23 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
186
177
|
return {
|
|
187
178
|
content: [
|
|
188
179
|
{
|
|
189
|
-
type:
|
|
190
|
-
text:
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: 'No snippets found matching your query. Try broader keywords or check available categories with list_categories.',
|
|
191
182
|
},
|
|
192
183
|
],
|
|
193
184
|
};
|
|
194
185
|
}
|
|
195
|
-
const text = results.map(formatSnippetBrief).join(
|
|
186
|
+
const text = results.map(formatSnippetBrief).join('\n\n');
|
|
196
187
|
return {
|
|
197
188
|
content: [
|
|
198
189
|
{
|
|
199
|
-
type:
|
|
200
|
-
text: `Found ${results.length} snippet${results.length > 1 ?
|
|
190
|
+
type: 'text',
|
|
191
|
+
text: `Found ${results.length} snippet${results.length > 1 ? 's' : ''}:\n\n${text}`,
|
|
201
192
|
},
|
|
202
193
|
],
|
|
203
194
|
};
|
|
204
195
|
});
|
|
205
|
-
server.tool(
|
|
196
|
+
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 recommend_snippet.', {
|
|
206
197
|
id: z
|
|
207
198
|
.string()
|
|
208
199
|
.describe('Snippet ID, e.g. "glass/frosted", "animations/fade-in", "easing/tokens"'),
|
|
@@ -210,41 +201,38 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
210
201
|
const registry = await getRegistry();
|
|
211
202
|
const snippet = registry.snippets.find((s) => s.id === id);
|
|
212
203
|
if (!snippet) {
|
|
213
|
-
const available = registry.snippets.map((s) => s.id).join(
|
|
204
|
+
const available = registry.snippets.map((s) => s.id).join(', ');
|
|
214
205
|
return {
|
|
215
206
|
content: [
|
|
216
207
|
{
|
|
217
|
-
type:
|
|
208
|
+
type: 'text',
|
|
218
209
|
text: `Snippet "${id}" not found. Available snippets: ${available}`,
|
|
219
210
|
},
|
|
220
211
|
],
|
|
221
212
|
};
|
|
222
213
|
}
|
|
223
214
|
return {
|
|
224
|
-
content: [{ type:
|
|
215
|
+
content: [{ type: 'text', text: formatSnippetFull(snippet) }],
|
|
225
216
|
};
|
|
226
217
|
});
|
|
227
|
-
server.tool(
|
|
218
|
+
server.tool('recommend_snippet', 'Describe a UI problem or desired effect and get the best matching Webspire snippets with reasoning. Supports optional constraints like dark mode or reduced motion requirements.', {
|
|
228
219
|
use_case: z
|
|
229
220
|
.string()
|
|
230
221
|
.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")'),
|
|
231
|
-
category: z
|
|
232
|
-
.string()
|
|
233
|
-
.optional()
|
|
234
|
-
.describe("Limit recommendations to a specific category"),
|
|
222
|
+
category: z.string().optional().describe('Limit recommendations to a specific category'),
|
|
235
223
|
needs_dark_mode: z
|
|
236
224
|
.boolean()
|
|
237
225
|
.optional()
|
|
238
|
-
.describe(
|
|
226
|
+
.describe('Only recommend snippets with dark mode support'),
|
|
239
227
|
needs_responsive: z
|
|
240
228
|
.boolean()
|
|
241
229
|
.optional()
|
|
242
|
-
.describe(
|
|
230
|
+
.describe('Only recommend snippets with responsive support'),
|
|
243
231
|
needs_reduced_motion: z
|
|
244
232
|
.boolean()
|
|
245
233
|
.optional()
|
|
246
|
-
.describe(
|
|
247
|
-
}, async ({ use_case, category, needs_dark_mode, needs_responsive, needs_reduced_motion
|
|
234
|
+
.describe('Only recommend snippets that respect prefers-reduced-motion'),
|
|
235
|
+
}, async ({ use_case, category, needs_dark_mode, needs_responsive, needs_reduced_motion }) => {
|
|
248
236
|
const registry = await getRegistry();
|
|
249
237
|
let filteredRegistry = registry;
|
|
250
238
|
if (needs_reduced_motion) {
|
|
@@ -264,8 +252,8 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
264
252
|
return {
|
|
265
253
|
content: [
|
|
266
254
|
{
|
|
267
|
-
type:
|
|
268
|
-
text:
|
|
255
|
+
type: 'text',
|
|
256
|
+
text: 'No matching snippet found for your description. Try rephrasing or relax the constraints.',
|
|
269
257
|
},
|
|
270
258
|
],
|
|
271
259
|
};
|
|
@@ -274,25 +262,25 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
274
262
|
.map((s, i) => {
|
|
275
263
|
const reasons = [];
|
|
276
264
|
if (s.darkMode)
|
|
277
|
-
reasons.push(
|
|
265
|
+
reasons.push('dark mode');
|
|
278
266
|
if (s.meta.accessibility.prefersReducedMotion)
|
|
279
|
-
reasons.push(
|
|
267
|
+
reasons.push('reduced motion');
|
|
280
268
|
if (s.meta.accessibility.supportsCheck)
|
|
281
|
-
reasons.push(
|
|
282
|
-
const reasonStr = reasons.length > 0 ? ` (${reasons.join(
|
|
269
|
+
reasons.push('@supports fallback');
|
|
270
|
+
const reasonStr = reasons.length > 0 ? ` (${reasons.join(', ')})` : '';
|
|
283
271
|
return `${i + 1}. ${formatSnippetBrief(s)}${reasonStr}\n Usage: \`${s.meta.usageExample}\``;
|
|
284
272
|
})
|
|
285
|
-
.join(
|
|
273
|
+
.join('\n\n');
|
|
286
274
|
return {
|
|
287
275
|
content: [
|
|
288
276
|
{
|
|
289
|
-
type:
|
|
290
|
-
text: `Top ${results.length} recommendation${results.length > 1 ?
|
|
277
|
+
type: 'text',
|
|
278
|
+
text: `Top ${results.length} recommendation${results.length > 1 ? 's' : ''} for "${use_case}":\n\n${text}\n\nUse get_snippet(id) for full CSS and details.`,
|
|
291
279
|
},
|
|
292
280
|
],
|
|
293
281
|
};
|
|
294
282
|
});
|
|
295
|
-
server.tool(
|
|
283
|
+
server.tool('list_pattern_families', 'List all available pattern families with counts', {}, async () => {
|
|
296
284
|
const registry = await getRegistry();
|
|
297
285
|
const patterns = registry.patterns ?? [];
|
|
298
286
|
const counts = {};
|
|
@@ -301,34 +289,34 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
301
289
|
}
|
|
302
290
|
const result = Object.entries(counts)
|
|
303
291
|
.sort(([, a], [, b]) => b - a)
|
|
304
|
-
.map(([family, count]) => `${family}: ${count} pattern${count > 1 ?
|
|
305
|
-
.join(
|
|
292
|
+
.map(([family, count]) => `${family}: ${count} pattern${count > 1 ? 's' : ''}`)
|
|
293
|
+
.join('\n');
|
|
306
294
|
return {
|
|
307
295
|
content: [
|
|
308
296
|
{
|
|
309
|
-
type:
|
|
297
|
+
type: 'text',
|
|
310
298
|
text: `${patterns.length} patterns across ${Object.keys(counts).length} families:\n\n${result}`,
|
|
311
299
|
},
|
|
312
300
|
],
|
|
313
301
|
};
|
|
314
302
|
});
|
|
315
|
-
server.tool(
|
|
303
|
+
server.tool('search_patterns', 'Search Webspire UI patterns by intent, family, tier, domain, tone, or UX goal. Uses weighted scoring with synonym expansion for better results.', {
|
|
316
304
|
query: z
|
|
317
305
|
.string()
|
|
318
306
|
.describe('Search query, e.g. "hero with image", "pricing section", "trust building for legal"'),
|
|
319
307
|
family: z
|
|
320
308
|
.string()
|
|
321
309
|
.optional()
|
|
322
|
-
.describe(
|
|
323
|
-
tier: z.enum([
|
|
310
|
+
.describe('Filter by pattern family, e.g. hero, pricing, navbar'),
|
|
311
|
+
tier: z.enum(['base', 'enhanced']).optional().describe('Filter by tier'),
|
|
324
312
|
domain: z
|
|
325
313
|
.string()
|
|
326
314
|
.optional()
|
|
327
|
-
.describe(
|
|
315
|
+
.describe('Filter/boost by business domain: legal, healthcare, education, finance, saas, ecommerce, agency, consulting, nonprofit, gastronomy, realestate, personal'),
|
|
328
316
|
tone: z
|
|
329
317
|
.string()
|
|
330
318
|
.optional()
|
|
331
|
-
.describe(
|
|
319
|
+
.describe('Filter/boost by tone: serious, premium, modern, friendly, approachable, approachable_professional, technical, editorial, playful, institutional, industrial, casual'),
|
|
332
320
|
ux_goal: z
|
|
333
321
|
.enum(UX_GOAL_KEYS)
|
|
334
322
|
.optional()
|
|
@@ -347,8 +335,8 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
347
335
|
return {
|
|
348
336
|
content: [
|
|
349
337
|
{
|
|
350
|
-
type:
|
|
351
|
-
text:
|
|
338
|
+
type: 'text',
|
|
339
|
+
text: 'No patterns found matching your query.',
|
|
352
340
|
},
|
|
353
341
|
],
|
|
354
342
|
};
|
|
@@ -356,18 +344,16 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
356
344
|
return {
|
|
357
345
|
content: [
|
|
358
346
|
{
|
|
359
|
-
type:
|
|
360
|
-
text: `Found ${results.length} pattern${results.length > 1 ?
|
|
347
|
+
type: 'text',
|
|
348
|
+
text: `Found ${results.length} pattern${results.length > 1 ? 's' : ''}:\n\n${results.map(formatPatternBrief).join('\n\n')}`,
|
|
361
349
|
},
|
|
362
350
|
],
|
|
363
351
|
};
|
|
364
352
|
});
|
|
365
|
-
server.tool(
|
|
353
|
+
server.tool('compose_page', 'Compose an optimal page structure by selecting and ordering patterns based on business domain, tone, and UX goals. Also recommends matching CSS snippets for polish and interaction.', {
|
|
366
354
|
domain: z.enum(DOMAIN_KEYS).describe(`Business domain: ${DOMAIN_LABEL}`),
|
|
367
355
|
tone: z.enum(TONE_KEYS).describe(`Desired tone: ${TONE_LABEL}`),
|
|
368
|
-
ux_goals: z
|
|
369
|
-
.array(z.enum(UX_GOAL_KEYS))
|
|
370
|
-
.describe(`UX goals: ${UX_GOAL_LABEL}`),
|
|
356
|
+
ux_goals: z.array(z.enum(UX_GOAL_KEYS)).describe(`UX goals: ${UX_GOAL_LABEL}`),
|
|
371
357
|
content_needs: z
|
|
372
358
|
.array(z.enum(CONTENT_NEED_KEYS))
|
|
373
359
|
.optional()
|
|
@@ -376,7 +362,7 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
376
362
|
.number()
|
|
377
363
|
.optional()
|
|
378
364
|
.default(8)
|
|
379
|
-
.describe(
|
|
365
|
+
.describe('Maximum number of sections to include (default: 8)'),
|
|
380
366
|
}, async ({ domain, tone, ux_goals, content_needs, max_sections }) => {
|
|
381
367
|
const registry = await getRegistry();
|
|
382
368
|
const allPatterns = registry.patterns ?? [];
|
|
@@ -393,7 +379,7 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
393
379
|
return {
|
|
394
380
|
content: [
|
|
395
381
|
{
|
|
396
|
-
type:
|
|
382
|
+
type: 'text',
|
|
397
383
|
text: `No suitable patterns found for domain="${domain}", tone="${tone}". Try broader criteria.`,
|
|
398
384
|
},
|
|
399
385
|
],
|
|
@@ -401,43 +387,47 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
401
387
|
}
|
|
402
388
|
const sections = result.patterns
|
|
403
389
|
.map((p, i) => {
|
|
404
|
-
const role = p.ai?.compositionRole ??
|
|
390
|
+
const role = p.ai?.compositionRole ?? 'supporting';
|
|
405
391
|
return `${i + 1}. **${p.title}** (${p.id}) — role: ${role}\n ${p.summary}`;
|
|
406
392
|
})
|
|
407
|
-
.join(
|
|
393
|
+
.join('\n\n');
|
|
408
394
|
const reasoningText = result.reasoning.length > 0
|
|
409
|
-
? `\n\n## Selection Reasoning\n${result.reasoning.join(
|
|
410
|
-
:
|
|
411
|
-
const warningsText = result.warnings.length > 0
|
|
412
|
-
? `\n\n## Warnings\n${result.warnings.join("\n")}`
|
|
413
|
-
: "";
|
|
395
|
+
? `\n\n## Selection Reasoning\n${result.reasoning.join('\n')}`
|
|
396
|
+
: '';
|
|
397
|
+
const warningsText = result.warnings.length > 0 ? `\n\n## Warnings\n${result.warnings.join('\n')}` : '';
|
|
414
398
|
const snippetsText = result.snippets.length > 0
|
|
415
399
|
? `\n\n## Recommended Snippets\n${result.snippets
|
|
416
400
|
.map(({ snippet, reason }, i) => `${i + 1}. **${snippet.title}** (${snippet.id}) — ${reason}`)
|
|
417
|
-
.join(
|
|
418
|
-
:
|
|
401
|
+
.join('\n')}\n\nUse \`get_snippet(id)\` to retrieve the CSS for any snippet.`
|
|
402
|
+
: '';
|
|
403
|
+
// Font recommendation
|
|
404
|
+
let fontsText = '';
|
|
405
|
+
const fontsDataLoaded = await loadFonts();
|
|
406
|
+
if (fontsDataLoaded) {
|
|
407
|
+
const fontRec = recommendFonts(fontsDataLoaded, domain, tone);
|
|
408
|
+
const predNote = fontRec.isPredefined ? ' (predefined pairing)' : '';
|
|
409
|
+
fontsText = `\n\n## Recommended Fonts${predNote}\n- **Heading:** ${fontRec.heading.name} (\`${fontRec.heading.npm}\`)\n- **Body:** ${fontRec.body.name} (\`${fontRec.body.npm}\`)\n- **Mono:** ${fontRec.mono.name} (\`${fontRec.mono.npm}\`)\n\n\`\`\`bash\n${fontRec.install}\n\`\`\`\n\n\`\`\`css\n${fontRec.tailwindTheme}\n\`\`\`\n\nUse \`recommend_fonts\` for detailed reasoning.`;
|
|
410
|
+
}
|
|
419
411
|
return {
|
|
420
412
|
content: [
|
|
421
413
|
{
|
|
422
|
-
type:
|
|
423
|
-
text: `# Composed Page: ${domain} / ${tone}\n\nUX Goals: ${ux_goals.join(
|
|
414
|
+
type: 'text',
|
|
415
|
+
text: `# Composed Page: ${domain} / ${tone}\n\nUX Goals: ${ux_goals.join(', ')}${content_needs?.length ? `\nContent Needs: ${content_needs.join(', ')}` : ''}\n\n## Recommended Sections\n\n${sections}${snippetsText}${fontsText}${warningsText}${reasoningText}\n\nUse \`get_pattern(id)\` to get full HTML for each section.`,
|
|
424
416
|
},
|
|
425
417
|
],
|
|
426
418
|
};
|
|
427
419
|
});
|
|
428
|
-
server.tool(
|
|
429
|
-
id: z
|
|
430
|
-
.string()
|
|
431
|
-
.describe('Pattern ID, e.g. "hero/base", "hero/with-image"'),
|
|
420
|
+
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.', {
|
|
421
|
+
id: z.string().describe('Pattern ID, e.g. "hero/base", "hero/with-image"'),
|
|
432
422
|
}, async ({ id }) => {
|
|
433
423
|
const registry = await getRegistry();
|
|
434
424
|
const pattern = (registry.patterns ?? []).find((p) => p.id === id);
|
|
435
425
|
if (!pattern) {
|
|
436
|
-
const available = (registry.patterns ?? []).map((p) => p.id).join(
|
|
426
|
+
const available = (registry.patterns ?? []).map((p) => p.id).join(', ');
|
|
437
427
|
return {
|
|
438
428
|
content: [
|
|
439
429
|
{
|
|
440
|
-
type:
|
|
430
|
+
type: 'text',
|
|
441
431
|
text: `Pattern "${id}" not found. Available patterns: ${available}`,
|
|
442
432
|
},
|
|
443
433
|
],
|
|
@@ -446,13 +436,13 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
446
436
|
return {
|
|
447
437
|
content: [
|
|
448
438
|
{
|
|
449
|
-
type:
|
|
439
|
+
type: 'text',
|
|
450
440
|
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`,
|
|
451
441
|
},
|
|
452
442
|
],
|
|
453
443
|
};
|
|
454
444
|
});
|
|
455
|
-
server.tool(
|
|
445
|
+
server.tool('list_templates', 'List all available page templates grouped by category', {}, async () => {
|
|
456
446
|
const registry = await getRegistry();
|
|
457
447
|
const templates = registry.templates ?? [];
|
|
458
448
|
const counts = {};
|
|
@@ -461,29 +451,27 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
461
451
|
}
|
|
462
452
|
const result = Object.entries(counts)
|
|
463
453
|
.sort(([, a], [, b]) => b - a)
|
|
464
|
-
.map(([cat, count]) => `${cat}: ${count} template${count > 1 ?
|
|
465
|
-
.join(
|
|
454
|
+
.map(([cat, count]) => `${cat}: ${count} template${count > 1 ? 's' : ''}`)
|
|
455
|
+
.join('\n');
|
|
466
456
|
return {
|
|
467
457
|
content: [
|
|
468
458
|
{
|
|
469
|
-
type:
|
|
459
|
+
type: 'text',
|
|
470
460
|
text: `${templates.length} templates across ${Object.keys(counts).length} categories:\n\n${result}`,
|
|
471
461
|
},
|
|
472
462
|
],
|
|
473
463
|
};
|
|
474
464
|
});
|
|
475
|
-
server.tool(
|
|
476
|
-
query: z
|
|
477
|
-
.string()
|
|
478
|
-
.describe('Search query, e.g. "saas landing", "portfolio dark", "shop"'),
|
|
465
|
+
server.tool('search_templates', 'Search Webspire page templates by keyword, category, or style.', {
|
|
466
|
+
query: z.string().describe('Search query, e.g. "saas landing", "portfolio dark", "shop"'),
|
|
479
467
|
category: z
|
|
480
468
|
.string()
|
|
481
469
|
.optional()
|
|
482
|
-
.describe(
|
|
470
|
+
.describe('Filter by category, e.g. saas-landing, agency, shop'),
|
|
483
471
|
style: z
|
|
484
472
|
.string()
|
|
485
473
|
.optional()
|
|
486
|
-
.describe(
|
|
474
|
+
.describe('Filter by style, e.g. modern, bold, minimal, corporate'),
|
|
487
475
|
}, async ({ query, category, style }) => {
|
|
488
476
|
const registry = await getRegistry();
|
|
489
477
|
let templates = registry.templates ?? [];
|
|
@@ -497,13 +485,13 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
497
485
|
const haystack = [
|
|
498
486
|
t.title,
|
|
499
487
|
t.summary,
|
|
500
|
-
t.description ??
|
|
488
|
+
t.description ?? '',
|
|
501
489
|
t.category,
|
|
502
490
|
t.style,
|
|
503
491
|
...t.tags,
|
|
504
492
|
...t.sections,
|
|
505
493
|
]
|
|
506
|
-
.join(
|
|
494
|
+
.join(' ')
|
|
507
495
|
.toLowerCase();
|
|
508
496
|
const score = terms.reduce((acc, term) => acc + (haystack.includes(term) ? 1 : 0), 0);
|
|
509
497
|
return { template: t, score };
|
|
@@ -516,8 +504,8 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
516
504
|
return {
|
|
517
505
|
content: [
|
|
518
506
|
{
|
|
519
|
-
type:
|
|
520
|
-
text:
|
|
507
|
+
type: 'text',
|
|
508
|
+
text: 'No templates found matching your query.',
|
|
521
509
|
},
|
|
522
510
|
],
|
|
523
511
|
};
|
|
@@ -525,46 +513,40 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
525
513
|
return {
|
|
526
514
|
content: [
|
|
527
515
|
{
|
|
528
|
-
type:
|
|
529
|
-
text: `Found ${results.length} template${results.length > 1 ?
|
|
516
|
+
type: 'text',
|
|
517
|
+
text: `Found ${results.length} template${results.length > 1 ? 's' : ''}:\n\n${results.map(formatTemplateBrief).join('\n\n')}`,
|
|
530
518
|
},
|
|
531
519
|
],
|
|
532
520
|
};
|
|
533
521
|
});
|
|
534
|
-
server.tool(
|
|
535
|
-
id: z
|
|
536
|
-
.string()
|
|
537
|
-
.describe('Template ID, e.g. "saas-landing/modern", "shop/catalog"'),
|
|
522
|
+
server.tool('get_template', 'Get full template HTML for a specific page template. Returns standalone HTML ready to use.', {
|
|
523
|
+
id: z.string().describe('Template ID, e.g. "saas-landing/modern", "shop/catalog"'),
|
|
538
524
|
}, async ({ id }) => {
|
|
539
525
|
const registry = await getRegistry();
|
|
540
526
|
const template = (registry.templates ?? []).find((t) => t.id === id);
|
|
541
527
|
if (!template) {
|
|
542
|
-
const available = (registry.templates ?? [])
|
|
543
|
-
.map((t) => t.id)
|
|
544
|
-
.join(", ");
|
|
528
|
+
const available = (registry.templates ?? []).map((t) => t.id).join(', ');
|
|
545
529
|
return {
|
|
546
530
|
content: [
|
|
547
531
|
{
|
|
548
|
-
type:
|
|
532
|
+
type: 'text',
|
|
549
533
|
text: `Template "${id}" not found. Available templates: ${available}`,
|
|
550
534
|
},
|
|
551
535
|
],
|
|
552
536
|
};
|
|
553
537
|
}
|
|
554
538
|
return {
|
|
555
|
-
content: [
|
|
556
|
-
{ type: "text", text: formatTemplateFull(template) },
|
|
557
|
-
],
|
|
539
|
+
content: [{ type: 'text', text: formatTemplateFull(template) }],
|
|
558
540
|
};
|
|
559
541
|
});
|
|
560
|
-
server.tool(
|
|
542
|
+
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.', {
|
|
561
543
|
project_tokens: z
|
|
562
544
|
.string()
|
|
563
545
|
.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"'),
|
|
564
546
|
framework: z
|
|
565
|
-
.enum([
|
|
547
|
+
.enum(['tailwind', 'custom', 'bootstrap', 'chakra', 'shadcn'])
|
|
566
548
|
.optional()
|
|
567
|
-
.describe(
|
|
549
|
+
.describe('CSS framework used in your project'),
|
|
568
550
|
}, async ({ project_tokens, framework }) => {
|
|
569
551
|
const lines = project_tokens
|
|
570
552
|
.split(/[;\n,]+/)
|
|
@@ -574,107 +556,101 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
574
556
|
const TOKEN_HINTS = [
|
|
575
557
|
{
|
|
576
558
|
patterns: [/brand|primary|main|accent-color/i],
|
|
577
|
-
wsToken:
|
|
578
|
-
description:
|
|
559
|
+
wsToken: '--ws-color-primary',
|
|
560
|
+
description: 'Primary/brand color',
|
|
579
561
|
},
|
|
580
562
|
{
|
|
581
563
|
patterns: [/brand.*hover|primary.*hover|primary.*dark/i],
|
|
582
|
-
wsToken:
|
|
583
|
-
description:
|
|
564
|
+
wsToken: '--ws-color-primary-hover',
|
|
565
|
+
description: 'Primary hover state',
|
|
584
566
|
},
|
|
585
567
|
{
|
|
586
568
|
patterns: [/brand.*light|primary.*light|primary.*50|brand.*50/i],
|
|
587
|
-
wsToken:
|
|
588
|
-
description:
|
|
569
|
+
wsToken: '--ws-color-primary-soft',
|
|
570
|
+
description: 'Primary soft background',
|
|
589
571
|
},
|
|
590
572
|
{
|
|
591
573
|
patterns: [/secondary|accent(?!-color)/i],
|
|
592
|
-
wsToken:
|
|
593
|
-
description:
|
|
574
|
+
wsToken: '--ws-color-accent',
|
|
575
|
+
description: 'Secondary/accent color',
|
|
594
576
|
},
|
|
595
577
|
{
|
|
596
578
|
patterns: [/^bg$|background(?!.*secondary)|surface(?!.*alt)/i],
|
|
597
|
-
wsToken:
|
|
598
|
-
description:
|
|
579
|
+
wsToken: '--ws-color-surface',
|
|
580
|
+
description: 'Page background',
|
|
599
581
|
},
|
|
600
582
|
{
|
|
601
|
-
patterns: [
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
wsToken: "--ws-color-surface-alt",
|
|
605
|
-
description: "Subtle background",
|
|
583
|
+
patterns: [/bg.*secondary|bg.*subtle|bg.*muted|surface.*alt|bg.*light/i],
|
|
584
|
+
wsToken: '--ws-color-surface-alt',
|
|
585
|
+
description: 'Subtle background',
|
|
606
586
|
},
|
|
607
587
|
{
|
|
608
588
|
patterns: [/^text$|text.*primary|foreground(?!.*muted)/i],
|
|
609
|
-
wsToken:
|
|
610
|
-
description:
|
|
589
|
+
wsToken: '--ws-color-text',
|
|
590
|
+
description: 'Primary text color',
|
|
611
591
|
},
|
|
612
592
|
{
|
|
613
|
-
patterns: [
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
wsToken: "--ws-color-text-muted",
|
|
617
|
-
description: "Muted text color",
|
|
593
|
+
patterns: [/text.*secondary|text.*soft|text.*muted|muted.*foreground/i],
|
|
594
|
+
wsToken: '--ws-color-text-muted',
|
|
595
|
+
description: 'Muted text color',
|
|
618
596
|
},
|
|
619
597
|
{
|
|
620
598
|
patterns: [/border(?!.*strong)|divider/i],
|
|
621
|
-
wsToken:
|
|
622
|
-
description:
|
|
599
|
+
wsToken: '--ws-color-border',
|
|
600
|
+
description: 'Default border',
|
|
623
601
|
},
|
|
624
602
|
{
|
|
625
603
|
patterns: [/success|green|positive/i],
|
|
626
|
-
wsToken:
|
|
627
|
-
description:
|
|
604
|
+
wsToken: '--ws-color-success',
|
|
605
|
+
description: 'Success color',
|
|
628
606
|
},
|
|
629
607
|
{
|
|
630
608
|
patterns: [/warning|amber|yellow|caution/i],
|
|
631
|
-
wsToken:
|
|
632
|
-
description:
|
|
609
|
+
wsToken: '--ws-color-warning',
|
|
610
|
+
description: 'Warning color',
|
|
633
611
|
},
|
|
634
612
|
{
|
|
635
613
|
patterns: [/danger|error|red|destructive/i],
|
|
636
|
-
wsToken:
|
|
637
|
-
description:
|
|
614
|
+
wsToken: '--ws-color-danger',
|
|
615
|
+
description: 'Danger/error color',
|
|
638
616
|
},
|
|
639
617
|
{
|
|
640
618
|
patterns: [/radius|rounded|corner/i],
|
|
641
|
-
wsToken:
|
|
642
|
-
description:
|
|
619
|
+
wsToken: '--ws-radius-md',
|
|
620
|
+
description: 'Border radius',
|
|
643
621
|
},
|
|
644
622
|
];
|
|
645
623
|
for (const line of lines) {
|
|
646
|
-
const [rawName] = line.split(
|
|
647
|
-
const name = rawName.replace(/^--color-|^--/,
|
|
624
|
+
const [rawName] = line.split(':').map((s) => s.trim());
|
|
625
|
+
const name = rawName.replace(/^--color-|^--/, '');
|
|
648
626
|
for (const hint of TOKEN_HINTS) {
|
|
649
627
|
if (hint.patterns.some((p) => p.test(name))) {
|
|
650
|
-
const varRef = rawName.startsWith(
|
|
651
|
-
? `var(${rawName})`
|
|
652
|
-
: rawName;
|
|
628
|
+
const varRef = rawName.startsWith('--') ? `var(${rawName})` : rawName;
|
|
653
629
|
mappings.push(` ${hint.wsToken}: ${varRef}; /* ${hint.description} ← ${rawName} */`);
|
|
654
630
|
break;
|
|
655
631
|
}
|
|
656
632
|
}
|
|
657
633
|
}
|
|
658
634
|
const frameworkNote = framework
|
|
659
|
-
? `\nFramework detected: ${framework}. ${framework ===
|
|
660
|
-
?
|
|
661
|
-
: framework ===
|
|
662
|
-
?
|
|
663
|
-
:
|
|
664
|
-
:
|
|
635
|
+
? `\nFramework detected: ${framework}. ${framework === 'tailwind'
|
|
636
|
+
? 'Use @theme to register --ws-* tokens, or override in your main CSS.'
|
|
637
|
+
: framework === 'shadcn'
|
|
638
|
+
? 'Map shadcn/ui --* variables to --ws-* in your globals.css.'
|
|
639
|
+
: 'Override --ws-* tokens in your global stylesheet.'}\n`
|
|
640
|
+
: '';
|
|
665
641
|
const css = mappings.length > 0
|
|
666
|
-
? `:root {\n${mappings.join(
|
|
667
|
-
:
|
|
642
|
+
? `:root {\n${mappings.join('\n')}\n}`
|
|
643
|
+
: '/* No matching tokens detected. Provide CSS custom properties like:\n --color-brand: #2563eb; --color-bg: #ffffff */';
|
|
668
644
|
return {
|
|
669
645
|
content: [
|
|
670
646
|
{
|
|
671
|
-
type:
|
|
647
|
+
type: 'text',
|
|
672
648
|
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`,
|
|
673
649
|
},
|
|
674
650
|
],
|
|
675
651
|
};
|
|
676
652
|
});
|
|
677
|
-
server.tool(
|
|
653
|
+
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.', {
|
|
678
654
|
components: z
|
|
679
655
|
.array(z.string())
|
|
680
656
|
.optional()
|
|
@@ -682,31 +658,29 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
682
658
|
}, async ({ components }) => {
|
|
683
659
|
const registry = await getRegistry();
|
|
684
660
|
// Read bundled CSS files
|
|
685
|
-
const { readFile } = await import(
|
|
686
|
-
const { dirname, resolve } = await import(
|
|
687
|
-
const { fileURLToPath } = await import(
|
|
661
|
+
const { readFile } = await import('node:fs/promises');
|
|
662
|
+
const { dirname, resolve } = await import('node:path');
|
|
663
|
+
const { fileURLToPath } = await import('node:url');
|
|
688
664
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
689
665
|
let tokensCss;
|
|
690
666
|
let componentsCss;
|
|
691
667
|
try {
|
|
692
|
-
tokensCss = await readFile(resolve(thisDir,
|
|
693
|
-
componentsCss = await readFile(resolve(thisDir,
|
|
668
|
+
tokensCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-tokens.css'), 'utf-8');
|
|
669
|
+
componentsCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-components.css'), 'utf-8');
|
|
694
670
|
}
|
|
695
671
|
catch {
|
|
696
672
|
return {
|
|
697
673
|
content: [
|
|
698
674
|
{
|
|
699
|
-
type:
|
|
700
|
-
text:
|
|
675
|
+
type: 'text',
|
|
676
|
+
text: 'Token CSS files not found in package. Update @webspire/mcp to the latest version.',
|
|
701
677
|
},
|
|
702
678
|
],
|
|
703
679
|
};
|
|
704
680
|
}
|
|
705
681
|
// Filter component tokens if specific families requested
|
|
706
682
|
if (components && components.length > 0) {
|
|
707
|
-
const filtered = [
|
|
708
|
-
"/* Webspire Component Tokens (filtered) */",
|
|
709
|
-
];
|
|
683
|
+
const filtered = ['/* Webspire Component Tokens (filtered) */'];
|
|
710
684
|
const sections = componentsCss.split(/(?=^\.ws-)/m);
|
|
711
685
|
for (const section of sections) {
|
|
712
686
|
const classMatch = section.match(/^\.ws-([a-z0-9-]+)/);
|
|
@@ -714,56 +688,217 @@ export function registerToolsWithProvider(server, getRegistry) {
|
|
|
714
688
|
filtered.push(section.trim());
|
|
715
689
|
}
|
|
716
690
|
}
|
|
717
|
-
componentsCss = filtered.join(
|
|
691
|
+
componentsCss = filtered.join('\n\n');
|
|
718
692
|
}
|
|
719
|
-
const families = components ? components.join(
|
|
693
|
+
const families = components ? components.join(', ') : 'all';
|
|
720
694
|
const patternCount = registry.patterns?.length ?? 0;
|
|
721
695
|
return {
|
|
722
696
|
content: [
|
|
723
697
|
{
|
|
724
|
-
type:
|
|
698
|
+
type: 'text',
|
|
725
699
|
text: [
|
|
726
700
|
`# WebSpire Token Setup (${families} families, ${patternCount} patterns)`,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
701
|
+
'',
|
|
702
|
+
'## 1. Write `webspire-tokens.css` to your project',
|
|
703
|
+
'',
|
|
704
|
+
'```css',
|
|
731
705
|
tokensCss,
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
706
|
+
'```',
|
|
707
|
+
'',
|
|
708
|
+
'## 2. Write `webspire-components.css` to your project',
|
|
709
|
+
'',
|
|
710
|
+
'```css',
|
|
737
711
|
componentsCss,
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
712
|
+
'```',
|
|
713
|
+
'',
|
|
714
|
+
'## 3. Import in your main CSS',
|
|
715
|
+
'',
|
|
716
|
+
'```css',
|
|
743
717
|
'@import "./webspire-tokens.css";',
|
|
744
718
|
'@import "./webspire-components.css";',
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
].join(
|
|
719
|
+
'```',
|
|
720
|
+
'',
|
|
721
|
+
'## 4. Override to match your brand',
|
|
722
|
+
'',
|
|
723
|
+
'```css',
|
|
724
|
+
':root {',
|
|
725
|
+
' --ws-color-primary: #your-brand-color;',
|
|
726
|
+
' --ws-color-primary-hover: #your-brand-darker;',
|
|
727
|
+
'}',
|
|
728
|
+
'```',
|
|
729
|
+
'',
|
|
730
|
+
'Docs: https://webspire.de/tokens',
|
|
731
|
+
].join('\n'),
|
|
758
732
|
},
|
|
759
733
|
],
|
|
760
734
|
};
|
|
761
735
|
});
|
|
736
|
+
// --- Font Recommendation Tool ---
|
|
737
|
+
server.tool('recommend_fonts', 'Recommend a font pairing (heading + body + mono) based on business domain and tone. Returns npm install command and Tailwind theme configuration.', {
|
|
738
|
+
domain: z.enum(DOMAIN_KEYS).describe(`Business domain: ${DOMAIN_LABEL}`),
|
|
739
|
+
tone: z.enum(TONE_KEYS).describe(`Desired tone: ${TONE_LABEL}`),
|
|
740
|
+
}, async ({ domain, tone }) => {
|
|
741
|
+
const fontsData = await loadFonts();
|
|
742
|
+
if (!fontsData) {
|
|
743
|
+
return {
|
|
744
|
+
content: [
|
|
745
|
+
{
|
|
746
|
+
type: 'text',
|
|
747
|
+
text: 'Font data not available. Update @webspire/mcp to the latest version.',
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const rec = recommendFonts(fontsData, domain, tone);
|
|
753
|
+
const predefinedNote = rec.isPredefined ? '(predefined pairing)' : '(dynamically scored)';
|
|
754
|
+
const text = [
|
|
755
|
+
`# Font Recommendation: ${tone} / ${domain} ${predefinedNote}`,
|
|
756
|
+
'',
|
|
757
|
+
'## Heading',
|
|
758
|
+
`**${rec.heading.name}** (\`${rec.heading.npm}\`)`,
|
|
759
|
+
`CSS: \`${rec.heading.css}\``,
|
|
760
|
+
`Reason: ${rec.heading.reason}`,
|
|
761
|
+
'',
|
|
762
|
+
'## Body',
|
|
763
|
+
`**${rec.body.name}** (\`${rec.body.npm}\`)`,
|
|
764
|
+
`CSS: \`${rec.body.css}\``,
|
|
765
|
+
`Reason: ${rec.body.reason}`,
|
|
766
|
+
'',
|
|
767
|
+
'## Mono',
|
|
768
|
+
`**${rec.mono.name}** (\`${rec.mono.npm}\`)`,
|
|
769
|
+
`CSS: \`${rec.mono.css}\``,
|
|
770
|
+
`Reason: ${rec.mono.reason}`,
|
|
771
|
+
'',
|
|
772
|
+
'## Install',
|
|
773
|
+
'```bash',
|
|
774
|
+
rec.install,
|
|
775
|
+
'```',
|
|
776
|
+
'',
|
|
777
|
+
'## Tailwind Theme',
|
|
778
|
+
'```css',
|
|
779
|
+
rec.tailwindTheme,
|
|
780
|
+
'```',
|
|
781
|
+
'',
|
|
782
|
+
'Browse all fonts: https://webspire.de/fonts',
|
|
783
|
+
].join('\n');
|
|
784
|
+
return {
|
|
785
|
+
content: [{ type: 'text', text }],
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
// --- Canvas Effect Tools ---
|
|
789
|
+
server.tool('search_canvas_effects', 'Search Webspire Canvas effects by keyword, category, or capability. Returns matching effects with their descriptions.', {
|
|
790
|
+
query: z
|
|
791
|
+
.string()
|
|
792
|
+
.describe('Search query, e.g. "particles", "interactive background", "gradient"'),
|
|
793
|
+
category: z
|
|
794
|
+
.string()
|
|
795
|
+
.optional()
|
|
796
|
+
.describe('Filter by category: backgrounds, interactive, decorative, data-viz, text, business'),
|
|
797
|
+
interactive: z.boolean().optional().describe('Filter for interactive effects'),
|
|
798
|
+
animate: z.boolean().optional().describe('Filter for animated effects'),
|
|
799
|
+
}, async ({ query, category, interactive, animate }) => {
|
|
800
|
+
const registry = await getRegistry();
|
|
801
|
+
const effects = registry.canvasEffects ?? [];
|
|
802
|
+
if (effects.length === 0) {
|
|
803
|
+
return {
|
|
804
|
+
content: [
|
|
805
|
+
{
|
|
806
|
+
type: 'text',
|
|
807
|
+
text: 'No canvas effects available in registry. Canvas effects may need to be bundled first.',
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
const results = searchCanvasEffects(effects, { query, category, interactive, animate });
|
|
813
|
+
if (results.length === 0) {
|
|
814
|
+
return {
|
|
815
|
+
content: [
|
|
816
|
+
{
|
|
817
|
+
type: 'text',
|
|
818
|
+
text: 'No canvas effects found matching your query.',
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const text = results
|
|
824
|
+
.map((e) => `**${e.title}** (${e.id})\n ${e.description}\n Category: ${e.category} | Animate: ${e.animate} | Interactive: ${e.interactive}`)
|
|
825
|
+
.join('\n\n');
|
|
826
|
+
return {
|
|
827
|
+
content: [
|
|
828
|
+
{
|
|
829
|
+
type: 'text',
|
|
830
|
+
text: `Found ${results.length} canvas effect${results.length > 1 ? 's' : ''}:\n\n${text}\n\nUse \`get_canvas_effect(id)\` for full JS source and embed code.`,
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
};
|
|
834
|
+
});
|
|
835
|
+
server.tool('get_canvas_effect', 'Get full JavaScript source, parameters, and embed HTML for a specific canvas effect.', {
|
|
836
|
+
id: z.string().describe('Canvas effect ID, e.g. "backgrounds/particle-field"'),
|
|
837
|
+
}, async ({ id }) => {
|
|
838
|
+
const registry = await getRegistry();
|
|
839
|
+
const effects = registry.canvasEffects ?? [];
|
|
840
|
+
const effect = effects.find((e) => e.id === id);
|
|
841
|
+
if (!effect) {
|
|
842
|
+
const available = effects.map((e) => e.id).join(', ');
|
|
843
|
+
return {
|
|
844
|
+
content: [
|
|
845
|
+
{
|
|
846
|
+
type: 'text',
|
|
847
|
+
text: `Canvas effect "${id}" not found. Available: ${available}`,
|
|
848
|
+
},
|
|
849
|
+
],
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
const params = effect.parameters
|
|
853
|
+
.map((p) => ` ${p.name} (${p.type}, default: ${String(p.default)}): ${p.description}`)
|
|
854
|
+
.join('\n');
|
|
855
|
+
const embedHtml = `<canvas id="ws-canvas" style="position:absolute;inset:0;width:100%;height:100%;" aria-hidden="true"></canvas>
|
|
856
|
+
<script type="module">
|
|
857
|
+
import { mountCanvas } from './mount-canvas.js';
|
|
858
|
+
import effect from './${id.split('/').pop()}.js';
|
|
859
|
+
mountCanvas(document.getElementById('ws-canvas'), effect, { animate: ${effect.animate} });
|
|
860
|
+
</script>`;
|
|
861
|
+
const mountSource = registry.mountCanvas ?? '/* mountCanvas source not bundled */';
|
|
862
|
+
const text = [
|
|
863
|
+
`# ${effect.title}`,
|
|
864
|
+
'',
|
|
865
|
+
effect.description,
|
|
866
|
+
'',
|
|
867
|
+
`Category: ${effect.category} | Animate: ${effect.animate} | Interactive: ${effect.interactive} | Complexity: ${effect.complexity}`,
|
|
868
|
+
'',
|
|
869
|
+
'## Parameters',
|
|
870
|
+
params || ' (none)',
|
|
871
|
+
'',
|
|
872
|
+
'## JavaScript',
|
|
873
|
+
'```javascript',
|
|
874
|
+
effect.js,
|
|
875
|
+
'```',
|
|
876
|
+
'',
|
|
877
|
+
'## mountCanvas Runtime',
|
|
878
|
+
'```javascript',
|
|
879
|
+
mountSource,
|
|
880
|
+
'```',
|
|
881
|
+
'',
|
|
882
|
+
'## Embed HTML',
|
|
883
|
+
'```html',
|
|
884
|
+
embedHtml,
|
|
885
|
+
'```',
|
|
886
|
+
'',
|
|
887
|
+
effect.useCases.length > 0
|
|
888
|
+
? `## Use Cases\n${effect.useCases.map((u) => `- ${u}`).join('\n')}`
|
|
889
|
+
: '',
|
|
890
|
+
]
|
|
891
|
+
.filter(Boolean)
|
|
892
|
+
.join('\n');
|
|
893
|
+
return {
|
|
894
|
+
content: [{ type: 'text', text }],
|
|
895
|
+
};
|
|
896
|
+
});
|
|
762
897
|
}
|
|
763
898
|
export function registerResourcesWithProvider(server, getRegistry) {
|
|
764
|
-
server.resource(
|
|
765
|
-
description:
|
|
766
|
-
mimeType:
|
|
899
|
+
server.resource('categories', 'webspire://categories', {
|
|
900
|
+
description: 'List all snippet categories with counts',
|
|
901
|
+
mimeType: 'application/json',
|
|
767
902
|
}, async () => {
|
|
768
903
|
const registry = await getRegistry();
|
|
769
904
|
const counts = {};
|
|
@@ -773,18 +908,18 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
773
908
|
return {
|
|
774
909
|
contents: [
|
|
775
910
|
{
|
|
776
|
-
uri:
|
|
777
|
-
mimeType:
|
|
911
|
+
uri: 'webspire://categories',
|
|
912
|
+
mimeType: 'application/json',
|
|
778
913
|
text: JSON.stringify(counts, null, 2),
|
|
779
914
|
},
|
|
780
915
|
],
|
|
781
916
|
};
|
|
782
917
|
});
|
|
783
|
-
server.resource(
|
|
784
|
-
description:
|
|
785
|
-
mimeType:
|
|
918
|
+
server.resource('category', 'webspire://category/{name}', {
|
|
919
|
+
description: 'List snippets in a specific category',
|
|
920
|
+
mimeType: 'application/json',
|
|
786
921
|
}, async (uri) => {
|
|
787
|
-
const name = uri.pathname.replace(
|
|
922
|
+
const name = uri.pathname.replace('//', '').split('/').pop();
|
|
788
923
|
const registry = await getRegistry();
|
|
789
924
|
const snippets = registry.snippets.filter((s) => s.category === name);
|
|
790
925
|
const brief = snippets.map((s) => ({
|
|
@@ -799,18 +934,18 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
799
934
|
contents: [
|
|
800
935
|
{
|
|
801
936
|
uri: uri.href,
|
|
802
|
-
mimeType:
|
|
937
|
+
mimeType: 'application/json',
|
|
803
938
|
text: JSON.stringify(brief, null, 2),
|
|
804
939
|
},
|
|
805
940
|
],
|
|
806
941
|
};
|
|
807
942
|
});
|
|
808
|
-
server.resource(
|
|
809
|
-
description:
|
|
810
|
-
mimeType:
|
|
943
|
+
server.resource('snippet', 'webspire://snippet/{id}', {
|
|
944
|
+
description: 'Get full snippet data including CSS source',
|
|
945
|
+
mimeType: 'application/json',
|
|
811
946
|
}, async (uri) => {
|
|
812
|
-
const parts = uri.pathname.replace(
|
|
813
|
-
const id = parts.slice(1).join(
|
|
947
|
+
const parts = uri.pathname.replace('//', '').split('/');
|
|
948
|
+
const id = parts.slice(1).join('/');
|
|
814
949
|
const registry = await getRegistry();
|
|
815
950
|
const snippet = registry.snippets.find((s) => s.id === id);
|
|
816
951
|
if (!snippet) {
|
|
@@ -818,7 +953,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
818
953
|
contents: [
|
|
819
954
|
{
|
|
820
955
|
uri: uri.href,
|
|
821
|
-
mimeType:
|
|
956
|
+
mimeType: 'text/plain',
|
|
822
957
|
text: `Snippet "${id}" not found`,
|
|
823
958
|
},
|
|
824
959
|
],
|
|
@@ -828,13 +963,13 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
828
963
|
contents: [
|
|
829
964
|
{
|
|
830
965
|
uri: uri.href,
|
|
831
|
-
mimeType:
|
|
966
|
+
mimeType: 'application/json',
|
|
832
967
|
text: JSON.stringify(snippet, null, 2),
|
|
833
968
|
},
|
|
834
969
|
],
|
|
835
970
|
};
|
|
836
971
|
});
|
|
837
|
-
server.resource(
|
|
972
|
+
server.resource('patterns', 'webspire://patterns', { description: 'List all patterns', mimeType: 'application/json' }, async () => {
|
|
838
973
|
const registry = await getRegistry();
|
|
839
974
|
const patterns = (registry.patterns ?? []).map((p) => ({
|
|
840
975
|
id: p.id,
|
|
@@ -847,19 +982,19 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
847
982
|
return {
|
|
848
983
|
contents: [
|
|
849
984
|
{
|
|
850
|
-
uri:
|
|
851
|
-
mimeType:
|
|
985
|
+
uri: 'webspire://patterns',
|
|
986
|
+
mimeType: 'application/json',
|
|
852
987
|
text: JSON.stringify(patterns, null, 2),
|
|
853
988
|
},
|
|
854
989
|
],
|
|
855
990
|
};
|
|
856
991
|
});
|
|
857
|
-
server.resource(
|
|
858
|
-
description:
|
|
859
|
-
mimeType:
|
|
992
|
+
server.resource('pattern', 'webspire://pattern/{id}', {
|
|
993
|
+
description: 'Get full pattern data including HTML source',
|
|
994
|
+
mimeType: 'application/json',
|
|
860
995
|
}, async (uri) => {
|
|
861
|
-
const parts = uri.pathname.replace(
|
|
862
|
-
const id = parts.slice(1).join(
|
|
996
|
+
const parts = uri.pathname.replace('//', '').split('/');
|
|
997
|
+
const id = parts.slice(1).join('/');
|
|
863
998
|
const registry = await getRegistry();
|
|
864
999
|
const pattern = (registry.patterns ?? []).find((p) => p.id === id);
|
|
865
1000
|
if (!pattern) {
|
|
@@ -867,7 +1002,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
867
1002
|
contents: [
|
|
868
1003
|
{
|
|
869
1004
|
uri: uri.href,
|
|
870
|
-
mimeType:
|
|
1005
|
+
mimeType: 'text/plain',
|
|
871
1006
|
text: `Pattern "${id}" not found`,
|
|
872
1007
|
},
|
|
873
1008
|
],
|
|
@@ -877,13 +1012,13 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
877
1012
|
contents: [
|
|
878
1013
|
{
|
|
879
1014
|
uri: uri.href,
|
|
880
|
-
mimeType:
|
|
1015
|
+
mimeType: 'application/json',
|
|
881
1016
|
text: JSON.stringify(pattern, null, 2),
|
|
882
1017
|
},
|
|
883
1018
|
],
|
|
884
1019
|
};
|
|
885
1020
|
});
|
|
886
|
-
server.resource(
|
|
1021
|
+
server.resource('templates', 'webspire://templates', { description: 'List all page templates', mimeType: 'application/json' }, async () => {
|
|
887
1022
|
const registry = await getRegistry();
|
|
888
1023
|
const templates = (registry.templates ?? []).map((t) => ({
|
|
889
1024
|
id: t.id,
|
|
@@ -895,19 +1030,19 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
895
1030
|
return {
|
|
896
1031
|
contents: [
|
|
897
1032
|
{
|
|
898
|
-
uri:
|
|
899
|
-
mimeType:
|
|
1033
|
+
uri: 'webspire://templates',
|
|
1034
|
+
mimeType: 'application/json',
|
|
900
1035
|
text: JSON.stringify(templates, null, 2),
|
|
901
1036
|
},
|
|
902
1037
|
],
|
|
903
1038
|
};
|
|
904
1039
|
});
|
|
905
|
-
server.resource(
|
|
906
|
-
description:
|
|
907
|
-
mimeType:
|
|
1040
|
+
server.resource('template', 'webspire://template/{id}', {
|
|
1041
|
+
description: 'Get full template data including HTML source',
|
|
1042
|
+
mimeType: 'application/json',
|
|
908
1043
|
}, async (uri) => {
|
|
909
|
-
const parts = uri.pathname.replace(
|
|
910
|
-
const id = parts.slice(1).join(
|
|
1044
|
+
const parts = uri.pathname.replace('//', '').split('/');
|
|
1045
|
+
const id = parts.slice(1).join('/');
|
|
911
1046
|
const registry = await getRegistry();
|
|
912
1047
|
const template = (registry.templates ?? []).find((t) => t.id === id);
|
|
913
1048
|
if (!template) {
|
|
@@ -915,7 +1050,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
915
1050
|
contents: [
|
|
916
1051
|
{
|
|
917
1052
|
uri: uri.href,
|
|
918
|
-
mimeType:
|
|
1053
|
+
mimeType: 'text/plain',
|
|
919
1054
|
text: `Template "${id}" not found`,
|
|
920
1055
|
},
|
|
921
1056
|
],
|
|
@@ -925,7 +1060,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
|
|
|
925
1060
|
contents: [
|
|
926
1061
|
{
|
|
927
1062
|
uri: uri.href,
|
|
928
|
-
mimeType:
|
|
1063
|
+
mimeType: 'application/json',
|
|
929
1064
|
text: JSON.stringify(template, null, 2),
|
|
930
1065
|
},
|
|
931
1066
|
],
|