@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.
@@ -1,146 +1,140 @@
1
- import { z } from "zod";
2
- import { searchSnippets } from "./registry.js";
3
- import { composePage, CONTENT_NEED_KEYS, CONTENT_NEED_LABEL, DOMAIN_KEYS, DOMAIN_LABEL, normalizeContentNeed, searchPatterns, TONE_KEYS, TONE_LABEL, UX_GOAL_KEYS, UX_GOAL_LABEL, } from "./search.js";
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 ? " Dark mode: yes" : "",
10
- s.responsive ? " Responsive: yes" : "",
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("\n");
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("prefers-reduced-motion");
20
+ features.push('prefers-reduced-motion');
23
21
  if (a11y.prefersColorScheme)
24
- features.push("prefers-color-scheme");
22
+ features.push('prefers-color-scheme');
25
23
  if (a11y.supportsCheck)
26
- features.push("@supports fallback");
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] ?? "unset"}`)
36
- .join("\n");
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
- "## Usage",
43
- "```html",
37
+ '',
38
+ '## Usage',
39
+ '```html',
44
40
  s.meta.usageExample,
45
- "```",
46
- "",
47
- "## CSS",
48
- "```css",
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
- ? `## Dependencies\n${s.dependencies.join(", ")}`
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 ? "Dark mode: supported" : "",
63
- s.responsive ? "Responsive: yes" : "",
56
+ s.darkMode ? 'Dark mode: supported' : '',
57
+ s.responsive ? 'Responsive: yes' : '',
64
58
  ]
65
- .filter((line) => line !== "")
66
- .join("\n");
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("\n");
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
- "## Identity",
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
- "## HTML",
94
- "```html",
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("\n");
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("\n");
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
- "## Identity",
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 ? " (required)" : ""}: ${s.description}`).join("\n")}`
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("\n")}`
131
- : "",
132
- "## HTML",
133
- "```html",
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("\n");
133
+ .filter((line) => line !== '')
134
+ .join('\n');
141
135
  }
142
136
  export function registerToolsWithProvider(server, getRegistry) {
143
- server.tool("list_categories", "List all available snippet categories with snippet counts", {}, async () => {
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 ? "s" : ""}`)
152
- .join("\n");
145
+ .map(([cat, count]) => `${cat}: ${count} snippet${count > 1 ? 's' : ''}`)
146
+ .join('\n');
153
147
  return {
154
148
  content: [
155
149
  {
156
- type: "text",
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("search_snippets", "Search Webspire CSS snippets by keyword, problem description, or use case. Returns matching snippets with their descriptions and IDs.", {
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("Filter by category: glass, animations, easing, scroll, decorative, interactions, text"),
170
- tags: z.array(z.string()).optional().describe("Filter by tags"),
171
- darkMode: z.boolean().optional().describe("Filter for dark mode support"),
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: "text",
190
- text: "No snippets found matching your query. Try broader keywords or check available categories with list_categories.",
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("\n\n");
186
+ const text = results.map(formatSnippetBrief).join('\n\n');
196
187
  return {
197
188
  content: [
198
189
  {
199
- type: "text",
200
- text: `Found ${results.length} snippet${results.length > 1 ? "s" : ""}:\n\n${text}`,
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("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.", {
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: "text",
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: "text", text: formatSnippetFull(snippet) }],
215
+ content: [{ type: 'text', text: formatSnippetFull(snippet) }],
225
216
  };
226
217
  });
227
- 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.", {
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("Only recommend snippets with dark mode support"),
226
+ .describe('Only recommend snippets with dark mode support'),
239
227
  needs_responsive: z
240
228
  .boolean()
241
229
  .optional()
242
- .describe("Only recommend snippets with responsive support"),
230
+ .describe('Only recommend snippets with responsive support'),
243
231
  needs_reduced_motion: z
244
232
  .boolean()
245
233
  .optional()
246
- .describe("Only recommend snippets that respect prefers-reduced-motion"),
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: "text",
268
- text: "No matching snippet found for your description. Try rephrasing or relax the constraints.",
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("dark mode");
265
+ reasons.push('dark mode');
278
266
  if (s.meta.accessibility.prefersReducedMotion)
279
- reasons.push("reduced motion");
267
+ reasons.push('reduced motion');
280
268
  if (s.meta.accessibility.supportsCheck)
281
- reasons.push("@supports fallback");
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("\n\n");
273
+ .join('\n\n');
286
274
  return {
287
275
  content: [
288
276
  {
289
- type: "text",
290
- 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.`,
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("list_pattern_families", "List all available pattern families with counts", {}, async () => {
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 ? "s" : ""}`)
305
- .join("\n");
292
+ .map(([family, count]) => `${family}: ${count} pattern${count > 1 ? 's' : ''}`)
293
+ .join('\n');
306
294
  return {
307
295
  content: [
308
296
  {
309
- type: "text",
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("search_patterns", "Search Webspire UI patterns by intent, family, tier, domain, tone, or UX goal. Uses weighted scoring with synonym expansion for better results.", {
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("Filter by pattern family, e.g. hero, pricing, navbar"),
323
- tier: z.enum(["base", "enhanced"]).optional().describe("Filter by tier"),
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("Filter/boost by business domain: legal, healthcare, education, finance, saas, ecommerce, agency, consulting, nonprofit, gastronomy, realestate, personal"),
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("Filter/boost by tone: serious, premium, modern, friendly, approachable, approachable_professional, technical, editorial, playful, institutional, industrial, casual"),
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: "text",
351
- text: "No patterns found matching your query.",
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: "text",
360
- text: `Found ${results.length} pattern${results.length > 1 ? "s" : ""}:\n\n${results.map(formatPatternBrief).join("\n\n")}`,
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("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.", {
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("Maximum number of sections to include (default: 8)"),
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: "text",
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 ?? "supporting";
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("\n\n");
393
+ .join('\n\n');
408
394
  const reasoningText = result.reasoning.length > 0
409
- ? `\n\n## Selection Reasoning\n${result.reasoning.join("\n")}`
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("\n")}\n\nUse \`get_snippet(id)\` to retrieve the CSS for any snippet.`
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: "text",
423
- 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}${warningsText}${reasoningText}\n\nUse \`get_pattern(id)\` to get full HTML for each section.`,
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("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.", {
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: "text",
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: "text",
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("list_templates", "List all available page templates grouped by category", {}, async () => {
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 ? "s" : ""}`)
465
- .join("\n");
454
+ .map(([cat, count]) => `${cat}: ${count} template${count > 1 ? 's' : ''}`)
455
+ .join('\n');
466
456
  return {
467
457
  content: [
468
458
  {
469
- type: "text",
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("search_templates", "Search Webspire page templates by keyword, category, or style.", {
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("Filter by category, e.g. saas-landing, agency, shop"),
470
+ .describe('Filter by category, e.g. saas-landing, agency, shop'),
483
471
  style: z
484
472
  .string()
485
473
  .optional()
486
- .describe("Filter by style, e.g. modern, bold, minimal, corporate"),
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: "text",
520
- text: "No templates found matching your query.",
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: "text",
529
- text: `Found ${results.length} template${results.length > 1 ? "s" : ""}:\n\n${results.map(formatTemplateBrief).join("\n\n")}`,
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("get_template", "Get full template HTML for a specific page template. Returns standalone HTML ready to use.", {
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: "text",
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("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.", {
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(["tailwind", "custom", "bootstrap", "chakra", "shadcn"])
547
+ .enum(['tailwind', 'custom', 'bootstrap', 'chakra', 'shadcn'])
566
548
  .optional()
567
- .describe("CSS framework used in your project"),
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: "--ws-color-primary",
578
- description: "Primary/brand color",
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: "--ws-color-primary-hover",
583
- description: "Primary hover state",
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: "--ws-color-primary-soft",
588
- description: "Primary soft background",
569
+ wsToken: '--ws-color-primary-soft',
570
+ description: 'Primary soft background',
589
571
  },
590
572
  {
591
573
  patterns: [/secondary|accent(?!-color)/i],
592
- wsToken: "--ws-color-accent",
593
- description: "Secondary/accent color",
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: "--ws-color-surface",
598
- description: "Page background",
579
+ wsToken: '--ws-color-surface',
580
+ description: 'Page background',
599
581
  },
600
582
  {
601
- patterns: [
602
- /bg.*secondary|bg.*subtle|bg.*muted|surface.*alt|bg.*light/i,
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: "--ws-color-text",
610
- description: "Primary text color",
589
+ wsToken: '--ws-color-text',
590
+ description: 'Primary text color',
611
591
  },
612
592
  {
613
- patterns: [
614
- /text.*secondary|text.*soft|text.*muted|muted.*foreground/i,
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: "--ws-color-border",
622
- description: "Default border",
599
+ wsToken: '--ws-color-border',
600
+ description: 'Default border',
623
601
  },
624
602
  {
625
603
  patterns: [/success|green|positive/i],
626
- wsToken: "--ws-color-success",
627
- description: "Success color",
604
+ wsToken: '--ws-color-success',
605
+ description: 'Success color',
628
606
  },
629
607
  {
630
608
  patterns: [/warning|amber|yellow|caution/i],
631
- wsToken: "--ws-color-warning",
632
- description: "Warning color",
609
+ wsToken: '--ws-color-warning',
610
+ description: 'Warning color',
633
611
  },
634
612
  {
635
613
  patterns: [/danger|error|red|destructive/i],
636
- wsToken: "--ws-color-danger",
637
- description: "Danger/error color",
614
+ wsToken: '--ws-color-danger',
615
+ description: 'Danger/error color',
638
616
  },
639
617
  {
640
618
  patterns: [/radius|rounded|corner/i],
641
- wsToken: "--ws-radius-md",
642
- description: "Border radius",
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(":").map((s) => s.trim());
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 === "tailwind"
660
- ? "Use @theme to register --ws-* tokens, or override in your main CSS."
661
- : framework === "shadcn"
662
- ? "Map shadcn/ui --* variables to --ws-* in your globals.css."
663
- : "Override --ws-* tokens in your global stylesheet."}\n`
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("\n")}\n}`
667
- : "/* No matching tokens detected. Provide CSS custom properties like:\n --color-brand: #2563eb; --color-bg: #ffffff */";
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: "text",
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("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.", {
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("node:fs/promises");
686
- const { dirname, resolve } = await import("node:path");
687
- const { fileURLToPath } = await import("node:url");
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, "..", "css", "webspire-tokens.css"), "utf-8");
693
- componentsCss = await readFile(resolve(thisDir, "..", "css", "webspire-components.css"), "utf-8");
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: "text",
700
- text: "Token CSS files not found in package. Update @webspire/mcp to the latest version.",
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("\n\n");
691
+ componentsCss = filtered.join('\n\n');
718
692
  }
719
- const families = components ? components.join(", ") : "all";
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: "text",
698
+ type: 'text',
725
699
  text: [
726
700
  `# WebSpire Token Setup (${families} families, ${patternCount} patterns)`,
727
- "",
728
- "## 1. Write `webspire-tokens.css` to your project",
729
- "",
730
- "```css",
701
+ '',
702
+ '## 1. Write `webspire-tokens.css` to your project',
703
+ '',
704
+ '```css',
731
705
  tokensCss,
732
- "```",
733
- "",
734
- "## 2. Write `webspire-components.css` to your project",
735
- "",
736
- "```css",
706
+ '```',
707
+ '',
708
+ '## 2. Write `webspire-components.css` to your project',
709
+ '',
710
+ '```css',
737
711
  componentsCss,
738
- "```",
739
- "",
740
- "## 3. Import in your main CSS",
741
- "",
742
- "```css",
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
- "## 4. Override to match your brand",
748
- "",
749
- "```css",
750
- ":root {",
751
- " --ws-color-primary: #your-brand-color;",
752
- " --ws-color-primary-hover: #your-brand-darker;",
753
- "}",
754
- "```",
755
- "",
756
- "Docs: https://webspire.de/tokens",
757
- ].join("\n"),
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("categories", "webspire://categories", {
765
- description: "List all snippet categories with counts",
766
- mimeType: "application/json",
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: "webspire://categories",
777
- mimeType: "application/json",
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("category", "webspire://category/{name}", {
784
- description: "List snippets in a specific category",
785
- mimeType: "application/json",
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("//", "").split("/").pop();
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: "application/json",
937
+ mimeType: 'application/json',
803
938
  text: JSON.stringify(brief, null, 2),
804
939
  },
805
940
  ],
806
941
  };
807
942
  });
808
- server.resource("snippet", "webspire://snippet/{id}", {
809
- description: "Get full snippet data including CSS source",
810
- mimeType: "application/json",
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("//", "").split("/");
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: "text/plain",
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: "application/json",
966
+ mimeType: 'application/json',
832
967
  text: JSON.stringify(snippet, null, 2),
833
968
  },
834
969
  ],
835
970
  };
836
971
  });
837
- server.resource("patterns", "webspire://patterns", { description: "List all patterns", mimeType: "application/json" }, async () => {
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: "webspire://patterns",
851
- mimeType: "application/json",
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("pattern", "webspire://pattern/{id}", {
858
- description: "Get full pattern data including HTML source",
859
- mimeType: "application/json",
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("//", "").split("/");
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: "text/plain",
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: "application/json",
1015
+ mimeType: 'application/json',
881
1016
  text: JSON.stringify(pattern, null, 2),
882
1017
  },
883
1018
  ],
884
1019
  };
885
1020
  });
886
- server.resource("templates", "webspire://templates", { description: "List all page templates", mimeType: "application/json" }, async () => {
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: "webspire://templates",
899
- mimeType: "application/json",
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("template", "webspire://template/{id}", {
906
- description: "Get full template data including HTML source",
907
- mimeType: "application/json",
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("//", "").split("/");
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: "text/plain",
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: "application/json",
1063
+ mimeType: 'application/json',
929
1064
  text: JSON.stringify(template, null, 2),
930
1065
  },
931
1066
  ],