@webspire/mcp 0.8.0 → 0.8.1

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,139 +1,146 @@
1
- import { z } from 'zod';
2
- import { searchSnippets } from './registry.js';
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";
3
4
  function formatSnippetBrief(s) {
4
5
  return [
5
6
  `**${s.title}** (${s.id})`,
6
7
  ` ${s.description}`,
7
- ` Category: ${s.category} | Tags: ${s.tags.join(', ')}`,
8
- s.darkMode ? ' Dark mode: yes' : '',
9
- s.responsive ? ' Responsive: yes' : '',
10
- s.meta.useCases.length > 0 ? ` Use cases: ${s.meta.useCases.slice(0, 2).join('; ')}` : '',
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
+ : "",
11
14
  ]
12
15
  .filter(Boolean)
13
- .join('\n');
16
+ .join("\n");
14
17
  }
15
18
  function formatAccessibility(s) {
16
19
  const a11y = s.meta.accessibility;
17
20
  const features = [];
18
21
  if (a11y.prefersReducedMotion)
19
- features.push('prefers-reduced-motion');
22
+ features.push("prefers-reduced-motion");
20
23
  if (a11y.prefersColorScheme)
21
- features.push('prefers-color-scheme');
24
+ features.push("prefers-color-scheme");
22
25
  if (a11y.supportsCheck)
23
- features.push('@supports fallback');
26
+ features.push("@supports fallback");
24
27
  if (a11y.ariaNotes?.length)
25
28
  features.push(...a11y.ariaNotes);
26
- return features.length > 0 ? `## Accessibility\nRespects: ${features.join(', ')}` : '';
29
+ return features.length > 0
30
+ ? `## Accessibility\nRespects: ${features.join(", ")}`
31
+ : "";
27
32
  }
28
33
  function formatSnippetFull(s) {
29
34
  const props = s.meta.customProperties
30
- .map((p) => ` ${p}: ${s.meta.defaultValues?.[p] ?? 'unset'}`)
31
- .join('\n');
35
+ .map((p) => ` ${p}: ${s.meta.defaultValues?.[p] ?? "unset"}`)
36
+ .join("\n");
32
37
  return [
33
38
  `# ${s.title}`,
34
- '',
39
+ "",
35
40
  s.description,
36
- '',
37
- '## Usage',
38
- '```html',
41
+ "",
42
+ "## Usage",
43
+ "```html",
39
44
  s.meta.usageExample,
40
- '```',
41
- '',
42
- '## CSS',
43
- '```css',
45
+ "```",
46
+ "",
47
+ "## CSS",
48
+ "```css",
44
49
  s.css,
45
- '```',
46
- '',
47
- s.meta.customProperties.length > 0 ? `## Custom Properties\n${props}` : '',
48
- s.meta.classes.length > 0 ? `## Classes\n${s.meta.classes.join(', ')}` : '',
50
+ "```",
51
+ "",
52
+ s.meta.customProperties.length > 0 ? `## Custom Properties\n${props}` : "",
53
+ s.meta.classes.length > 0 ? `## Classes\n${s.meta.classes.join(", ")}` : "",
49
54
  formatAccessibility(s),
50
- s.dependencies?.length ? `## Dependencies\n${s.dependencies.join(', ')}` : '',
51
- s.variants?.length ? `## Variants\n${s.variants.join(', ')}` : '',
52
- '',
55
+ s.dependencies?.length
56
+ ? `## Dependencies\n${s.dependencies.join(", ")}`
57
+ : "",
58
+ s.variants?.length ? `## Variants\n${s.variants.join(", ")}` : "",
59
+ "",
53
60
  `Browser support: ${s.meta.browser}`,
54
61
  `Size: ${s.meta.lines} lines, ${s.meta.bytes} bytes`,
55
- s.darkMode ? 'Dark mode: supported' : '',
56
- s.responsive ? 'Responsive: yes' : '',
62
+ s.darkMode ? "Dark mode: supported" : "",
63
+ s.responsive ? "Responsive: yes" : "",
57
64
  ]
58
- .filter((line) => line !== '')
59
- .join('\n');
65
+ .filter((line) => line !== "")
66
+ .join("\n");
60
67
  }
61
68
  function formatTemplateBrief(t) {
62
69
  return [
63
70
  `**${t.title}** (${t.id})`,
64
71
  ` ${t.summary}`,
65
72
  ` Category: ${t.category} | Style: ${t.style}`,
66
- t.tags.length > 0 ? ` Tags: ${t.tags.join(', ')}` : '',
73
+ t.tags.length > 0 ? ` Tags: ${t.tags.join(", ")}` : "",
67
74
  ]
68
75
  .filter(Boolean)
69
- .join('\n');
76
+ .join("\n");
70
77
  }
71
78
  function formatTemplateFull(t) {
72
79
  return [
73
80
  `# ${t.title}`,
74
- '',
81
+ "",
75
82
  t.description ?? t.summary,
76
- '',
77
- '## Identity',
83
+ "",
84
+ "## Identity",
78
85
  `ID: ${t.id}`,
79
86
  `Category: ${t.category}`,
80
87
  `Style: ${t.style}`,
81
- '',
82
- t.tags.length > 0 ? `## Tags\n${t.tags.join(', ')}` : '',
83
- t.sections.length > 0 ? `## Sections\n${t.sections.join(', ')}` : '',
84
- t.patterns.length > 0 ? `## Uses Patterns\n${t.patterns.join(', ')}` : '',
85
- '',
86
- '## HTML',
87
- '```html',
88
+ "",
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",
88
95
  t.html,
89
- '```',
96
+ "```",
90
97
  ]
91
- .filter((line) => line !== '')
92
- .join('\n');
98
+ .filter((line) => line !== "")
99
+ .join("\n");
93
100
  }
94
101
  function formatPatternBrief(p) {
95
102
  return [
96
103
  `**${p.title}** (${p.id})`,
97
104
  ` ${p.summary}`,
98
105
  ` Family: ${p.family} | Tier: ${p.tier} | Kind: ${p.kind}`,
99
- p.tags.length > 0 ? ` Tags: ${p.tags.join(', ')}` : '',
100
- p.extends ? ` Extends: ${p.extends}` : '',
106
+ p.tags.length > 0 ? ` Tags: ${p.tags.join(", ")}` : "",
107
+ p.extends ? ` Extends: ${p.extends}` : "",
101
108
  ]
102
109
  .filter(Boolean)
103
- .join('\n');
110
+ .join("\n");
104
111
  }
105
112
  function formatPatternFull(p) {
106
113
  return [
107
114
  `# ${p.title}`,
108
- '',
115
+ "",
109
116
  p.description ?? p.summary,
110
- '',
111
- '## Identity',
117
+ "",
118
+ "## Identity",
112
119
  `ID: ${p.id}`,
113
120
  `Family: ${p.family}`,
114
121
  `Tier: ${p.tier}`,
115
122
  `Kind: ${p.kind}`,
116
- p.extends ? `Extends: ${p.extends}` : '',
117
- '',
118
- p.tags.length > 0 ? `## Tags\n${p.tags.join(', ')}` : '',
123
+ p.extends ? `Extends: ${p.extends}` : "",
124
+ "",
125
+ p.tags.length > 0 ? `## Tags\n${p.tags.join(", ")}` : "",
119
126
  p.slots.length > 0
120
- ? `## Slots\n${p.slots.map((s) => `- ${s.name}${s.required ? ' (required)' : ''}: ${s.description}`).join('\n')}`
121
- : '',
127
+ ? `## Slots\n${p.slots.map((s) => `- ${s.name}${s.required ? " (required)" : ""}: ${s.description}`).join("\n")}`
128
+ : "",
122
129
  p.props.length > 0
123
- ? `## Props\n${p.props.map((prop) => `- ${prop.name} (${prop.type}) default=${String(prop.default)}`).join('\n')}`
124
- : '',
125
- '## HTML',
126
- '```html',
130
+ ? `## Props\n${p.props.map((prop) => `- ${prop.name} (${prop.type}) default=${String(prop.default)}`).join("\n")}`
131
+ : "",
132
+ "## HTML",
133
+ "```html",
127
134
  p.html,
128
- '```',
129
- p.css ? `## CSS\n\`\`\`css\n${p.css}\n\`\`\`` : '',
130
- p.js ? `## JS\n\`\`\`js\n${p.js}\n\`\`\`` : '',
135
+ "```",
136
+ p.css ? `## CSS\n\`\`\`css\n${p.css}\n\`\`\`` : "",
137
+ p.js ? `## JS\n\`\`\`js\n${p.js}\n\`\`\`` : "",
131
138
  ]
132
- .filter((line) => line !== '')
133
- .join('\n');
139
+ .filter((line) => line !== "")
140
+ .join("\n");
134
141
  }
135
142
  export function registerToolsWithProvider(server, getRegistry) {
136
- server.tool('list_categories', 'List all available snippet categories with snippet counts', {}, async () => {
143
+ server.tool("list_categories", "List all available snippet categories with snippet counts", {}, async () => {
137
144
  const registry = await getRegistry();
138
145
  const counts = {};
139
146
  for (const snippet of registry.snippets) {
@@ -141,28 +148,31 @@ export function registerToolsWithProvider(server, getRegistry) {
141
148
  }
142
149
  const result = Object.entries(counts)
143
150
  .sort(([, a], [, b]) => b - a)
144
- .map(([cat, count]) => `${cat}: ${count} snippet${count > 1 ? 's' : ''}`)
145
- .join('\n');
151
+ .map(([cat, count]) => `${cat}: ${count} snippet${count > 1 ? "s" : ""}`)
152
+ .join("\n");
146
153
  return {
147
154
  content: [
148
155
  {
149
- type: 'text',
156
+ type: "text",
150
157
  text: `${registry.snippets.length} snippets across ${Object.keys(counts).length} categories:\n\n${result}`,
151
158
  },
152
159
  ],
153
160
  };
154
161
  });
155
- server.tool('search_snippets', 'Search Webspire CSS snippets by keyword, problem description, or use case. Returns matching snippets with their descriptions and IDs.', {
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
163
  query: z
157
164
  .string()
158
165
  .describe('Search query: problem description, use case, or CSS technique (e.g. "glass card", "fade animation", "blur entrance")'),
159
166
  category: z
160
167
  .string()
161
168
  .optional()
162
- .describe('Filter by category: glass, animations, easing, scroll, decorative, interactions, text'),
163
- tags: z.array(z.string()).optional().describe('Filter by tags'),
164
- darkMode: z.boolean().optional().describe('Filter for dark mode support'),
165
- responsive: z.boolean().optional().describe('Filter for responsive support'),
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"),
166
176
  }, async ({ query, category, tags, darkMode, responsive }) => {
167
177
  const registry = await getRegistry();
168
178
  const results = searchSnippets(registry, {
@@ -176,23 +186,23 @@ export function registerToolsWithProvider(server, getRegistry) {
176
186
  return {
177
187
  content: [
178
188
  {
179
- type: 'text',
180
- text: 'No snippets found matching your query. Try broader keywords or check available categories with list_categories.',
189
+ type: "text",
190
+ text: "No snippets found matching your query. Try broader keywords or check available categories with list_categories.",
181
191
  },
182
192
  ],
183
193
  };
184
194
  }
185
- const text = results.map(formatSnippetBrief).join('\n\n');
195
+ const text = results.map(formatSnippetBrief).join("\n\n");
186
196
  return {
187
197
  content: [
188
198
  {
189
- type: 'text',
190
- text: `Found ${results.length} snippet${results.length > 1 ? 's' : ''}:\n\n${text}`,
199
+ type: "text",
200
+ text: `Found ${results.length} snippet${results.length > 1 ? "s" : ""}:\n\n${text}`,
191
201
  },
192
202
  ],
193
203
  };
194
204
  });
195
- 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.', {
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
206
  id: z
197
207
  .string()
198
208
  .describe('Snippet ID, e.g. "glass/frosted", "animations/fade-in", "easing/tokens"'),
@@ -200,38 +210,41 @@ export function registerToolsWithProvider(server, getRegistry) {
200
210
  const registry = await getRegistry();
201
211
  const snippet = registry.snippets.find((s) => s.id === id);
202
212
  if (!snippet) {
203
- const available = registry.snippets.map((s) => s.id).join(', ');
213
+ const available = registry.snippets.map((s) => s.id).join(", ");
204
214
  return {
205
215
  content: [
206
216
  {
207
- type: 'text',
217
+ type: "text",
208
218
  text: `Snippet "${id}" not found. Available snippets: ${available}`,
209
219
  },
210
220
  ],
211
221
  };
212
222
  }
213
223
  return {
214
- content: [{ type: 'text', text: formatSnippetFull(snippet) }],
224
+ content: [{ type: "text", text: formatSnippetFull(snippet) }],
215
225
  };
216
226
  });
217
- 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.', {
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
228
  use_case: z
219
229
  .string()
220
230
  .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")'),
221
- category: z.string().optional().describe('Limit recommendations to a specific category'),
231
+ category: z
232
+ .string()
233
+ .optional()
234
+ .describe("Limit recommendations to a specific category"),
222
235
  needs_dark_mode: z
223
236
  .boolean()
224
237
  .optional()
225
- .describe('Only recommend snippets with dark mode support'),
238
+ .describe("Only recommend snippets with dark mode support"),
226
239
  needs_responsive: z
227
240
  .boolean()
228
241
  .optional()
229
- .describe('Only recommend snippets with responsive support'),
242
+ .describe("Only recommend snippets with responsive support"),
230
243
  needs_reduced_motion: z
231
244
  .boolean()
232
245
  .optional()
233
- .describe('Only recommend snippets that respect prefers-reduced-motion'),
234
- }, async ({ use_case, category, needs_dark_mode, needs_responsive, needs_reduced_motion }) => {
246
+ .describe("Only recommend snippets that respect prefers-reduced-motion"),
247
+ }, async ({ use_case, category, needs_dark_mode, needs_responsive, needs_reduced_motion, }) => {
235
248
  const registry = await getRegistry();
236
249
  let filteredRegistry = registry;
237
250
  if (needs_reduced_motion) {
@@ -251,8 +264,8 @@ export function registerToolsWithProvider(server, getRegistry) {
251
264
  return {
252
265
  content: [
253
266
  {
254
- type: 'text',
255
- text: 'No matching snippet found for your description. Try rephrasing or relax the constraints.',
267
+ type: "text",
268
+ text: "No matching snippet found for your description. Try rephrasing or relax the constraints.",
256
269
  },
257
270
  ],
258
271
  };
@@ -261,25 +274,25 @@ export function registerToolsWithProvider(server, getRegistry) {
261
274
  .map((s, i) => {
262
275
  const reasons = [];
263
276
  if (s.darkMode)
264
- reasons.push('dark mode');
277
+ reasons.push("dark mode");
265
278
  if (s.meta.accessibility.prefersReducedMotion)
266
- reasons.push('reduced motion');
279
+ reasons.push("reduced motion");
267
280
  if (s.meta.accessibility.supportsCheck)
268
- reasons.push('@supports fallback');
269
- const reasonStr = reasons.length > 0 ? ` (${reasons.join(', ')})` : '';
281
+ reasons.push("@supports fallback");
282
+ const reasonStr = reasons.length > 0 ? ` (${reasons.join(", ")})` : "";
270
283
  return `${i + 1}. ${formatSnippetBrief(s)}${reasonStr}\n Usage: \`${s.meta.usageExample}\``;
271
284
  })
272
- .join('\n\n');
285
+ .join("\n\n");
273
286
  return {
274
287
  content: [
275
288
  {
276
- type: 'text',
277
- 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.`,
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.`,
278
291
  },
279
292
  ],
280
293
  };
281
294
  });
282
- server.tool('list_pattern_families', 'List all available pattern families with counts', {}, async () => {
295
+ server.tool("list_pattern_families", "List all available pattern families with counts", {}, async () => {
283
296
  const registry = await getRegistry();
284
297
  const patterns = registry.patterns ?? [];
285
298
  const counts = {};
@@ -288,62 +301,54 @@ export function registerToolsWithProvider(server, getRegistry) {
288
301
  }
289
302
  const result = Object.entries(counts)
290
303
  .sort(([, a], [, b]) => b - a)
291
- .map(([family, count]) => `${family}: ${count} pattern${count > 1 ? 's' : ''}`)
292
- .join('\n');
304
+ .map(([family, count]) => `${family}: ${count} pattern${count > 1 ? "s" : ""}`)
305
+ .join("\n");
293
306
  return {
294
307
  content: [
295
308
  {
296
- type: 'text',
309
+ type: "text",
297
310
  text: `${patterns.length} patterns across ${Object.keys(counts).length} families:\n\n${result}`,
298
311
  },
299
312
  ],
300
313
  };
301
314
  });
302
- server.tool('search_patterns', 'Search Webspire UI patterns by intent, family, and tier.', {
303
- query: z.string().describe('Search query, e.g. "hero with image", "pricing section"'),
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.", {
316
+ query: z
317
+ .string()
318
+ .describe('Search query, e.g. "hero with image", "pricing section", "trust building for legal"'),
304
319
  family: z
305
320
  .string()
306
321
  .optional()
307
- .describe('Filter by pattern family, e.g. hero, pricing, navbar'),
308
- tier: z.enum(['base', 'enhanced']).optional().describe('Filter by tier'),
309
- }, async ({ query, family, tier }) => {
322
+ .describe("Filter by pattern family, e.g. hero, pricing, navbar"),
323
+ tier: z.enum(["base", "enhanced"]).optional().describe("Filter by tier"),
324
+ domain: z
325
+ .string()
326
+ .optional()
327
+ .describe("Filter/boost by business domain: legal, healthcare, education, finance, saas, ecommerce, agency, consulting, nonprofit, gastronomy, realestate, personal"),
328
+ tone: z
329
+ .string()
330
+ .optional()
331
+ .describe("Filter/boost by tone: serious, premium, modern, friendly, approachable, approachable_professional, technical, editorial, playful, institutional, industrial, casual"),
332
+ ux_goal: z
333
+ .enum(UX_GOAL_KEYS)
334
+ .optional()
335
+ .describe(`Filter/boost by UX goal: ${UX_GOAL_LABEL}`),
336
+ }, async ({ query, family, tier, domain, tone, ux_goal }) => {
310
337
  const registry = await getRegistry();
311
- let patterns = registry.patterns ?? [];
312
- if (family) {
313
- patterns = patterns.filter((p) => p.family === family);
314
- }
315
- if (tier) {
316
- patterns = patterns.filter((p) => p.tier === tier);
317
- }
318
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
319
- const results = patterns
320
- .map((p) => {
321
- const haystack = [
322
- p.title,
323
- p.summary,
324
- p.description ?? '',
325
- p.family,
326
- p.kind,
327
- ...p.tags,
328
- ...p.search.intent,
329
- ...p.search.keywords,
330
- ...p.search.useCases,
331
- ]
332
- .join(' ')
333
- .toLowerCase();
334
- const score = terms.reduce((acc, t) => acc + (haystack.includes(t) ? 1 : 0), 0);
335
- return { pattern: p, score };
336
- })
337
- .filter((row) => row.score > 0)
338
- .sort((a, b) => b.score - a.score)
339
- .slice(0, 10)
340
- .map((row) => row.pattern);
338
+ const results = searchPatterns(registry.patterns ?? [], {
339
+ query,
340
+ family,
341
+ tier,
342
+ domain,
343
+ tone,
344
+ uxGoal: ux_goal,
345
+ });
341
346
  if (results.length === 0) {
342
347
  return {
343
348
  content: [
344
349
  {
345
- type: 'text',
346
- text: 'No patterns found matching your query.',
350
+ type: "text",
351
+ text: "No patterns found matching your query.",
347
352
  },
348
353
  ],
349
354
  };
@@ -351,23 +356,88 @@ export function registerToolsWithProvider(server, getRegistry) {
351
356
  return {
352
357
  content: [
353
358
  {
354
- type: 'text',
355
- text: `Found ${results.length} pattern${results.length > 1 ? 's' : ''}:\n\n${results.map(formatPatternBrief).join('\n\n')}`,
359
+ type: "text",
360
+ text: `Found ${results.length} pattern${results.length > 1 ? "s" : ""}:\n\n${results.map(formatPatternBrief).join("\n\n")}`,
356
361
  },
357
362
  ],
358
363
  };
359
364
  });
360
- server.tool('get_pattern', 'Get full pattern data including HTML (and optional CSS/JS) for a specific pattern. Patterns use --ws-* design tokens via component classes import webspire-tokens.css + webspire-components.css to enable.', {
361
- id: z.string().describe('Pattern ID, e.g. "hero/base", "hero/with-image"'),
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.", {
366
+ domain: z.enum(DOMAIN_KEYS).describe(`Business domain: ${DOMAIN_LABEL}`),
367
+ 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}`),
371
+ content_needs: z
372
+ .array(z.enum(CONTENT_NEED_KEYS))
373
+ .optional()
374
+ .describe(`Optional explicit content requirements. Allowed values: ${CONTENT_NEED_LABEL}`),
375
+ max_sections: z
376
+ .number()
377
+ .optional()
378
+ .default(8)
379
+ .describe("Maximum number of sections to include (default: 8)"),
380
+ }, async ({ domain, tone, ux_goals, content_needs, max_sections }) => {
381
+ const registry = await getRegistry();
382
+ const allPatterns = registry.patterns ?? [];
383
+ const result = composePage(allPatterns, registry.snippets, {
384
+ domain,
385
+ tone,
386
+ uxGoals: ux_goals,
387
+ contentNeeds: content_needs
388
+ ?.map(normalizeContentNeed)
389
+ .filter((need) => need !== null),
390
+ maxSections: max_sections,
391
+ });
392
+ if (result.patterns.length === 0) {
393
+ return {
394
+ content: [
395
+ {
396
+ type: "text",
397
+ text: `No suitable patterns found for domain="${domain}", tone="${tone}". Try broader criteria.`,
398
+ },
399
+ ],
400
+ };
401
+ }
402
+ const sections = result.patterns
403
+ .map((p, i) => {
404
+ const role = p.ai?.compositionRole ?? "supporting";
405
+ return `${i + 1}. **${p.title}** (${p.id}) — role: ${role}\n ${p.summary}`;
406
+ })
407
+ .join("\n\n");
408
+ 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
+ : "";
414
+ const snippetsText = result.snippets.length > 0
415
+ ? `\n\n## Recommended Snippets\n${result.snippets
416
+ .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
+ : "";
419
+ return {
420
+ content: [
421
+ {
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.`,
424
+ },
425
+ ],
426
+ };
427
+ });
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"'),
362
432
  }, async ({ id }) => {
363
433
  const registry = await getRegistry();
364
434
  const pattern = (registry.patterns ?? []).find((p) => p.id === id);
365
435
  if (!pattern) {
366
- const available = (registry.patterns ?? []).map((p) => p.id).join(', ');
436
+ const available = (registry.patterns ?? []).map((p) => p.id).join(", ");
367
437
  return {
368
438
  content: [
369
439
  {
370
- type: 'text',
440
+ type: "text",
371
441
  text: `Pattern "${id}" not found. Available patterns: ${available}`,
372
442
  },
373
443
  ],
@@ -376,13 +446,13 @@ export function registerToolsWithProvider(server, getRegistry) {
376
446
  return {
377
447
  content: [
378
448
  {
379
- type: 'text',
449
+ type: "text",
380
450
  text: `${formatPatternFull(pattern)}\n\n---\n**Setup:** Import \`webspire-tokens.css\` and \`webspire-components.css\` to enable token support.\nOverride \`--ws-color-primary\` etc. to match your brand. Docs: https://webspire.de/tokens`,
381
451
  },
382
452
  ],
383
453
  };
384
454
  });
385
- server.tool('list_templates', 'List all available page templates grouped by category', {}, async () => {
455
+ server.tool("list_templates", "List all available page templates grouped by category", {}, async () => {
386
456
  const registry = await getRegistry();
387
457
  const templates = registry.templates ?? [];
388
458
  const counts = {};
@@ -391,27 +461,29 @@ export function registerToolsWithProvider(server, getRegistry) {
391
461
  }
392
462
  const result = Object.entries(counts)
393
463
  .sort(([, a], [, b]) => b - a)
394
- .map(([cat, count]) => `${cat}: ${count} template${count > 1 ? 's' : ''}`)
395
- .join('\n');
464
+ .map(([cat, count]) => `${cat}: ${count} template${count > 1 ? "s" : ""}`)
465
+ .join("\n");
396
466
  return {
397
467
  content: [
398
468
  {
399
- type: 'text',
469
+ type: "text",
400
470
  text: `${templates.length} templates across ${Object.keys(counts).length} categories:\n\n${result}`,
401
471
  },
402
472
  ],
403
473
  };
404
474
  });
405
- server.tool('search_templates', 'Search Webspire page templates by keyword, category, or style.', {
406
- query: z.string().describe('Search query, e.g. "saas landing", "portfolio dark", "shop"'),
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"'),
407
479
  category: z
408
480
  .string()
409
481
  .optional()
410
- .describe('Filter by category, e.g. saas-landing, agency, shop'),
482
+ .describe("Filter by category, e.g. saas-landing, agency, shop"),
411
483
  style: z
412
484
  .string()
413
485
  .optional()
414
- .describe('Filter by style, e.g. modern, bold, minimal, corporate'),
486
+ .describe("Filter by style, e.g. modern, bold, minimal, corporate"),
415
487
  }, async ({ query, category, style }) => {
416
488
  const registry = await getRegistry();
417
489
  let templates = registry.templates ?? [];
@@ -425,13 +497,13 @@ export function registerToolsWithProvider(server, getRegistry) {
425
497
  const haystack = [
426
498
  t.title,
427
499
  t.summary,
428
- t.description ?? '',
500
+ t.description ?? "",
429
501
  t.category,
430
502
  t.style,
431
503
  ...t.tags,
432
504
  ...t.sections,
433
505
  ]
434
- .join(' ')
506
+ .join(" ")
435
507
  .toLowerCase();
436
508
  const score = terms.reduce((acc, term) => acc + (haystack.includes(term) ? 1 : 0), 0);
437
509
  return { template: t, score };
@@ -442,46 +514,57 @@ export function registerToolsWithProvider(server, getRegistry) {
442
514
  .map((row) => row.template);
443
515
  if (results.length === 0) {
444
516
  return {
445
- content: [{ type: 'text', text: 'No templates found matching your query.' }],
517
+ content: [
518
+ {
519
+ type: "text",
520
+ text: "No templates found matching your query.",
521
+ },
522
+ ],
446
523
  };
447
524
  }
448
525
  return {
449
526
  content: [
450
527
  {
451
- type: 'text',
452
- text: `Found ${results.length} template${results.length > 1 ? 's' : ''}:\n\n${results.map(formatTemplateBrief).join('\n\n')}`,
528
+ type: "text",
529
+ text: `Found ${results.length} template${results.length > 1 ? "s" : ""}:\n\n${results.map(formatTemplateBrief).join("\n\n")}`,
453
530
  },
454
531
  ],
455
532
  };
456
533
  });
457
- server.tool('get_template', 'Get full template HTML for a specific page template. Returns standalone HTML ready to use.', {
458
- id: z.string().describe('Template ID, e.g. "saas-landing/modern", "shop/catalog"'),
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"'),
459
538
  }, async ({ id }) => {
460
539
  const registry = await getRegistry();
461
540
  const template = (registry.templates ?? []).find((t) => t.id === id);
462
541
  if (!template) {
463
- const available = (registry.templates ?? []).map((t) => t.id).join(', ');
542
+ const available = (registry.templates ?? [])
543
+ .map((t) => t.id)
544
+ .join(", ");
464
545
  return {
465
546
  content: [
466
547
  {
467
- type: 'text',
548
+ type: "text",
468
549
  text: `Template "${id}" not found. Available templates: ${available}`,
469
550
  },
470
551
  ],
471
552
  };
472
553
  }
473
554
  return {
474
- content: [{ type: 'text', text: formatTemplateFull(template) }],
555
+ content: [
556
+ { type: "text", text: formatTemplateFull(template) },
557
+ ],
475
558
  };
476
559
  });
477
- server.tool('recommend_token_mapping', 'Analyze project design tokens and recommend how to map them to Webspire --ws-* tokens. Provide your existing CSS custom properties or Tailwind theme colors.', {
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.", {
478
561
  project_tokens: z
479
562
  .string()
480
563
  .describe('Your project\'s design tokens or CSS custom properties, e.g. "--color-brand-500: #2563eb; --color-bg: #fafafa" or "primary: blue-600, background: white"'),
481
564
  framework: z
482
- .enum(['tailwind', 'custom', 'bootstrap', 'chakra', 'shadcn'])
565
+ .enum(["tailwind", "custom", "bootstrap", "chakra", "shadcn"])
483
566
  .optional()
484
- .describe('CSS framework used in your project'),
567
+ .describe("CSS framework used in your project"),
485
568
  }, async ({ project_tokens, framework }) => {
486
569
  const lines = project_tokens
487
570
  .split(/[;\n,]+/)
@@ -491,101 +574,107 @@ export function registerToolsWithProvider(server, getRegistry) {
491
574
  const TOKEN_HINTS = [
492
575
  {
493
576
  patterns: [/brand|primary|main|accent-color/i],
494
- wsToken: '--ws-color-primary',
495
- description: 'Primary/brand color',
577
+ wsToken: "--ws-color-primary",
578
+ description: "Primary/brand color",
496
579
  },
497
580
  {
498
581
  patterns: [/brand.*hover|primary.*hover|primary.*dark/i],
499
- wsToken: '--ws-color-primary-hover',
500
- description: 'Primary hover state',
582
+ wsToken: "--ws-color-primary-hover",
583
+ description: "Primary hover state",
501
584
  },
502
585
  {
503
586
  patterns: [/brand.*light|primary.*light|primary.*50|brand.*50/i],
504
- wsToken: '--ws-color-primary-soft',
505
- description: 'Primary soft background',
587
+ wsToken: "--ws-color-primary-soft",
588
+ description: "Primary soft background",
506
589
  },
507
590
  {
508
591
  patterns: [/secondary|accent(?!-color)/i],
509
- wsToken: '--ws-color-accent',
510
- description: 'Secondary/accent color',
592
+ wsToken: "--ws-color-accent",
593
+ description: "Secondary/accent color",
511
594
  },
512
595
  {
513
596
  patterns: [/^bg$|background(?!.*secondary)|surface(?!.*alt)/i],
514
- wsToken: '--ws-color-surface',
515
- description: 'Page background',
597
+ wsToken: "--ws-color-surface",
598
+ description: "Page background",
516
599
  },
517
600
  {
518
- patterns: [/bg.*secondary|bg.*subtle|bg.*muted|surface.*alt|bg.*light/i],
519
- wsToken: '--ws-color-surface-alt',
520
- description: 'Subtle background',
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",
521
606
  },
522
607
  {
523
608
  patterns: [/^text$|text.*primary|foreground(?!.*muted)/i],
524
- wsToken: '--ws-color-text',
525
- description: 'Primary text color',
609
+ wsToken: "--ws-color-text",
610
+ description: "Primary text color",
526
611
  },
527
612
  {
528
- patterns: [/text.*secondary|text.*soft|text.*muted|muted.*foreground/i],
529
- wsToken: '--ws-color-text-muted',
530
- description: 'Muted text color',
613
+ patterns: [
614
+ /text.*secondary|text.*soft|text.*muted|muted.*foreground/i,
615
+ ],
616
+ wsToken: "--ws-color-text-muted",
617
+ description: "Muted text color",
531
618
  },
532
619
  {
533
620
  patterns: [/border(?!.*strong)|divider/i],
534
- wsToken: '--ws-color-border',
535
- description: 'Default border',
621
+ wsToken: "--ws-color-border",
622
+ description: "Default border",
536
623
  },
537
624
  {
538
625
  patterns: [/success|green|positive/i],
539
- wsToken: '--ws-color-success',
540
- description: 'Success color',
626
+ wsToken: "--ws-color-success",
627
+ description: "Success color",
541
628
  },
542
629
  {
543
630
  patterns: [/warning|amber|yellow|caution/i],
544
- wsToken: '--ws-color-warning',
545
- description: 'Warning color',
631
+ wsToken: "--ws-color-warning",
632
+ description: "Warning color",
546
633
  },
547
634
  {
548
635
  patterns: [/danger|error|red|destructive/i],
549
- wsToken: '--ws-color-danger',
550
- description: 'Danger/error color',
636
+ wsToken: "--ws-color-danger",
637
+ description: "Danger/error color",
551
638
  },
552
639
  {
553
640
  patterns: [/radius|rounded|corner/i],
554
- wsToken: '--ws-radius-md',
555
- description: 'Border radius',
641
+ wsToken: "--ws-radius-md",
642
+ description: "Border radius",
556
643
  },
557
644
  ];
558
645
  for (const line of lines) {
559
- const [rawName] = line.split(':').map((s) => s.trim());
560
- const name = rawName.replace(/^--color-|^--/, '');
646
+ const [rawName] = line.split(":").map((s) => s.trim());
647
+ const name = rawName.replace(/^--color-|^--/, "");
561
648
  for (const hint of TOKEN_HINTS) {
562
649
  if (hint.patterns.some((p) => p.test(name))) {
563
- const varRef = rawName.startsWith('--') ? `var(${rawName})` : rawName;
650
+ const varRef = rawName.startsWith("--")
651
+ ? `var(${rawName})`
652
+ : rawName;
564
653
  mappings.push(` ${hint.wsToken}: ${varRef}; /* ${hint.description} ← ${rawName} */`);
565
654
  break;
566
655
  }
567
656
  }
568
657
  }
569
658
  const frameworkNote = framework
570
- ? `\nFramework detected: ${framework}. ${framework === 'tailwind'
571
- ? 'Use @theme to register --ws-* tokens, or override in your main CSS.'
572
- : framework === 'shadcn'
573
- ? 'Map shadcn/ui --* variables to --ws-* in your globals.css.'
574
- : 'Override --ws-* tokens in your global stylesheet.'}\n`
575
- : '';
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
+ : "";
576
665
  const css = mappings.length > 0
577
- ? `:root {\n${mappings.join('\n')}\n}`
578
- : '/* No matching tokens detected. Provide CSS custom properties like:\n --color-brand: #2563eb; --color-bg: #ffffff */';
666
+ ? `:root {\n${mappings.join("\n")}\n}`
667
+ : "/* No matching tokens detected. Provide CSS custom properties like:\n --color-brand: #2563eb; --color-bg: #ffffff */";
579
668
  return {
580
669
  content: [
581
670
  {
582
- type: 'text',
671
+ type: "text",
583
672
  text: `# Recommended Token Mapping\n${frameworkNote}\n\`\`\`css\n${css}\n\`\`\`\n\nPaste this into your project's CSS, then import webspire-tokens.css.\nAdjust values as needed — these are suggestions based on naming conventions.\n\nFull token reference: https://webspire.de/tokens`,
584
673
  },
585
674
  ],
586
675
  };
587
676
  });
588
- server.tool('setup_tokens', 'Get the WebSpire token CSS files to write into your project. Returns the content of webspire-tokens.css (alias tokens + dark mode) and webspire-components.css (per-component tokens). Write these files to your project and import them in your main CSS.', {
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.", {
589
678
  components: z
590
679
  .array(z.string())
591
680
  .optional()
@@ -593,29 +682,31 @@ export function registerToolsWithProvider(server, getRegistry) {
593
682
  }, async ({ components }) => {
594
683
  const registry = await getRegistry();
595
684
  // Read bundled CSS files
596
- const { readFile } = await import('node:fs/promises');
597
- const { dirname, resolve } = await import('node:path');
598
- const { fileURLToPath } = await import('node:url');
685
+ const { readFile } = await import("node:fs/promises");
686
+ const { dirname, resolve } = await import("node:path");
687
+ const { fileURLToPath } = await import("node:url");
599
688
  const thisDir = dirname(fileURLToPath(import.meta.url));
600
689
  let tokensCss;
601
690
  let componentsCss;
602
691
  try {
603
- tokensCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-tokens.css'), 'utf-8');
604
- componentsCss = await readFile(resolve(thisDir, '..', 'css', 'webspire-components.css'), 'utf-8');
692
+ tokensCss = await readFile(resolve(thisDir, "..", "css", "webspire-tokens.css"), "utf-8");
693
+ componentsCss = await readFile(resolve(thisDir, "..", "css", "webspire-components.css"), "utf-8");
605
694
  }
606
695
  catch {
607
696
  return {
608
697
  content: [
609
698
  {
610
- type: 'text',
611
- text: 'Token CSS files not found in package. Update @webspire/mcp to the latest version.',
699
+ type: "text",
700
+ text: "Token CSS files not found in package. Update @webspire/mcp to the latest version.",
612
701
  },
613
702
  ],
614
703
  };
615
704
  }
616
705
  // Filter component tokens if specific families requested
617
706
  if (components && components.length > 0) {
618
- const filtered = ['/* Webspire Component Tokens (filtered) */'];
707
+ const filtered = [
708
+ "/* Webspire Component Tokens (filtered) */",
709
+ ];
619
710
  const sections = componentsCss.split(/(?=^\.ws-)/m);
620
711
  for (const section of sections) {
621
712
  const classMatch = section.match(/^\.ws-([a-z0-9-]+)/);
@@ -623,56 +714,56 @@ export function registerToolsWithProvider(server, getRegistry) {
623
714
  filtered.push(section.trim());
624
715
  }
625
716
  }
626
- componentsCss = filtered.join('\n\n');
717
+ componentsCss = filtered.join("\n\n");
627
718
  }
628
- const families = components ? components.join(', ') : 'all';
719
+ const families = components ? components.join(", ") : "all";
629
720
  const patternCount = registry.patterns?.length ?? 0;
630
721
  return {
631
722
  content: [
632
723
  {
633
- type: 'text',
724
+ type: "text",
634
725
  text: [
635
726
  `# WebSpire Token Setup (${families} families, ${patternCount} patterns)`,
636
- '',
637
- '## 1. Write `webspire-tokens.css` to your project',
638
- '',
639
- '```css',
727
+ "",
728
+ "## 1. Write `webspire-tokens.css` to your project",
729
+ "",
730
+ "```css",
640
731
  tokensCss,
641
- '```',
642
- '',
643
- '## 2. Write `webspire-components.css` to your project',
644
- '',
645
- '```css',
732
+ "```",
733
+ "",
734
+ "## 2. Write `webspire-components.css` to your project",
735
+ "",
736
+ "```css",
646
737
  componentsCss,
647
- '```',
648
- '',
649
- '## 3. Import in your main CSS',
650
- '',
651
- '```css',
738
+ "```",
739
+ "",
740
+ "## 3. Import in your main CSS",
741
+ "",
742
+ "```css",
652
743
  '@import "./webspire-tokens.css";',
653
744
  '@import "./webspire-components.css";',
654
- '```',
655
- '',
656
- '## 4. Override to match your brand',
657
- '',
658
- '```css',
659
- ':root {',
660
- ' --ws-color-primary: #your-brand-color;',
661
- ' --ws-color-primary-hover: #your-brand-darker;',
662
- '}',
663
- '```',
664
- '',
665
- 'Docs: https://webspire.de/tokens',
666
- ].join('\n'),
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"),
667
758
  },
668
759
  ],
669
760
  };
670
761
  });
671
762
  }
672
763
  export function registerResourcesWithProvider(server, getRegistry) {
673
- server.resource('categories', 'webspire://categories', {
674
- description: 'List all snippet categories with counts',
675
- mimeType: 'application/json',
764
+ server.resource("categories", "webspire://categories", {
765
+ description: "List all snippet categories with counts",
766
+ mimeType: "application/json",
676
767
  }, async () => {
677
768
  const registry = await getRegistry();
678
769
  const counts = {};
@@ -682,18 +773,18 @@ export function registerResourcesWithProvider(server, getRegistry) {
682
773
  return {
683
774
  contents: [
684
775
  {
685
- uri: 'webspire://categories',
686
- mimeType: 'application/json',
776
+ uri: "webspire://categories",
777
+ mimeType: "application/json",
687
778
  text: JSON.stringify(counts, null, 2),
688
779
  },
689
780
  ],
690
781
  };
691
782
  });
692
- server.resource('category', 'webspire://category/{name}', {
693
- description: 'List snippets in a specific category',
694
- mimeType: 'application/json',
783
+ server.resource("category", "webspire://category/{name}", {
784
+ description: "List snippets in a specific category",
785
+ mimeType: "application/json",
695
786
  }, async (uri) => {
696
- const name = uri.pathname.replace('//', '').split('/').pop();
787
+ const name = uri.pathname.replace("//", "").split("/").pop();
697
788
  const registry = await getRegistry();
698
789
  const snippets = registry.snippets.filter((s) => s.category === name);
699
790
  const brief = snippets.map((s) => ({
@@ -708,18 +799,18 @@ export function registerResourcesWithProvider(server, getRegistry) {
708
799
  contents: [
709
800
  {
710
801
  uri: uri.href,
711
- mimeType: 'application/json',
802
+ mimeType: "application/json",
712
803
  text: JSON.stringify(brief, null, 2),
713
804
  },
714
805
  ],
715
806
  };
716
807
  });
717
- server.resource('snippet', 'webspire://snippet/{id}', {
718
- description: 'Get full snippet data including CSS source',
719
- mimeType: 'application/json',
808
+ server.resource("snippet", "webspire://snippet/{id}", {
809
+ description: "Get full snippet data including CSS source",
810
+ mimeType: "application/json",
720
811
  }, async (uri) => {
721
- const parts = uri.pathname.replace('//', '').split('/');
722
- const id = parts.slice(1).join('/');
812
+ const parts = uri.pathname.replace("//", "").split("/");
813
+ const id = parts.slice(1).join("/");
723
814
  const registry = await getRegistry();
724
815
  const snippet = registry.snippets.find((s) => s.id === id);
725
816
  if (!snippet) {
@@ -727,7 +818,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
727
818
  contents: [
728
819
  {
729
820
  uri: uri.href,
730
- mimeType: 'text/plain',
821
+ mimeType: "text/plain",
731
822
  text: `Snippet "${id}" not found`,
732
823
  },
733
824
  ],
@@ -737,13 +828,13 @@ export function registerResourcesWithProvider(server, getRegistry) {
737
828
  contents: [
738
829
  {
739
830
  uri: uri.href,
740
- mimeType: 'application/json',
831
+ mimeType: "application/json",
741
832
  text: JSON.stringify(snippet, null, 2),
742
833
  },
743
834
  ],
744
835
  };
745
836
  });
746
- server.resource('patterns', 'webspire://patterns', { description: 'List all patterns', mimeType: 'application/json' }, async () => {
837
+ server.resource("patterns", "webspire://patterns", { description: "List all patterns", mimeType: "application/json" }, async () => {
747
838
  const registry = await getRegistry();
748
839
  const patterns = (registry.patterns ?? []).map((p) => ({
749
840
  id: p.id,
@@ -756,19 +847,19 @@ export function registerResourcesWithProvider(server, getRegistry) {
756
847
  return {
757
848
  contents: [
758
849
  {
759
- uri: 'webspire://patterns',
760
- mimeType: 'application/json',
850
+ uri: "webspire://patterns",
851
+ mimeType: "application/json",
761
852
  text: JSON.stringify(patterns, null, 2),
762
853
  },
763
854
  ],
764
855
  };
765
856
  });
766
- server.resource('pattern', 'webspire://pattern/{id}', {
767
- description: 'Get full pattern data including HTML source',
768
- mimeType: 'application/json',
857
+ server.resource("pattern", "webspire://pattern/{id}", {
858
+ description: "Get full pattern data including HTML source",
859
+ mimeType: "application/json",
769
860
  }, async (uri) => {
770
- const parts = uri.pathname.replace('//', '').split('/');
771
- const id = parts.slice(1).join('/');
861
+ const parts = uri.pathname.replace("//", "").split("/");
862
+ const id = parts.slice(1).join("/");
772
863
  const registry = await getRegistry();
773
864
  const pattern = (registry.patterns ?? []).find((p) => p.id === id);
774
865
  if (!pattern) {
@@ -776,7 +867,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
776
867
  contents: [
777
868
  {
778
869
  uri: uri.href,
779
- mimeType: 'text/plain',
870
+ mimeType: "text/plain",
780
871
  text: `Pattern "${id}" not found`,
781
872
  },
782
873
  ],
@@ -786,13 +877,13 @@ export function registerResourcesWithProvider(server, getRegistry) {
786
877
  contents: [
787
878
  {
788
879
  uri: uri.href,
789
- mimeType: 'application/json',
880
+ mimeType: "application/json",
790
881
  text: JSON.stringify(pattern, null, 2),
791
882
  },
792
883
  ],
793
884
  };
794
885
  });
795
- server.resource('templates', 'webspire://templates', { description: 'List all page templates', mimeType: 'application/json' }, async () => {
886
+ server.resource("templates", "webspire://templates", { description: "List all page templates", mimeType: "application/json" }, async () => {
796
887
  const registry = await getRegistry();
797
888
  const templates = (registry.templates ?? []).map((t) => ({
798
889
  id: t.id,
@@ -804,19 +895,19 @@ export function registerResourcesWithProvider(server, getRegistry) {
804
895
  return {
805
896
  contents: [
806
897
  {
807
- uri: 'webspire://templates',
808
- mimeType: 'application/json',
898
+ uri: "webspire://templates",
899
+ mimeType: "application/json",
809
900
  text: JSON.stringify(templates, null, 2),
810
901
  },
811
902
  ],
812
903
  };
813
904
  });
814
- server.resource('template', 'webspire://template/{id}', {
815
- description: 'Get full template data including HTML source',
816
- mimeType: 'application/json',
905
+ server.resource("template", "webspire://template/{id}", {
906
+ description: "Get full template data including HTML source",
907
+ mimeType: "application/json",
817
908
  }, async (uri) => {
818
- const parts = uri.pathname.replace('//', '').split('/');
819
- const id = parts.slice(1).join('/');
909
+ const parts = uri.pathname.replace("//", "").split("/");
910
+ const id = parts.slice(1).join("/");
820
911
  const registry = await getRegistry();
821
912
  const template = (registry.templates ?? []).find((t) => t.id === id);
822
913
  if (!template) {
@@ -824,7 +915,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
824
915
  contents: [
825
916
  {
826
917
  uri: uri.href,
827
- mimeType: 'text/plain',
918
+ mimeType: "text/plain",
828
919
  text: `Template "${id}" not found`,
829
920
  },
830
921
  ],
@@ -834,7 +925,7 @@ export function registerResourcesWithProvider(server, getRegistry) {
834
925
  contents: [
835
926
  {
836
927
  uri: uri.href,
837
- mimeType: 'application/json',
928
+ mimeType: "application/json",
838
929
  text: JSON.stringify(template, null, 2),
839
930
  },
840
931
  ],