@smwb/ui-mcp-solid 0.1.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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/component-meta/base/animateHeight/animateHeight.json +74 -0
  4. package/component-meta/base/icon/icon.json +61 -0
  5. package/component-meta/base/ripple/ripple.json +51 -0
  6. package/component-meta/clickAwayListener/clickAwayListener.json +40 -0
  7. package/component-meta/dataDisplay/accordion/accordion.json +79 -0
  8. package/component-meta/dataDisplay/avatar/avatar.avatarGroup.json +41 -0
  9. package/component-meta/dataDisplay/avatar/avatar.json +56 -0
  10. package/component-meta/dataDisplay/badge/badge.json +66 -0
  11. package/component-meta/dataDisplay/card/card.json +46 -0
  12. package/component-meta/dataDisplay/carousel/carousel.json +89 -0
  13. package/component-meta/dataDisplay/chat/chat.json +225 -0
  14. package/component-meta/dataDisplay/chat/chatAttachmentImage.json +27 -0
  15. package/component-meta/dataDisplay/chat/chatAttachments.json +32 -0
  16. package/component-meta/dataDisplay/chat/chatComposer.json +76 -0
  17. package/component-meta/dataDisplay/chat/chatHeader.json +60 -0
  18. package/component-meta/dataDisplay/chat/chatIconButton.json +38 -0
  19. package/component-meta/dataDisplay/chat/chatLayout.chatLayoutChat.json +20 -0
  20. package/component-meta/dataDisplay/chat/chatLayout.json +85 -0
  21. package/component-meta/dataDisplay/chat/chatMessageAvatar.json +34 -0
  22. package/component-meta/dataDisplay/chat/chatMessageBubble.json +91 -0
  23. package/component-meta/dataDisplay/chat/chatMessageIncoming.json +60 -0
  24. package/component-meta/dataDisplay/chat/chatMessageItem.json +65 -0
  25. package/component-meta/dataDisplay/chat/chatMessageOutgoing.json +60 -0
  26. package/component-meta/dataDisplay/chat/chatMessageSkeleton.json +39 -0
  27. package/component-meta/dataDisplay/chat/chatMessageStatus.json +44 -0
  28. package/component-meta/dataDisplay/chat/chatMessageSystem.json +29 -0
  29. package/component-meta/dataDisplay/chat/chatMessages.json +91 -0
  30. package/component-meta/dataDisplay/chat/chatOverlayLayout.chatOverlayLayoutChat.json +20 -0
  31. package/component-meta/dataDisplay/chat/chatOverlayLayout.json +70 -0
  32. package/component-meta/dataDisplay/chat/chatShell.json +71 -0
  33. package/component-meta/dataDisplay/chip/chip.json +104 -0
  34. package/component-meta/dataDisplay/expansionPanel/expansionPanel.json +85 -0
  35. package/component-meta/dataDisplay/imagesList/imagesList.json +57 -0
  36. package/component-meta/dataDisplay/imagesList/imagesListItem/imagesListItem.json +64 -0
  37. package/component-meta/dataDisplay/imagesList/imagesListItem/imagesListItemModal.json +32 -0
  38. package/component-meta/dataDisplay/list/list.json +36 -0
  39. package/component-meta/dataDisplay/list/listItem.json +62 -0
  40. package/component-meta/dataDisplay/table/table.json +59 -0
  41. package/component-meta/dataDisplay/table/tableBody.json +26 -0
  42. package/component-meta/dataDisplay/table/tableCell.json +31 -0
  43. package/component-meta/dataDisplay/table/tableHead.json +26 -0
  44. package/component-meta/dataDisplay/table/tableHeadCell.json +58 -0
  45. package/component-meta/dataDisplay/table/tablePagination/tablePagination.baseTablePagination.json +47 -0
  46. package/component-meta/dataDisplay/table/tablePagination/tablePagination.json +21 -0
  47. package/component-meta/dataDisplay/table/tableRow.json +36 -0
  48. package/component-meta/dataDisplay/treeView/treeView.json +93 -0
  49. package/component-meta/dataDisplay/typography/typography.json +56 -0
  50. package/component-meta/feedBack/message/message.json +53 -0
  51. package/component-meta/feedBack/modal/modal.json +92 -0
  52. package/component-meta/feedBack/modal/modal.modalSlot.json +22 -0
  53. package/component-meta/feedBack/progressIndicator/progressIndicator.circularProgress.json +22 -0
  54. package/component-meta/feedBack/progressIndicator/progressIndicator.json +57 -0
  55. package/component-meta/feedBack/skeleton/skeleton.json +61 -0
  56. package/component-meta/feedBack/snackbar/snackbar.json +71 -0
  57. package/component-meta/feedBack/snackbar/snackbar.snackbarsProvider.json +51 -0
  58. package/component-meta/feedBack/tooltip/tooltip.json +62 -0
  59. package/component-meta/inputs/button/button.json +94 -0
  60. package/component-meta/inputs/buttonGroups/buttonGroup.json +59 -0
  61. package/component-meta/inputs/checkbox/checkbox.json +89 -0
  62. package/component-meta/inputs/datePicker/dataPickerDays.json +87 -0
  63. package/component-meta/inputs/datePicker/dataPickerHeader.json +48 -0
  64. package/component-meta/inputs/datePicker/dataPickerInput.json +34 -0
  65. package/component-meta/inputs/datePicker/datePicker.json +129 -0
  66. package/component-meta/inputs/datePicker/datePickerActionLabel.json +55 -0
  67. package/component-meta/inputs/datePicker/datePickerDay.json +61 -0
  68. package/component-meta/inputs/datePicker/datePickerIconButton.json +18 -0
  69. package/component-meta/inputs/datePicker/datePickerList.json +33 -0
  70. package/component-meta/inputs/dateTimePicker/dateTimePicker.json +120 -0
  71. package/component-meta/inputs/dateTimePicker/dateTimePickerInput.json +35 -0
  72. package/component-meta/inputs/fileDrop/countPreview.json +40 -0
  73. package/component-meta/inputs/fileDrop/fileDrop.json +71 -0
  74. package/component-meta/inputs/fileDrop/preview.json +46 -0
  75. package/component-meta/inputs/fileDrop/previewWrapper.json +51 -0
  76. package/component-meta/inputs/fileInput/fileInput.json +61 -0
  77. package/component-meta/inputs/floatingButton/floatingButton.json +43 -0
  78. package/component-meta/inputs/radioButton/radioButton.json +90 -0
  79. package/component-meta/inputs/rating/rating.json +104 -0
  80. package/component-meta/inputs/selectField/dropdownMenu.json +107 -0
  81. package/component-meta/inputs/selectField/selectField.json +203 -0
  82. package/component-meta/inputs/slider/slider.json +133 -0
  83. package/component-meta/inputs/textField/textField.json +126 -0
  84. package/component-meta/inputs/textField/textFieldAdornment.json +47 -0
  85. package/component-meta/inputs/textField/textFieldWrapper.json +96 -0
  86. package/component-meta/inputs/timePicker/timePicker.json +75 -0
  87. package/component-meta/inputs/timePicker/timePickerColumns.json +37 -0
  88. package/component-meta/inputs/timePicker/timePickerInput.json +35 -0
  89. package/component-meta/inputs/timePicker/timePickerList.json +32 -0
  90. package/component-meta/inputs/toggle/toggle.json +90 -0
  91. package/component-meta/layout/appLayout/appLayout.json +88 -0
  92. package/component-meta/layout/divider/divider.json +56 -0
  93. package/component-meta/layout/grid/grid.column.json +81 -0
  94. package/component-meta/layout/grid/grid.container.json +36 -0
  95. package/component-meta/layout/grid/grid.row.json +51 -0
  96. package/component-meta/layout/page/page.json +41 -0
  97. package/component-meta/layout/pageHeader/pageHeader.json +53 -0
  98. package/component-meta/layout/screenDivider/screenDivider.json +57 -0
  99. package/component-meta/layout/sheet/sheet.json +114 -0
  100. package/component-meta/layout/stack/stack.json +56 -0
  101. package/component-meta/navigation/appBar/appBar.json +71 -0
  102. package/component-meta/navigation/bottomBar/bottomBar.bottomBarMenuItem.json +55 -0
  103. package/component-meta/navigation/bottomBar/bottomBar.json +61 -0
  104. package/component-meta/navigation/breadcrumbs/breadcrumbs.json +61 -0
  105. package/component-meta/navigation/menu/menu.json +20 -0
  106. package/component-meta/navigation/menu/menuDivider.json +26 -0
  107. package/component-meta/navigation/menu/menuFloating.json +51 -0
  108. package/component-meta/navigation/menu/menuItem.json +68 -0
  109. package/component-meta/navigation/menu/menuItemIcon.json +31 -0
  110. package/component-meta/navigation/menu/menuItemText.json +31 -0
  111. package/component-meta/navigation/menu/menuList.json +26 -0
  112. package/component-meta/navigation/menu/menuSubmenu.json +47 -0
  113. package/component-meta/navigation/pagination/pagination.json +98 -0
  114. package/component-meta/navigation/sidebar/sidebar.json +79 -0
  115. package/component-meta/navigation/sidebar/sidebar.sidebarMenuItem.json +56 -0
  116. package/component-meta/navigation/stepper/stepper.json +53 -0
  117. package/component-meta/navigation/tabs/tab/tab.json +64 -0
  118. package/component-meta/navigation/tabs/tabs.json +60 -0
  119. package/componentMeta.mjs +493 -0
  120. package/customizationMeta.mjs +451 -0
  121. package/package.json +37 -0
  122. package/server.mjs +169 -0
@@ -0,0 +1,451 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ compactText,
5
+ normalizeText,
6
+ resolveComponent,
7
+ searchComponents,
8
+ similarityScore,
9
+ splitCamelCase,
10
+ tokenize,
11
+ } from "./componentMeta.mjs";
12
+
13
+ const APPEARANCE_PROP_NAMES = new Set([
14
+ "variant",
15
+ "size",
16
+ "color",
17
+ "colorTheme",
18
+ "themeMode",
19
+ "animation",
20
+ "dense",
21
+ "fullWidth",
22
+ "backdrop",
23
+ "orientation",
24
+ "elevation",
25
+ "severity",
26
+ "type",
27
+ "inline",
28
+ "placement",
29
+ "scrollable",
30
+ "stackable",
31
+ "stacked",
32
+ "icon",
33
+ "deleteIcon",
34
+ "startAdornment",
35
+ "endAdornment",
36
+ "rippleCenter",
37
+ "disableRipple",
38
+ "showFirstButton",
39
+ "showLastButton",
40
+ "textAlign",
41
+ "withTicks",
42
+ "width",
43
+ "height",
44
+ "fill",
45
+ "weight",
46
+ "grade",
47
+ ]);
48
+
49
+ const GLOBAL_CUSTOMIZATION_TOPICS = [
50
+ {
51
+ id: "global/runtime-css",
52
+ name: "Runtime CSS Custom Properties",
53
+ description: "Change colors, spacing, radius, elevation, and component tokens at runtime without rebuilding CSS.",
54
+ summary:
55
+ "Override semantic `--smui-*` custom properties on `:root`, on `[data-theme=\"dark\"]`, or on any subtree.",
56
+ howTo: [
57
+ "Set CSS variables on `:root` or a scoped container.",
58
+ "Prefer `--smui-*` tokens over targeting internal `.smwb-*` classes.",
59
+ "Use component-specific tokens such as `--smui-tooltipBg` or `--smui-tableBorderColor` when needed.",
60
+ ],
61
+ examples: [
62
+ {
63
+ title: "Brand color",
64
+ language: "css",
65
+ code: ":root { --smui-primaryColor: #00695c; --smui-radius-md: 2px; }",
66
+ },
67
+ {
68
+ title: "Dense region",
69
+ language: "css",
70
+ code: ".settings-panel { --smui-space-md: 8px; --smui-space-lg: 12px; }",
71
+ },
72
+ ],
73
+ relatedDocs: ["AI_THEMING.md", "README.md"],
74
+ keywords: ["css", "variables", "tokens", "theme", "runtime", "custom properties", "smui", "colors", "spacing"],
75
+ },
76
+ {
77
+ id: "global/dark-mode",
78
+ name: "Light and Dark Theme",
79
+ description: "Switch the library between light and dark appearance using `data-theme` or `useTheme()`.",
80
+ summary: "Dark tokens are emitted under `:root[data-theme=\"dark\"]`.",
81
+ howTo: [
82
+ "Set `<html data-theme=\"dark\">` for SSR or static HTML.",
83
+ "Use `useTheme()` from `@smwb/ui-react` for client-side toggling with persistence.",
84
+ "Combine with runtime `--smui-*` overrides for brand-specific dark palettes.",
85
+ ],
86
+ examples: [
87
+ {
88
+ title: "Theme toggle",
89
+ language: "tsx",
90
+ code: "const { theme, setTheme } = useTheme();",
91
+ },
92
+ ],
93
+ relatedDocs: ["AI_THEMING.md", "README.md"],
94
+ keywords: ["dark", "light", "theme", "useTheme", "data-theme", "mode"],
95
+ },
96
+ {
97
+ id: "global/less-overrides",
98
+ name: "Compile-Time Less Overrides",
99
+ description: "Rebuild the stylesheet after changing design tokens in `theme.config` or a custom theme folder.",
100
+ summary: "Use Less token overrides for library-wide rebranding, density, radius, and per-component defaults.",
101
+ howTo: [
102
+ "Create `theme.config` at the Less compiler root.",
103
+ "Assign tokens such as `@primaryColor`, `@smui-spacing-unit`, `@cardPadding`.",
104
+ "Recompile `styles/less/components.less` and import the generated CSS in the app.",
105
+ ],
106
+ examples: [
107
+ {
108
+ title: "theme.config",
109
+ language: "less",
110
+ code: "@theme: \"summer\";\n@primaryColor: @md-teal-600;\n@smui-radius-md: 2px;",
111
+ },
112
+ ],
113
+ relatedDocs: ["AI_LESS_OVERRIDES.md", "README.md"],
114
+ keywords: ["less", "theme.config", "compile", "tokens", "rebrand", "override", "build"],
115
+ },
116
+ {
117
+ id: "global/component-props",
118
+ name: "Component Appearance Props",
119
+ description: "Customize a single instance through props such as `variant`, `color`, `size`, and component-specific options.",
120
+ summary: "This is the cheapest per-instance customization layer and requires no CSS rebuild.",
121
+ howTo: [
122
+ "Inspect the component `*Props` metadata or call `get_component_props`.",
123
+ "Use standardized props where available: `size`, `color`, `variant`.",
124
+ "Prefer props before writing custom CSS for one-off UI changes.",
125
+ ],
126
+ examples: [
127
+ {
128
+ title: "Button appearance",
129
+ language: "tsx",
130
+ code: "<Button variant=\"outlined\" color=\"primary\" size=\"large\" label=\"Save\" />",
131
+ },
132
+ ],
133
+ relatedDocs: ["AI_THEMING.md"],
134
+ keywords: ["props", "variant", "color", "size", "appearance", "instance"],
135
+ },
136
+ {
137
+ id: "global/classname-style",
138
+ name: "className and style",
139
+ description: "Most components accept `className` and `style` on the root element for local layout or minor visual tweaks.",
140
+ summary: "Use this for spacing, positioning, or small visual adjustments outside the design tokens.",
141
+ howTo: [
142
+ "Pass `className` for app-level layout or utility classes.",
143
+ "Pass `style` only for local exceptions; prefer tokens and props first.",
144
+ "Avoid relying on internal `.smwb-*` selectors when possible.",
145
+ ],
146
+ examples: [
147
+ {
148
+ title: "Local spacing",
149
+ language: "tsx",
150
+ code: "<Card className=\"my-panel\" style={{ marginTop: 16 }} />",
151
+ },
152
+ ],
153
+ relatedDocs: ["AI_THEMING.md", "README.md"],
154
+ keywords: ["className", "style", "css", "layout", "local"],
155
+ },
156
+ ];
157
+
158
+ function extractEnumValues(typeText) {
159
+ if (!typeText) {
160
+ return [];
161
+ }
162
+ const matches = typeText.match(/"([^"]+)"/g);
163
+ if (!matches) {
164
+ return [];
165
+ }
166
+ return matches.map((value) => value.slice(1, -1));
167
+ }
168
+
169
+ function resolveLessPath(id, packageRoot) {
170
+ const segments = id.split("/").filter(Boolean);
171
+ const candidates = [
172
+ path.join("styles/less/components", ...segments.slice(1), `${segments.at(-1)}.less`),
173
+ path.join("styles/less/components", segments.at(-1), `${segments.at(-1)}.less`),
174
+ path.join("styles/less/components", segments.slice(-2).join("/"), `${segments.at(-1)}.less`),
175
+ ];
176
+
177
+ for (const relativePath of candidates) {
178
+ if (fs.existsSync(path.join(packageRoot, relativePath))) {
179
+ return relativePath.replace(/\\/g, "/");
180
+ }
181
+ }
182
+
183
+ return candidates[0].replace(/\\/g, "/");
184
+ }
185
+
186
+ function buildAppearanceProps(entry) {
187
+ const appearance = {};
188
+
189
+ for (const [propName, propMeta] of Object.entries(entry.props ?? {})) {
190
+ if (!APPEARANCE_PROP_NAMES.has(propName)) {
191
+ continue;
192
+ }
193
+ appearance[propName] = {
194
+ type: propMeta.type,
195
+ required: propMeta.required,
196
+ description: propMeta.description,
197
+ values: extractEnumValues(propMeta.type),
198
+ };
199
+ }
200
+
201
+ return appearance;
202
+ }
203
+
204
+ function buildRecommendations(entry, appearanceProps, lessPath) {
205
+ const recommendations = [];
206
+
207
+ if (Object.keys(appearanceProps).length) {
208
+ recommendations.push("Start with component props such as variant, color, and size for instance-level customization.");
209
+ }
210
+ recommendations.push("Use runtime `--smui-*` CSS variables for live theme tweaks without rebuilding CSS.");
211
+ recommendations.push(`Inspect \`${lessPath}\` for component-specific Less tokens and runtime CSS variables.`);
212
+ recommendations.push("Use `theme.config` when the change should apply library-wide after recompiling Less.");
213
+ if (entry.inheritedAttributes) {
214
+ recommendations.push("The root element also accepts standard HTML attributes, plus `className` and `style`.");
215
+ }
216
+
217
+ return recommendations;
218
+ }
219
+
220
+ export function buildComponentCustomization(entry, packageRoot) {
221
+ const appearanceProps = buildAppearanceProps(entry);
222
+ const lessPath = resolveLessPath(entry.id, packageRoot);
223
+ const lessFileExists = fs.existsSync(path.join(packageRoot, lessPath));
224
+
225
+ return {
226
+ id: entry.id,
227
+ type: "component",
228
+ name: entry.name,
229
+ description: entry.description,
230
+ customization: {
231
+ props: appearanceProps,
232
+ runtimeCss: {
233
+ description: "Override `--smui-*` CSS custom properties globally or on a subtree.",
234
+ lessPath: lessFileExists ? lessPath : undefined,
235
+ inspect: lessFileExists
236
+ ? `Search for var(--smui- in ${lessPath}`
237
+ : "Search styles/less/variables.less for global --smui-* tokens.",
238
+ },
239
+ lessVariables: {
240
+ description: "Override component Less tokens in theme.config and recompile the stylesheet.",
241
+ lessPath: lessFileExists ? lessPath : undefined,
242
+ inspect: lessFileExists ? `Search for @ tokens in ${lessPath}` : "Search styles/less/default/globals/default.variables.less",
243
+ },
244
+ className: {
245
+ available: Boolean(entry.inheritedAttributes),
246
+ description: "Additional CSS class name(s) on the root element.",
247
+ },
248
+ style: {
249
+ available: Boolean(entry.inheritedAttributes),
250
+ description: "Inline CSS styles on the root element.",
251
+ },
252
+ },
253
+ recommendations: buildRecommendations(entry, appearanceProps, lessPath),
254
+ relatedDocs: ["AI_THEMING.md", "AI_LESS_OVERRIDES.md"],
255
+ };
256
+ }
257
+
258
+ function enrichGlobalTopic(topic) {
259
+ return {
260
+ ...topic,
261
+ type: "global",
262
+ searchText: normalizeText(
263
+ [topic.id, topic.name, topic.description, topic.summary, ...(topic.keywords ?? []), ...(topic.howTo ?? [])].join(" ")
264
+ ),
265
+ };
266
+ }
267
+
268
+ export function loadCustomizationCatalog(catalog, packageRoot) {
269
+ const globalTopics = GLOBAL_CUSTOMIZATION_TOPICS.map(enrichGlobalTopic);
270
+ const components = catalog.all.map((entry) => {
271
+ const customization = buildComponentCustomization(entry, packageRoot);
272
+ return {
273
+ ...customization,
274
+ searchText: normalizeText(
275
+ [
276
+ customization.id,
277
+ customization.name,
278
+ customization.description,
279
+ ...Object.keys(customization.customization.props),
280
+ ...customization.recommendations,
281
+ "customization",
282
+ "theme",
283
+ "variant",
284
+ "color",
285
+ "size",
286
+ "less",
287
+ "css",
288
+ ].join(" ")
289
+ ),
290
+ };
291
+ });
292
+
293
+ const byId = new Map();
294
+ const byName = new Map();
295
+
296
+ for (const topic of [...globalTopics, ...components]) {
297
+ byId.set(normalizeText(topic.id), topic);
298
+ byName.set(normalizeText(topic.name), topic);
299
+ }
300
+
301
+ return {
302
+ globalTopics,
303
+ components,
304
+ all: [...globalTopics, ...components],
305
+ byId,
306
+ byName,
307
+ };
308
+ }
309
+
310
+ function scoreTopic(topic, query, queryTokens, queryCompact) {
311
+ let score = 0;
312
+ const reasons = [];
313
+ const name = normalizeText(topic.name);
314
+ const id = normalizeText(topic.id);
315
+
316
+ if (name === query || id === query || id.endsWith(`/${query}`)) {
317
+ score += 1000;
318
+ reasons.push("exact-match");
319
+ }
320
+ if (compactText(name) === queryCompact || compactText(id) === queryCompact) {
321
+ score += 900;
322
+ reasons.push("compact-match");
323
+ }
324
+ if (name.includes(query) || id.includes(query) || topic.searchText.includes(query)) {
325
+ score += 420;
326
+ reasons.push("text-match");
327
+ }
328
+
329
+ for (const token of queryTokens) {
330
+ if (name.includes(token) || id.includes(token) || topic.searchText.includes(token)) {
331
+ score += 60;
332
+ reasons.push("token-match");
333
+ }
334
+ if (topic.type === "component" && Object.keys(topic.customization?.props ?? {}).includes(token)) {
335
+ score += 120;
336
+ reasons.push("prop-match");
337
+ }
338
+ }
339
+
340
+ const fuzzy = similarityScore(name, queryCompact);
341
+ if (queryCompact.length >= 3 && fuzzy >= 0.72) {
342
+ score += Math.round(fuzzy * 220);
343
+ reasons.push("fuzzy-match");
344
+ }
345
+
346
+ if (topic.type === "global" && ["theme", "less", "css", "dark", "props"].includes(queryCompact)) {
347
+ score += 80;
348
+ reasons.push("global-keyword");
349
+ }
350
+
351
+ return { score, reasons: [...new Set(reasons)] };
352
+ }
353
+
354
+ export function searchCustomization(catalog, customizationCatalog, query, limit = 10) {
355
+ const normalizedQuery = normalizeText(query ?? "");
356
+ if (!normalizedQuery) {
357
+ return customizationCatalog.all.slice(0, limit).map((topic) => summarizeCustomizationTopic(topic));
358
+ }
359
+
360
+ const queryTokens = tokenize(normalizedQuery);
361
+ const queryCompact = compactText(normalizedQuery);
362
+
363
+ return customizationCatalog.all
364
+ .map((topic) => ({ topic, ...scoreTopic(topic, normalizedQuery, queryTokens, queryCompact) }))
365
+ .filter((item) => item.score >= 50)
366
+ .sort(
367
+ (a, b) =>
368
+ b.score - a.score ||
369
+ (a.topic.type === "component" && b.topic.type === "global" ? -1 : 0) ||
370
+ a.topic.name.localeCompare(b.topic.name)
371
+ )
372
+ .slice(0, limit)
373
+ .map(({ topic, score, reasons }) => ({
374
+ ...summarizeCustomizationTopic(topic),
375
+ score,
376
+ matchReasons: reasons,
377
+ }));
378
+ }
379
+
380
+ function summarizeCustomizationTopic(topic) {
381
+ if (topic.type === "global") {
382
+ return {
383
+ id: topic.id,
384
+ type: topic.type,
385
+ name: topic.name,
386
+ description: topic.description,
387
+ summary: topic.summary,
388
+ };
389
+ }
390
+
391
+ return {
392
+ id: topic.id,
393
+ type: topic.type,
394
+ name: topic.name,
395
+ description: topic.description,
396
+ appearanceProps: Object.keys(topic.customization.props),
397
+ layers: ["props", "runtimeCss", "lessVariables", "className", "style"].filter((layer) => {
398
+ if (layer === "props") {
399
+ return Object.keys(topic.customization.props).length > 0;
400
+ }
401
+ if (layer === "className" || layer === "style") {
402
+ return topic.customization[layer]?.available;
403
+ }
404
+ return true;
405
+ }),
406
+ };
407
+ }
408
+
409
+ export function resolveCustomizationTopic(customizationCatalog, nameOrId) {
410
+ const key = normalizeText(nameOrId ?? "");
411
+ if (!key) {
412
+ return null;
413
+ }
414
+
415
+ return (
416
+ customizationCatalog.byId.get(key) ??
417
+ customizationCatalog.byName.get(key) ??
418
+ customizationCatalog.all.find((topic) => normalizeText(topic.name) === key) ??
419
+ null
420
+ );
421
+ }
422
+
423
+ export function getCustomization(catalog, customizationCatalog, nameOrId) {
424
+ const direct = resolveCustomizationTopic(customizationCatalog, nameOrId);
425
+ if (direct) {
426
+ return stripSearchText(direct);
427
+ }
428
+
429
+ const component = resolveComponent(catalog, nameOrId);
430
+ if (component) {
431
+ const built = customizationCatalog.components.find((item) => item.id === component.id);
432
+ if (built) {
433
+ return stripSearchText(built);
434
+ }
435
+ }
436
+
437
+ const [bestMatch] = searchCustomization(catalog, customizationCatalog, nameOrId, 1);
438
+ if (!bestMatch || bestMatch.score < 120) {
439
+ return null;
440
+ }
441
+
442
+ const resolved = resolveCustomizationTopic(customizationCatalog, bestMatch.id);
443
+ return resolved ? stripSearchText(resolved) : null;
444
+ }
445
+
446
+ function stripSearchText(topic) {
447
+ const { searchText: _searchText, ...rest } = topic;
448
+ return rest;
449
+ }
450
+
451
+ export { GLOBAL_CUSTOMIZATION_TOPICS };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@smwb/ui-mcp-solid",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol server exposing the @smwb/ui-solid (SolidJS) component catalog and customization guide.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "bin": {
11
+ "ui-mcp-solid": "./server.mjs"
12
+ },
13
+ "files": [
14
+ "server.mjs",
15
+ "componentMeta.mjs",
16
+ "customizationMeta.mjs",
17
+ "component-meta"
18
+ ],
19
+ "scripts": {
20
+ "build": "npm run build:meta",
21
+ "build:meta": "node scripts/generate-meta.mjs",
22
+ "start": "node server.mjs",
23
+ "test": "vitest run",
24
+ "jest": "vitest run"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.29.0",
28
+ "zod": "^4.4.3"
29
+ },
30
+ "devDependencies": {
31
+ "@smwb/ui-solid": "^0.1.0",
32
+ "@smwb/ui-styles": "^1.0.0",
33
+ "glob": "^13.0.6",
34
+ "typescript": "^6.0.3",
35
+ "vitest": "^2.1.9"
36
+ }
37
+ }
package/server.mjs ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { getComponentProps, loadCatalogFromPackageRoot, searchComponents } from "./componentMeta.mjs";
8
+ import { getCustomization, loadCustomizationCatalog, searchCustomization } from "./customizationMeta.mjs";
9
+
10
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
11
+ // server.mjs sits at the package root; metadata lives in ./component-meta.
12
+ const packageRoot = scriptDir;
13
+ const catalog = loadCatalogFromPackageRoot(packageRoot);
14
+ const customizationCatalog = loadCustomizationCatalog(catalog, packageRoot);
15
+
16
+ const server = new McpServer(
17
+ {
18
+ name: "ui-solid-components",
19
+ version: process.env.npm_package_version ?? "0.0.0",
20
+ },
21
+ {
22
+ instructions:
23
+ "@smwb/ui-solid component catalog and customization guide. Use search_components / get_component_props for API metadata, and search_customization / get_customization for theming and appearance options.",
24
+ }
25
+ );
26
+
27
+ server.registerTool(
28
+ "search_components",
29
+ {
30
+ title: "Search @smwb/ui-solid components",
31
+ description:
32
+ "Search the @smwb/ui-solid component catalog by exact, partial, fuzzy, or multi-word queries across names, ids, descriptions, and prop names.",
33
+ inputSchema: z.object({
34
+ query: z.string().describe("Search query, for example: button, chat, text field"),
35
+ limit: z.number().int().min(1).max(50).optional().describe("Maximum number of matches to return"),
36
+ }),
37
+ },
38
+ async ({ query, limit }) => {
39
+ const results = searchComponents(catalog, query, limit ?? 10);
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify({ count: results.length, results }, null, 2),
45
+ },
46
+ ],
47
+ structuredContent: { count: results.length, results },
48
+ };
49
+ }
50
+ );
51
+
52
+ server.registerTool(
53
+ "get_component_props",
54
+ {
55
+ title: "Get @smwb/ui-solid component props",
56
+ description:
57
+ "Return the full metadata document for a @smwb/ui-solid component, including prop names, types, required flags, descriptions, and required Less imports (`styles.base`, `styles.entries`, `styles.dependencies`, `styles.importExample`).",
58
+ inputSchema: z.object({
59
+ nameOrId: z
60
+ .string()
61
+ .describe("Exact component name (Button) or metadata id (inputs/button/button, dataDisplay/chat/chat)"),
62
+ }),
63
+ },
64
+ async ({ nameOrId }) => {
65
+ const result = getComponentProps(catalog, nameOrId);
66
+ if (!result) {
67
+ return {
68
+ isError: true,
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: JSON.stringify(
73
+ {
74
+ error: "Component not found",
75
+ nameOrId,
76
+ hint: "Call search_components first to discover valid ids and names.",
77
+ },
78
+ null,
79
+ 2
80
+ ),
81
+ },
82
+ ],
83
+ };
84
+ }
85
+
86
+ return {
87
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
88
+ structuredContent: result,
89
+ };
90
+ }
91
+ );
92
+
93
+ server.registerTool(
94
+ "search_customization",
95
+ {
96
+ title: "Search @smwb/ui-solid customization options",
97
+ description:
98
+ "Search global theming approaches and component-specific customization options by exact, partial, fuzzy, or multi-word queries.",
99
+ inputSchema: z.object({
100
+ query: z
101
+ .string()
102
+ .describe("Search query, for example: button color, dark theme, less override, runtime css"),
103
+ limit: z.number().int().min(1).max(50).optional().describe("Maximum number of matches to return"),
104
+ }),
105
+ },
106
+ async ({ query, limit }) => {
107
+ const results = searchCustomization(catalog, customizationCatalog, query, limit ?? 10);
108
+ return {
109
+ content: [{ type: "text", text: JSON.stringify({ count: results.length, results }, null, 2) }],
110
+ structuredContent: { count: results.length, results },
111
+ };
112
+ }
113
+ );
114
+
115
+ server.registerTool(
116
+ "get_customization",
117
+ {
118
+ title: "Get @smwb/ui-solid customization guide",
119
+ description:
120
+ "Return what and how can be customized for a component or a global theming topic, including props, runtime CSS variables, Less overrides, and recommendations.",
121
+ inputSchema: z.object({
122
+ nameOrId: z
123
+ .string()
124
+ .describe(
125
+ "Component name/id (Button, inputs/button/button) or global topic id (global/runtime-css, global/dark-mode, global/less-overrides)"
126
+ ),
127
+ }),
128
+ },
129
+ async ({ nameOrId }) => {
130
+ const result = getCustomization(catalog, customizationCatalog, nameOrId);
131
+ if (!result) {
132
+ return {
133
+ isError: true,
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify(
138
+ {
139
+ error: "Customization topic not found",
140
+ nameOrId,
141
+ hint: "Call search_customization first to discover valid component and global topic ids.",
142
+ },
143
+ null,
144
+ 2
145
+ ),
146
+ },
147
+ ],
148
+ };
149
+ }
150
+
151
+ return {
152
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
153
+ structuredContent: result,
154
+ };
155
+ }
156
+ );
157
+
158
+ async function main() {
159
+ const transport = new StdioServerTransport();
160
+ await server.connect(transport);
161
+ console.error(
162
+ `@smwb/ui-solid MCP server ready (${catalog.count} components, ${customizationCatalog.globalTopics.length} global customization topics loaded)`
163
+ );
164
+ }
165
+
166
+ main().catch((error) => {
167
+ console.error(error);
168
+ process.exit(1);
169
+ });