@rio-cloud/uikit-mcp 1.0.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 (198) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +91 -0
  3. package/bin/uikit-mcp.mjs +23 -0
  4. package/data/pages/Components/components/accentbar.json +207 -0
  5. package/data/pages/Components/components/activity.json +87 -0
  6. package/data/pages/Components/components/animatednumber.json +99 -0
  7. package/data/pages/Components/components/animations.json +87 -0
  8. package/data/pages/Components/components/appheader.json +291 -0
  9. package/data/pages/Components/components/applayout.json +1198 -0
  10. package/data/pages/Components/components/appnavigationbar.json +327 -0
  11. package/data/pages/Components/components/areacharts.json +563 -0
  12. package/data/pages/Components/components/aspectratioplaceholder.json +75 -0
  13. package/data/pages/Components/components/assettree.json +3080 -0
  14. package/data/pages/Components/components/autosuggests.json +710 -0
  15. package/data/pages/Components/components/avatar.json +157 -0
  16. package/data/pages/Components/components/banner.json +599 -0
  17. package/data/pages/Components/components/barcharts.json +1507 -0
  18. package/data/pages/Components/components/barlist.json +223 -0
  19. package/data/pages/Components/components/basicmap.json +68 -0
  20. package/data/pages/Components/components/bottomsheet.json +601 -0
  21. package/data/pages/Components/components/button.json +583 -0
  22. package/data/pages/Components/components/buttontoolbar.json +63 -0
  23. package/data/pages/Components/components/calendarstripe.json +235 -0
  24. package/data/pages/Components/components/card.json +69 -0
  25. package/data/pages/Components/components/carousel.json +39 -0
  26. package/data/pages/Components/components/chartcolors.json +34 -0
  27. package/data/pages/Components/components/chartsgettingstarted.json +32 -0
  28. package/data/pages/Components/components/chat.json +39 -0
  29. package/data/pages/Components/components/checkbox.json +847 -0
  30. package/data/pages/Components/components/clearableinput.json +789 -0
  31. package/data/pages/Components/components/collapse.json +175 -0
  32. package/data/pages/Components/components/composedcharts.json +159 -0
  33. package/data/pages/Components/components/contentloader.json +233 -0
  34. package/data/pages/Components/components/datatabs.json +680 -0
  35. package/data/pages/Components/components/datepickers.json +287 -0
  36. package/data/pages/Components/components/dialogs.json +1492 -0
  37. package/data/pages/Components/components/divider.json +93 -0
  38. package/data/pages/Components/components/dropdowns.json +936 -0
  39. package/data/pages/Components/components/editablecontent.json +1117 -0
  40. package/data/pages/Components/components/expander.json +377 -0
  41. package/data/pages/Components/components/fade.json +403 -0
  42. package/data/pages/Components/components/fadeexpander.json +75 -0
  43. package/data/pages/Components/components/fadeup.json +127 -0
  44. package/data/pages/Components/components/feedback.json +269 -0
  45. package/data/pages/Components/components/filepickers.json +269 -0
  46. package/data/pages/Components/components/formlabel.json +115 -0
  47. package/data/pages/Components/components/fullscreenmap.json +22 -0
  48. package/data/pages/Components/components/groupeditemlist.json +323 -0
  49. package/data/pages/Components/components/iconlist.json +45 -0
  50. package/data/pages/Components/components/imagepreloader.json +81 -0
  51. package/data/pages/Components/components/labeledelement.json +75 -0
  52. package/data/pages/Components/components/licenseplate.json +69 -0
  53. package/data/pages/Components/components/linecharts.json +987 -0
  54. package/data/pages/Components/components/listmenu.json +313 -0
  55. package/data/pages/Components/components/loadmore.json +175 -0
  56. package/data/pages/Components/components/mainnavigation.json +39 -0
  57. package/data/pages/Components/components/mapcircle.json +34 -0
  58. package/data/pages/Components/components/mapcluster.json +51 -0
  59. package/data/pages/Components/components/mapcontext.json +105 -0
  60. package/data/pages/Components/components/mapdraggablemarker.json +34 -0
  61. package/data/pages/Components/components/mapgettingstarted.json +27 -0
  62. package/data/pages/Components/components/mapgroup.json +1198 -0
  63. package/data/pages/Components/components/mapinfobubble.json +34 -0
  64. package/data/pages/Components/components/maplayergroup.json +34 -0
  65. package/data/pages/Components/components/mapmarker.json +700 -0
  66. package/data/pages/Components/components/mappolygon.json +45 -0
  67. package/data/pages/Components/components/maproute.json +623 -0
  68. package/data/pages/Components/components/maproutegenerator.json +16 -0
  69. package/data/pages/Components/components/mapsettings.json +51 -0
  70. package/data/pages/Components/components/maputils.json +34 -0
  71. package/data/pages/Components/components/multiselects.json +1451 -0
  72. package/data/pages/Components/components/nodata.json +139 -0
  73. package/data/pages/Components/components/notifications.json +65 -0
  74. package/data/pages/Components/components/numbercontrol.json +301 -0
  75. package/data/pages/Components/components/onboarding.json +302 -0
  76. package/data/pages/Components/components/page.json +197 -0
  77. package/data/pages/Components/components/pager.json +93 -0
  78. package/data/pages/Components/components/piecharts.json +731 -0
  79. package/data/pages/Components/components/popover.json +251 -0
  80. package/data/pages/Components/components/position.json +69 -0
  81. package/data/pages/Components/components/radialbarcharts.json +1304 -0
  82. package/data/pages/Components/components/radiobutton.json +1105 -0
  83. package/data/pages/Components/components/releasenotes.json +44 -0
  84. package/data/pages/Components/components/resizer.json +93 -0
  85. package/data/pages/Components/components/responsivecolumnstripe.json +123 -0
  86. package/data/pages/Components/components/responsivevideo.json +75 -0
  87. package/data/pages/Components/components/rioglyph.json +93 -0
  88. package/data/pages/Components/components/rules.json +410 -0
  89. package/data/pages/Components/components/saveableinput.json +703 -0
  90. package/data/pages/Components/components/selects.json +701 -0
  91. package/data/pages/Components/components/sidebar.json +243 -0
  92. package/data/pages/Components/components/sliders.json +235 -0
  93. package/data/pages/Components/components/smoothscrollbars.json +335 -0
  94. package/data/pages/Components/components/spinners.json +343 -0
  95. package/data/pages/Components/components/states.json +1705 -0
  96. package/data/pages/Components/components/statswidgets.json +314 -0
  97. package/data/pages/Components/components/statusbar.json +177 -0
  98. package/data/pages/Components/components/stepbutton.json +57 -0
  99. package/data/pages/Components/components/steppedprogressbars.json +417 -0
  100. package/data/pages/Components/components/subnavigation.json +107 -0
  101. package/data/pages/Components/components/supportmarker.json +45 -0
  102. package/data/pages/Components/components/svgimage.json +81 -0
  103. package/data/pages/Components/components/switch.json +111 -0
  104. package/data/pages/Components/components/tables.json +144 -0
  105. package/data/pages/Components/components/tagmanager.json +86 -0
  106. package/data/pages/Components/components/tags.json +146 -0
  107. package/data/pages/Components/components/teaser.json +188 -0
  108. package/data/pages/Components/components/timeline.json +45 -0
  109. package/data/pages/Components/components/timepicker.json +163 -0
  110. package/data/pages/Components/components/togglebutton.json +247 -0
  111. package/data/pages/Components/components/tooltip.json +270 -0
  112. package/data/pages/Components/components/virtuallist.json +175 -0
  113. package/data/pages/Foundations/foundations.json +2475 -0
  114. package/data/pages/Getting-started/start/changelog.json +22 -0
  115. package/data/pages/Getting-started/start/goodtoknow.json +32 -0
  116. package/data/pages/Getting-started/start/guidelines/color-combinations.json +58 -0
  117. package/data/pages/Getting-started/start/guidelines/custom-css.json +27 -0
  118. package/data/pages/Getting-started/start/guidelines/custom-rioglyph.json +22 -0
  119. package/data/pages/Getting-started/start/guidelines/formatting.json +97 -0
  120. package/data/pages/Getting-started/start/guidelines/iframe.json +93 -0
  121. package/data/pages/Getting-started/start/guidelines/obfuscate-data.json +22 -0
  122. package/data/pages/Getting-started/start/guidelines/print-css.json +37 -0
  123. package/data/pages/Getting-started/start/guidelines/spinner.json +144 -0
  124. package/data/pages/Getting-started/start/guidelines/supported-browsers.json +22 -0
  125. package/data/pages/Getting-started/start/guidelines/writing.json +242 -0
  126. package/data/pages/Getting-started/start/howto.json +72 -0
  127. package/data/pages/Getting-started/start/intro.json +37 -0
  128. package/data/pages/Getting-started/start/responsiveness.json +52 -0
  129. package/data/pages/Templates/templates/common-table.json +39 -0
  130. package/data/pages/Templates/templates/detail-views.json +71 -0
  131. package/data/pages/Templates/templates/expandable-details.json +39 -0
  132. package/data/pages/Templates/templates/feature-cards.json +103 -0
  133. package/data/pages/Templates/templates/form-summary.json +39 -0
  134. package/data/pages/Templates/templates/form-toggle.json +39 -0
  135. package/data/pages/Templates/templates/list-blocks.json +119 -0
  136. package/data/pages/Templates/templates/loading-progress.json +39 -0
  137. package/data/pages/Templates/templates/options-panel.json +39 -0
  138. package/data/pages/Templates/templates/panel-variants.json +39 -0
  139. package/data/pages/Templates/templates/progress-cards.json +71 -0
  140. package/data/pages/Templates/templates/progress-success.json +39 -0
  141. package/data/pages/Templates/templates/settings-form.json +39 -0
  142. package/data/pages/Templates/templates/stats-blocks.json +135 -0
  143. package/data/pages/Templates/templates/table-panel.json +39 -0
  144. package/data/pages/Templates/templates/table-row-animation.json +39 -0
  145. package/data/pages/Templates/templates/usage-cards.json +39 -0
  146. package/data/pages/Utilities/utilities/deviceutils.json +39 -0
  147. package/data/pages/Utilities/utilities/featuretoggles.json +42 -0
  148. package/data/pages/Utilities/utilities/fueltypeutils.json +118 -0
  149. package/data/pages/Utilities/utilities/routeutils.json +34 -0
  150. package/data/pages/Utilities/utilities/useaftermount.json +63 -0
  151. package/data/pages/Utilities/utilities/useaverage.json +86 -0
  152. package/data/pages/Utilities/utilities/useclickoutside.json +69 -0
  153. package/data/pages/Utilities/utilities/useclipboard.json +57 -0
  154. package/data/pages/Utilities/utilities/usecount.json +92 -0
  155. package/data/pages/Utilities/utilities/usedarkmode.json +50 -0
  156. package/data/pages/Utilities/utilities/usedebuginfo.json +63 -0
  157. package/data/pages/Utilities/utilities/useeffectonce.json +57 -0
  158. package/data/pages/Utilities/utilities/useelapsedtime.json +57 -0
  159. package/data/pages/Utilities/utilities/useelementsize.json +63 -0
  160. package/data/pages/Utilities/utilities/useesc.json +57 -0
  161. package/data/pages/Utilities/utilities/useevent.json +75 -0
  162. package/data/pages/Utilities/utilities/usefocustrap.json +57 -0
  163. package/data/pages/Utilities/utilities/usefullscreen.json +197 -0
  164. package/data/pages/Utilities/utilities/usehover.json +57 -0
  165. package/data/pages/Utilities/utilities/useinterval.json +63 -0
  166. package/data/pages/Utilities/utilities/useisfocuswithin.json +75 -0
  167. package/data/pages/Utilities/utilities/usekey.json +75 -0
  168. package/data/pages/Utilities/utilities/uselocalstorage.json +69 -0
  169. package/data/pages/Utilities/utilities/uselocationsuggestions.json +110 -0
  170. package/data/pages/Utilities/utilities/usemax.json +86 -0
  171. package/data/pages/Utilities/utilities/usemin.json +86 -0
  172. package/data/pages/Utilities/utilities/usemutationobserver.json +69 -0
  173. package/data/pages/Utilities/utilities/useonlinestatus.json +39 -0
  174. package/data/pages/Utilities/utilities/useonscreen.json +63 -0
  175. package/data/pages/Utilities/utilities/usepostmessage.json +80 -0
  176. package/data/pages/Utilities/utilities/useprevious.json +63 -0
  177. package/data/pages/Utilities/utilities/useresizeobserver.json +65 -0
  178. package/data/pages/Utilities/utilities/usescrollposition.json +103 -0
  179. package/data/pages/Utilities/utilities/usesearch.json +197 -0
  180. package/data/pages/Utilities/utilities/usesorting.json +139 -0
  181. package/data/pages/Utilities/utilities/usestatewithvalidation.json +69 -0
  182. package/data/pages/Utilities/utilities/usesum.json +86 -0
  183. package/data/pages/Utilities/utilities/usetableexport.json +87 -0
  184. package/data/pages/Utilities/utilities/usetableselection.json +311 -0
  185. package/data/pages/Utilities/utilities/usetimeout.json +63 -0
  186. package/data/pages/Utilities/utilities/usetoggle.json +75 -0
  187. package/data/pages/Utilities/utilities/usewindowresize.json +63 -0
  188. package/data/version.json +4 -0
  189. package/docs/content-schema.md +147 -0
  190. package/docs/navigation-inventory.json +1310 -0
  191. package/docs/search-synonyms.json +43 -0
  192. package/package.json +38 -0
  193. package/server/index.mjs +268 -0
  194. package/server/lib/load-docs.mjs +48 -0
  195. package/server/lib/normalise-doc.mjs +220 -0
  196. package/server/lib/render-markdown.mjs +82 -0
  197. package/server/lib/search-index.mjs +49 -0
  198. package/server/lib/types.js +99 -0
@@ -0,0 +1,43 @@
1
+ {
2
+ "synonyms": {
3
+ "install": ["installation", "setup", "how to", "howto", "getting started"],
4
+ "version": ["release"],
5
+ "button": ["btn", "cta", "buttons"],
6
+ "dropdown": ["select", "combobox"],
7
+ "select": ["dropdown", "combobox"],
8
+ "table": ["datagrid", "grid"],
9
+ "datagrid": ["table", "grid"],
10
+ "form": ["input", "field"],
11
+ "modal": ["dialog"],
12
+ "dialog": ["modal"],
13
+ "toast": ["alert", "notification"],
14
+ "alert": ["toast", "notification"],
15
+ "datepicker": ["date picker", "calendar"],
16
+ "date picker": ["datepicker", "calendar"],
17
+ "accordion": ["expander"],
18
+ "expander": ["accordion"],
19
+ "chip": ["tag", "badge"],
20
+ "tag": ["chip", "badge"]
21
+ },
22
+ "preferredDocIds": {
23
+ "install": ["start/howto"],
24
+ "getting started": ["start/howto"],
25
+ "setup": ["start/howto"],
26
+ "version": ["start/howto"],
27
+ "button": ["components/button"],
28
+ "dropdown": ["components/dropdowns"],
29
+ "select": ["components/dropdowns"],
30
+ "table": ["components/tables"],
31
+ "datagrid": ["components/tables"],
32
+ "modal": ["components/dialogs"],
33
+ "dialog": ["components/dialogs"],
34
+ "toast": ["components/notifications"],
35
+ "alert": ["components/notifications"],
36
+ "datepicker": ["components/datepickers"],
37
+ "date picker": ["components/datepickers"],
38
+ "accordion": ["components/expander"],
39
+ "expander": ["components/expander"],
40
+ "chip": ["components/tags"],
41
+ "tag": ["components/tags"]
42
+ }
43
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@rio-cloud/uikit-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that serves the captured RIO UI Kit documentation to Model Context Protocol clients.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "author": "TB Digital Services GmbH",
10
+ "license": "Apache-2.0",
11
+ "bin": {
12
+ "uikit-mcp": "./bin/uikit-mcp.mjs"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "server/",
17
+ "data/",
18
+ "docs/",
19
+ "package.json",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "test": "node --test",
25
+ "setup:crawler": "npx playwright install chromium",
26
+ "crawl:full": "node tools/crawl-full.mjs",
27
+ "crawl:navigation": "node crawler/cli/crawl-navigation.mjs",
28
+ "capture:all": "node crawler/cli/capture-all.mjs",
29
+ "mcp:server": "node server/index.mjs",
30
+ "link:global": "npm link"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.20.2",
34
+ "flexsearch": "^0.8.212",
35
+ "playwright": "^1.45.0",
36
+ "zod": "^3.25.76"
37
+ }
38
+ }
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { z } from 'zod';
8
+
9
+ import { loadDocumentation } from './lib/load-docs.mjs';
10
+ import { renderDocMarkdown } from './lib/render-markdown.mjs';
11
+ import { buildSearchIndex } from './lib/search-index.mjs';
12
+
13
+ function readCapturedVersion() {
14
+ try {
15
+ const versionPath = path.resolve(new URL('../data/version.json', import.meta.url).pathname);
16
+ return JSON.parse(readFileSync(versionPath, 'utf8'));
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function loadSearchSynonyms() {
23
+ try {
24
+ const synonymsPath = path.resolve(new URL('../docs/search-synonyms.json', import.meta.url).pathname);
25
+ return JSON.parse(readFileSync(synonymsPath, 'utf8'));
26
+ } catch (error) {
27
+ console.warn('Warning: failed to load search synonyms:', error.message);
28
+ return { synonyms: {}, preferredDocIds: {} };
29
+ }
30
+ }
31
+
32
+ const VERSION_INFO = readCapturedVersion();
33
+ const CURRENT_UIKIT_VERSION = VERSION_INFO?.version ?? null;
34
+ const SEARCH_SYNONYMS = loadSearchSynonyms();
35
+
36
+ const server = new McpServer(
37
+ {
38
+ name: 'uikit-mcp-server',
39
+ version: '0.1.0'
40
+ },
41
+ {
42
+ instructions: `UI Kit MCP usage guidelines:
43
+ - Always discover documentation with search_docs; listResources is a static inventory and must not be used for guessing. Call readResource on the returned uikit-doc:// URI before using anything.
44
+ - Coding rules: import components from '@rio-cloud/rio-uikit', prefer styling via provided classes instead of custom CSS files, and wrap the app/content in ApplicationLayout.
45
+ - Follow documented props tables and examples; avoid inventing undocumented variants.
46
+ - Treat the captured metadata (Category, Section, Captured) as the source of truth. If you need newer upstream info, say so explicitly—live crawling/fetching is not available here.
47
+ - This server serves offline captures only; do not attempt to run Playwright or crawling scripts from this MCP session.
48
+ - Captured UI Kit version: ${CURRENT_UIKIT_VERSION ?? 'unknown'} (from data/version.json). Ask if you require a newer version.`
49
+ }
50
+ );
51
+
52
+ function buildResourceUri(docId) {
53
+ return new URL(`uikit-doc://${docId}`);
54
+ }
55
+
56
+ function expandDomainSynonyms(rawQuery) {
57
+ const config = SEARCH_SYNONYMS;
58
+ const query = rawQuery.toLowerCase();
59
+ const expansions = new Set();
60
+ const preferred = new Set();
61
+ const hints = new Set();
62
+
63
+ for (const [key, values] of Object.entries(config.synonyms ?? {})) {
64
+ if (query.includes(key.toLowerCase())) {
65
+ (values ?? []).forEach((value) => {
66
+ if (value.toLowerCase() !== query) {
67
+ expansions.add(value);
68
+ }
69
+ });
70
+ const ids = config.preferredDocIds?.[key] ?? [];
71
+ ids.forEach((id) => preferred.add(id));
72
+ if (config.hints?.[key]) {
73
+ hints.add(config.hints[key]);
74
+ }
75
+ }
76
+ }
77
+
78
+ return {
79
+ expansions: Array.from(expansions),
80
+ preferredDocIds: Array.from(preferred),
81
+ hints: Array.from(hints)
82
+ };
83
+ }
84
+
85
+ function registerDocResources(docs) {
86
+ for (const doc of docs.values()) {
87
+ const uri = buildResourceUri(doc.id);
88
+ const versionNote = CURRENT_UIKIT_VERSION
89
+ ? `\n\n> Captured UI Kit version: ${CURRENT_UIKIT_VERSION}`
90
+ : '';
91
+ const markdown = renderDocMarkdown(doc) + versionNote;
92
+
93
+ server.registerResource(
94
+ doc.id,
95
+ uri.href,
96
+ {
97
+ title: doc.title ?? doc.id,
98
+ description: doc.summary,
99
+ mimeType: 'text/markdown'
100
+ },
101
+ async () => ({
102
+ contents: [
103
+ {
104
+ uri: uri.href,
105
+ mimeType: 'text/markdown',
106
+ text: markdown
107
+ }
108
+ ]
109
+ })
110
+ );
111
+ }
112
+
113
+ console.error(`Registered ${docs.size} documentation resources.`);
114
+ }
115
+
116
+ function registerSearchTool(docs, index) {
117
+ const inputSchema = {
118
+ query: z.string().min(1, 'Query is required'),
119
+ limit: z.number().int().min(1).max(20).optional()
120
+ };
121
+
122
+ const outputSchema = {
123
+ results: z.array(
124
+ z.object({
125
+ resource: z.string(),
126
+ title: z.string(),
127
+ summary: z.string().optional(),
128
+ category: z.string().nullish(),
129
+ section: z.string().nullish()
130
+ })
131
+ )
132
+ };
133
+
134
+ server.registerTool(
135
+ 'search_docs',
136
+ {
137
+ title: 'Search UI Kit docs',
138
+ description:
139
+ 'Full-text search across the captured UI Kit documentation. Always call this before readResource; it is the only supported way to discover resource IDs.',
140
+ inputSchema,
141
+ outputSchema
142
+ },
143
+ async ({ query, limit = 5 }) => {
144
+ const searchQueue = [query];
145
+ const domainSynonyms = expandDomainSynonyms(query);
146
+ for (const extra of domainSynonyms.expansions) {
147
+ searchQueue.push(extra);
148
+ }
149
+
150
+ const seen = new Set();
151
+ const resolved = [];
152
+ const preferredDocIds = [];
153
+ const hints = [];
154
+
155
+ preferredDocIds.push(...domainSynonyms.preferredDocIds);
156
+ hints.push(...domainSynonyms.hints);
157
+
158
+ for (const id of preferredDocIds) {
159
+ if (seen.has(id)) continue;
160
+ const doc = docs.get(id);
161
+ if (!doc) continue;
162
+ seen.add(id);
163
+ resolved.push({
164
+ resource: buildResourceUri(id).href,
165
+ title: doc.title ?? id,
166
+ summary: doc.summary ?? '',
167
+ category: doc.category ?? null,
168
+ section: doc.section ?? null
169
+ });
170
+ if (resolved.length >= limit) break;
171
+ }
172
+
173
+ for (const searchQuery of searchQueue) {
174
+ const hits = index.search(searchQuery, {
175
+ limit: Math.max(limit * 2, 10),
176
+ suggest: true
177
+ });
178
+ for (const group of hits) {
179
+ for (const id of group.result ?? []) {
180
+ if (seen.has(id)) continue;
181
+ seen.add(id);
182
+ const doc = docs.get(id);
183
+ if (!doc) continue;
184
+ resolved.push({
185
+ resource: buildResourceUri(id).href,
186
+ title: doc.title ?? id,
187
+ summary: doc.summary ?? '',
188
+ category: doc.category ?? null,
189
+ section: doc.section ?? null
190
+ });
191
+ if (resolved.length >= limit) break;
192
+ }
193
+ if (resolved.length >= limit) break;
194
+ }
195
+ if (resolved.length >= limit) break;
196
+ }
197
+
198
+ let text =
199
+ resolved.length > 0
200
+ ? resolved
201
+ .map(
202
+ (entry, idx) =>
203
+ `${idx + 1}. ${entry.title} (${entry.resource})${
204
+ entry.summary ? `\n ${entry.summary}` : ''
205
+ }`
206
+ )
207
+ .join('\n\n')
208
+ .concat(
209
+ '\n\nAlways call readResource on one of the URIs above instead of guessing component routes.'
210
+ )
211
+ : `No matches for "${query}".`;
212
+
213
+ const uniqueHints = Array.from(new Set(hints.filter(Boolean)));
214
+ if (uniqueHints.length) {
215
+ text += `\n\n${uniqueHints.join('\n')}`;
216
+ }
217
+
218
+ if (CURRENT_UIKIT_VERSION) {
219
+ text += `\n\nCaptured UI Kit version: ${CURRENT_UIKIT_VERSION}. Read "How to use the UIKIT" → "Install the npm package" for installation details.`;
220
+ }
221
+
222
+ return {
223
+ structuredContent: { results: resolved },
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text
228
+ }
229
+ ]
230
+ };
231
+ }
232
+ );
233
+ }
234
+
235
+ async function initialiseServer() {
236
+ const docs = await loadDocumentation();
237
+ const index = buildSearchIndex(docs);
238
+ registerDocResources(docs);
239
+ registerSearchTool(docs, index);
240
+
241
+ const docIds = new Set(docs.keys());
242
+ const missingPreferred = new Set();
243
+ for (const ids of Object.values(SEARCH_SYNONYMS.preferredDocIds ?? {})) {
244
+ for (const id of ids ?? []) {
245
+ if (!docIds.has(id)) {
246
+ missingPreferred.add(id);
247
+ }
248
+ }
249
+ }
250
+ if (missingPreferred.size) {
251
+ console.warn(
252
+ `Warning: preferredDocIds not found in dataset: ${Array.from(missingPreferred).join(', ')}`
253
+ );
254
+ }
255
+
256
+ return { docs, index };
257
+ }
258
+
259
+ async function main() {
260
+ await initialiseServer();
261
+ const transport = new StdioServerTransport();
262
+ await server.connect(transport);
263
+ }
264
+
265
+ main().catch((error) => {
266
+ console.error(error);
267
+ process.exit(1);
268
+ });
@@ -0,0 +1,48 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { readdirSync, statSync } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { normaliseDocument } from './normalise-doc.mjs';
6
+
7
+ const DATA_ROOT = path.resolve(new URL('../../data/pages', import.meta.url).pathname);
8
+
9
+ /**
10
+ * Recursively collect JSON file paths.
11
+ * @param {string} dir
12
+ * @returns {string[]}
13
+ */
14
+ function walk(dir) {
15
+ const entries = readdirSync(dir);
16
+ const files = [];
17
+ for (const entry of entries) {
18
+ const fullPath = path.join(dir, entry);
19
+ const stats = statSync(fullPath);
20
+ if (stats.isDirectory()) {
21
+ files.push(...walk(fullPath));
22
+ } else if (stats.isFile() && entry.endsWith('.json')) {
23
+ files.push(fullPath);
24
+ }
25
+ }
26
+ return files;
27
+ }
28
+
29
+ /**
30
+ * Load all documentation artefacts and return a map keyed by doc id.
31
+ * @param {{ root?: string } } [options]
32
+ * @returns {Promise<Map<string, UiKitDoc>>}
33
+ */
34
+ export async function loadDocumentation(options = {}) {
35
+ const root = options.root ? path.resolve(options.root) : DATA_ROOT;
36
+ const files = walk(root);
37
+ const docs = new Map();
38
+
39
+ for (const file of files) {
40
+ const rawText = await readFile(file, 'utf8');
41
+ const rawDoc = JSON.parse(rawText);
42
+ const normalised = normaliseDocument(rawDoc);
43
+ docs.set(normalised.id, normalised);
44
+ }
45
+
46
+ return docs;
47
+ }
48
+
@@ -0,0 +1,220 @@
1
+ import './types.js';
2
+
3
+ /**
4
+ * Collapse excessive whitespace and normalise newlines.
5
+ * @param {string} value
6
+ * @returns {string}
7
+ */
8
+ function normaliseWhitespace(value) {
9
+ return value
10
+ .replace(/\r\n?/g, '\n')
11
+ .split('\n')
12
+ .map((line) => line.trim())
13
+ .filter((line, index, all) => line.length > 0 || (index > 0 && all[index - 1].length > 0))
14
+ .join('\n')
15
+ .trim();
16
+ }
17
+
18
+ const BLOCK_BREAK_REGEXP = /<\/(p|div|section|article|li|ul|ol|pre|table|thead|tbody|tr|h[1-6])>/gi;
19
+ const BR_REGEXP = /<br\s*\/?>/gi;
20
+ const TAG_REGEXP = /<[^>]+>/g;
21
+ const ENTITY_MAP = new Map([
22
+ ['&nbsp;', ' '],
23
+ ['&amp;', '&'],
24
+ ['&lt;', '<'],
25
+ ['&gt;', '>'],
26
+ ['&quot;', '"'],
27
+ ['&#39;', "'"],
28
+ ['&apos;', "'"]
29
+ ]);
30
+
31
+ /**
32
+ * Convert HTML into best-effort plaintext suitable for indexing.
33
+ * @param {string} html
34
+ * @returns {string}
35
+ */
36
+ export function htmlToPlainText(html) {
37
+ if (!html) return '';
38
+ let value = html;
39
+ value = value.replace(BLOCK_BREAK_REGEXP, '\n');
40
+ value = value.replace(BR_REGEXP, '\n');
41
+ value = value.replace(TAG_REGEXP, '');
42
+ for (const [entity, replacement] of ENTITY_MAP.entries()) {
43
+ value = value.replace(new RegExp(entity, 'g'), replacement);
44
+ }
45
+ return normaliseWhitespace(value);
46
+ }
47
+
48
+ /**
49
+ * Convert rudimentary markdown-ish copy into plain text.
50
+ * @param {string} markdown
51
+ * @returns {string}
52
+ */
53
+ function markdownToPlainText(markdown) {
54
+ if (!markdown) return '';
55
+ return normaliseWhitespace(
56
+ markdown
57
+ .replace(/`{1,3}([^`]+)`{1,3}/g, '$1')
58
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
59
+ .replace(/\*([^*]+)\*/g, '$1')
60
+ .replace(/__([^_]+)__|_([^_]+)_/g, (_, bold, italic) => bold ?? italic ?? '')
61
+ .replace(/> ?/g, '')
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Escape Markdown table cell content.
67
+ * @param {string} value
68
+ * @returns {string}
69
+ */
70
+ function escapeTableCell(value) {
71
+ return value
72
+ .replace(/\|/g, '\\|')
73
+ .replace(/\n+/g, '<br />')
74
+ .trim();
75
+ }
76
+
77
+ /**
78
+ * Turn props table data into Markdown.
79
+ * @param {RawPropsTable} table
80
+ * @returns {string}
81
+ */
82
+ function propsTableToMarkdown(table) {
83
+ const header = '| Name | Type | Default | Description |';
84
+ const divider = '| --- | --- | --- | --- |';
85
+ const rows = table.rows
86
+ .map((row) => {
87
+ const name = escapeTableCell(row.name);
88
+ const type = escapeTableCell(row.type);
89
+ const defaultValue = escapeTableCell(row.default || '—');
90
+ const description = escapeTableCell(row.description);
91
+ return `| ${name} | ${type} | ${defaultValue} | ${description} |`;
92
+ })
93
+ .join('\n');
94
+
95
+ const heading = table.heading ? `### ${table.heading}\n\n` : '';
96
+ return `${heading}${header}\n${divider}\n${rows}`.trim();
97
+ }
98
+
99
+ /**
100
+ * Derive hash route and identifier from metadata.
101
+ * @param {RawDoc['metadata']} metadata
102
+ * @returns {{ route: string, id: string }}
103
+ */
104
+ function deriveRoute(metadata) {
105
+ const source = metadata?.source ?? '';
106
+ const hashIndex = source.indexOf('#');
107
+ const fallback = metadata?.slug ? `#${metadata.slug}` : '';
108
+ const route = hashIndex >= 0 ? source.slice(hashIndex) : fallback;
109
+ const id = route.startsWith('#') ? route.slice(1) : route;
110
+ return { route, id };
111
+ }
112
+
113
+ /**
114
+ * Build a normalised document ready for MCP consumption.
115
+ * @param {RawDoc} raw
116
+ * @returns {UiKitDoc}
117
+ */
118
+ export function normaliseDocument(raw) {
119
+ if (!raw || typeof raw !== 'object') {
120
+ throw new TypeError('Expected raw document object');
121
+ }
122
+
123
+ const { route, id } = deriveRoute(raw.metadata ?? {});
124
+ const title = raw.title ?? null;
125
+ const sections = [];
126
+ const textAccumulator = [];
127
+
128
+ if (title) textAccumulator.push(title);
129
+ if (raw.lead) textAccumulator.push(raw.lead);
130
+
131
+ for (const section of raw.content ?? []) {
132
+ const heading = section.heading ?? '';
133
+ const bodyMarkdown = section.body ? normaliseWhitespace(section.body) : '';
134
+ const bodyPlainText = markdownToPlainText(bodyMarkdown);
135
+
136
+ if (heading) textAccumulator.push(heading);
137
+ if (bodyPlainText) textAccumulator.push(bodyPlainText);
138
+
139
+ const examples = [];
140
+ for (const example of section.examples ?? []) {
141
+ const caption = example.caption ?? heading ?? title ?? 'Example';
142
+ const html = example.rendered_html ?? '';
143
+ const plainText = htmlToPlainText(html);
144
+ if (plainText) textAccumulator.push(plainText);
145
+
146
+ const codeTabs = [];
147
+ const propsTables = [];
148
+
149
+ for (const tab of example.tabs ?? []) {
150
+ if (tab.code && tab.code.trim().length) {
151
+ codeTabs.push({
152
+ label: tab.label,
153
+ language: tab.language,
154
+ code: tab.code.trim()
155
+ });
156
+ }
157
+
158
+ if (Array.isArray(tab.props) && tab.props.length) {
159
+ for (const table of tab.props) {
160
+ const markdown = propsTableToMarkdown(table);
161
+ propsTables.push({
162
+ heading: table.heading ?? null,
163
+ rows: table.rows,
164
+ markdown
165
+ });
166
+ textAccumulator.push(markdownToPlainText(markdown));
167
+ }
168
+ }
169
+ }
170
+
171
+ examples.push({
172
+ caption,
173
+ html,
174
+ plainText,
175
+ codeTabs,
176
+ propsTables
177
+ });
178
+ }
179
+
180
+ sections.push({
181
+ heading,
182
+ bodyMarkdown,
183
+ bodyPlainText,
184
+ examples
185
+ });
186
+ }
187
+
188
+ const summaryCandidate =
189
+ raw.lead ||
190
+ (sections.find((section) => section.bodyPlainText)?.bodyPlainText ?? '') ||
191
+ (sections.find((section) => section.examples.length)?.examples[0].plainText ?? '');
192
+
193
+ const summary = summaryCandidate
194
+ ? summaryCandidate.split('\n')[0]
195
+ : `Documentation entry for ${id}`;
196
+
197
+ const seeAlso = (raw.see_also ?? []).map((link) => ({
198
+ label: link.label,
199
+ href: link.href
200
+ }));
201
+
202
+ const searchText = normaliseWhitespace(textAccumulator.join('\n'));
203
+
204
+ return {
205
+ id,
206
+ route,
207
+ category: raw.metadata?.category ?? null,
208
+ section: raw.metadata?.section ?? null,
209
+ title,
210
+ lead: raw.lead ?? null,
211
+ sections,
212
+ seeAlso,
213
+ sourceUrl: raw.metadata?.source ?? '',
214
+ capturedAt: raw.metadata?.captured_at ?? '',
215
+ slug: raw.metadata?.slug ?? id,
216
+ summary,
217
+ searchText
218
+ };
219
+ }
220
+
@@ -0,0 +1,82 @@
1
+ import './types.js';
2
+ import { htmlToPlainText } from './normalise-doc.mjs';
3
+
4
+ function heading(level, text) {
5
+ const prefix = '#'.repeat(level);
6
+ return `${prefix} ${text}`.trim();
7
+ }
8
+
9
+ function codeBlock(language, code) {
10
+ const fence = '```';
11
+ return `${fence}${language ?? ''}\n${code}\n${fence}`;
12
+ }
13
+
14
+ /**
15
+ * Convert a UiKitDoc to Markdown for MCP resource responses.
16
+ * @param {UiKitDoc} doc
17
+ * @returns {string}
18
+ */
19
+ export function renderDocMarkdown(doc) {
20
+ const parts = [];
21
+
22
+ const title = doc.title ?? doc.id;
23
+ parts.push(heading(1, title));
24
+
25
+ parts.push([
26
+ `*Category:* ${doc.category ?? '—'}`,
27
+ `*Section:* ${doc.section ?? '—'}`,
28
+ `*Source:* ${doc.sourceUrl}`,
29
+ `*Captured:* ${doc.capturedAt}`
30
+ ].join('\n'));
31
+
32
+ if (doc.lead) {
33
+ parts.push(doc.lead.trim());
34
+ }
35
+
36
+ for (const section of doc.sections) {
37
+ if (section.heading) {
38
+ parts.push(heading(2, section.heading));
39
+ }
40
+
41
+ if (section.bodyMarkdown) {
42
+ parts.push(section.bodyMarkdown);
43
+ }
44
+
45
+ for (const example of section.examples) {
46
+ parts.push(heading(3, `Example: ${example.caption}`));
47
+
48
+ if (example.plainText) {
49
+ parts.push(example.plainText);
50
+ } else if (example.html) {
51
+ parts.push(htmlToPlainText(example.html));
52
+ }
53
+
54
+ if (example.plainText || example.html) {
55
+ parts.push(heading(4, 'Summary'));
56
+ const summary = example.plainText || htmlToPlainText(example.html);
57
+ parts.push(summary);
58
+ }
59
+
60
+ for (const tab of example.codeTabs) {
61
+ parts.push(heading(4, `${tab.label} (${tab.language})`));
62
+ parts.push(codeBlock(tab.language, tab.code));
63
+ }
64
+
65
+ for (const table of example.propsTables) {
66
+ parts.push(heading(4, table.heading ? `Props: ${table.heading}` : 'Props'));
67
+ parts.push(table.markdown);
68
+ }
69
+ }
70
+ }
71
+
72
+ if (doc.seeAlso.length) {
73
+ parts.push(heading(2, 'See Also'));
74
+ const list = doc.seeAlso
75
+ .map((link) => `- [${link.label}](${link.href})`)
76
+ .join('\n');
77
+ parts.push(list);
78
+ }
79
+
80
+ parts.push('\n');
81
+ return parts.join('\n\n').trim();
82
+ }