@incursa/ui-kit 1.4.0 → 1.6.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.
Files changed (78) hide show
  1. package/LLMS.txt +4 -4
  2. package/README.md +46 -6
  3. package/dist/inc-design-language.css +192 -35
  4. package/dist/inc-design-language.css.map +1 -1
  5. package/dist/inc-design-language.min.css +1 -1
  6. package/dist/inc-design-language.min.css.map +1 -1
  7. package/dist/mcp/ai/agent-instructions.json +21 -0
  8. package/dist/mcp/ai/llms-txt.json +21 -0
  9. package/dist/mcp/components/buttons.json +29 -0
  10. package/dist/mcp/components/cards.json +29 -0
  11. package/dist/mcp/components/filter-bars.json +28 -0
  12. package/dist/mcp/components/form-choices.json +29 -0
  13. package/dist/mcp/components/forms.json +29 -0
  14. package/dist/mcp/components/interaction.json +28 -0
  15. package/dist/mcp/components/layout.json +28 -0
  16. package/dist/mcp/components/metrics.json +28 -0
  17. package/dist/mcp/components/states.json +28 -0
  18. package/dist/mcp/components/status.json +28 -0
  19. package/dist/mcp/components/tables.json +32 -0
  20. package/dist/mcp/components/utilities.json +28 -0
  21. package/dist/mcp/examples/data-grid-advanced.json +22 -0
  22. package/dist/mcp/examples/demo.json +24 -0
  23. package/dist/mcp/examples/forms-and-validation.json +21 -0
  24. package/dist/mcp/examples/native-patterns.json +21 -0
  25. package/dist/mcp/examples/overlay-workflows.json +22 -0
  26. package/dist/mcp/examples/record-detail.json +21 -0
  27. package/dist/mcp/examples/reference.json +23 -0
  28. package/dist/mcp/examples/states.json +21 -0
  29. package/dist/mcp/examples/web-components.json +24 -0
  30. package/dist/mcp/examples/work-queue.json +21 -0
  31. package/dist/mcp/guides/allowed-web-component-families.json +19 -0
  32. package/dist/mcp/guides/choose-css-vs-scss-vs-js-vs-web-components.json +20 -0
  33. package/dist/mcp/guides/customization-order.json +20 -0
  34. package/dist/mcp/guides/decision-tree.json +28 -0
  35. package/dist/mcp/guides/guardrails.json +20 -0
  36. package/dist/mcp/guides/install.json +31 -0
  37. package/dist/mcp/guides/latest.json +25 -0
  38. package/dist/mcp/guides/overview.json +26 -0
  39. package/dist/mcp/guides/package-metadata.json +25 -0
  40. package/dist/mcp/guides/update.json +26 -0
  41. package/dist/mcp/guides/when-to-use-css-first.json +20 -0
  42. package/dist/mcp/install.json +36 -0
  43. package/dist/mcp/patterns/data-grid-advanced.json +22 -0
  44. package/dist/mcp/patterns/demo.json +24 -0
  45. package/dist/mcp/patterns/forms-and-validation.json +21 -0
  46. package/dist/mcp/patterns/native-patterns.json +21 -0
  47. package/dist/mcp/patterns/overlay-workflows.json +22 -0
  48. package/dist/mcp/patterns/record-detail.json +21 -0
  49. package/dist/mcp/patterns/reference.json +24 -0
  50. package/dist/mcp/patterns/states.json +21 -0
  51. package/dist/mcp/patterns/web-components.json +24 -0
  52. package/dist/mcp/patterns/work-queue.json +21 -0
  53. package/dist/mcp/resources.json +2100 -0
  54. package/dist/mcp/search-index.json +827 -0
  55. package/dist/mcp/specs/control-conventions.json +21 -0
  56. package/dist/mcp/specs/public-surface.json +21 -0
  57. package/dist/mcp/specs/requirements-index.json +21 -0
  58. package/dist/mcp/specs/verification-index.json +21 -0
  59. package/dist/mcp/update.json +24 -0
  60. package/dist/mcp/worker.mjs +59959 -0
  61. package/dist/mcp/worker.mjs.map +7 -0
  62. package/dist/web-components/README.md +10 -4
  63. package/dist/web-components/RUNTIME-NOTES.md +7 -2
  64. package/dist/web-components/components/actions.js +557 -0
  65. package/dist/web-components/components/collections.js +272 -0
  66. package/dist/web-components/components/dom-helpers.js +46 -0
  67. package/dist/web-components/components/feedback.js +165 -0
  68. package/dist/web-components/index.js +4350 -813
  69. package/package.json +19 -8
  70. package/src/inc-design-language.scss +193 -35
  71. package/src/mcp/worker.ts +858 -0
  72. package/src/web-components/README.md +10 -4
  73. package/src/web-components/RUNTIME-NOTES.md +7 -2
  74. package/src/web-components/components/actions.js +557 -0
  75. package/src/web-components/components/collections.js +272 -0
  76. package/src/web-components/components/dom-helpers.js +46 -0
  77. package/src/web-components/components/feedback.js +165 -0
  78. package/src/web-components/index.js +53 -847
@@ -0,0 +1,858 @@
1
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import * as z from "zod/v4";
4
+ import resourcesManifest from "../../dist/mcp/resources.json";
5
+ import searchIndex from "../../dist/mcp/search-index.json";
6
+
7
+ type ResourceRecord = {
8
+ uri: string;
9
+ title: string;
10
+ kind: string;
11
+ searchKind: string;
12
+ summary: string;
13
+ body: string;
14
+ sourcePaths: string[];
15
+ mimeType: string;
16
+ aliases: string[];
17
+ relatedUris: string[];
18
+ group: string;
19
+ priority: number;
20
+ includeInSearch?: boolean;
21
+ searchText: string;
22
+ canonicalMarkup?: {
23
+ default: string;
24
+ variants?: Record<string, string>;
25
+ };
26
+ };
27
+
28
+ type SearchIndexEntry = {
29
+ uri: string;
30
+ title: string;
31
+ kind: string;
32
+ summary: string;
33
+ sourcePaths: string[];
34
+ aliases: string[];
35
+ priority: number;
36
+ searchText: string;
37
+ canonicalMarkup?: {
38
+ default: string;
39
+ variants?: Record<string, string>;
40
+ };
41
+ };
42
+
43
+ type InstallData = typeof resourcesManifest.install;
44
+ type UpdateData = typeof resourcesManifest.update;
45
+
46
+ const packageName = resourcesManifest.packageName ?? "@incursa/ui-kit";
47
+ const packageVersion = resourcesManifest.packageVersion ?? "0.0.0";
48
+ const resources = resourcesManifest.resources as ResourceRecord[];
49
+ const resourceMap = new Map(resources.map((resource) => [resource.uri, resource]));
50
+ const searchEntries = searchIndex as SearchIndexEntry[];
51
+ const installData = resourcesManifest.install as InstallData;
52
+ const updateData = resourcesManifest.update as UpdateData;
53
+ const componentResources = resources.filter((resource) => resource.group === "components");
54
+ const patternResources = resources.filter((resource) => resource.group === "patterns");
55
+ const specResources = resources.filter((resource) => resource.group === "specs");
56
+ const exampleResources = resources.filter((resource) => resource.group === "examples");
57
+ const fileResources = resources.filter((resource) => resource.group === "files");
58
+ const guideResources = resources.filter((resource) => resource.group === "guides" || resource.group === "ai");
59
+ const defaultPathPrefix = "/ui-kit";
60
+
61
+ type WorkerEnv = {
62
+ MCP_PATH_PREFIX?: string;
63
+ };
64
+
65
+ function normalizeText(value: string) {
66
+ return String(value ?? "")
67
+ .replace(/\r\n/g, "\n")
68
+ .replace(/\u00a0/g, " ")
69
+ .replace(/[ \t]+\n/g, "\n")
70
+ .replace(/\n{3,}/g, "\n\n")
71
+ .replace(/[ \t]{2,}/g, " ")
72
+ .trim()
73
+ .toLowerCase();
74
+ }
75
+
76
+ function escapeHtml(value: string) {
77
+ return String(value ?? "")
78
+ .replace(/&/g, "&amp;")
79
+ .replace(/</g, "&lt;")
80
+ .replace(/>/g, "&gt;")
81
+ .replace(/"/g, "&quot;");
82
+ }
83
+
84
+ function slugify(value: string) {
85
+ return normalizeText(value)
86
+ .replace(/['"]/g, "")
87
+ .replace(/[^a-z0-9]+/g, "-")
88
+ .replace(/^-+|-+$/g, "");
89
+ }
90
+
91
+ function collectClasses(markup: string) {
92
+ const classes = new Set<string>();
93
+ for (const match of markup.matchAll(/class=(["'])([^"']+)\1/g)) {
94
+ for (const cls of match[2].split(/\s+/g)) {
95
+ if (cls.startsWith("inc-")) {
96
+ classes.add(cls);
97
+ }
98
+ }
99
+ }
100
+ return Array.from(classes).sort();
101
+ }
102
+
103
+ function collectHelperHooks(markup: string) {
104
+ const hooks = new Set<string>();
105
+ for (const match of markup.matchAll(/\b(data-inc-[a-z0-9-]+)(?:=|\s|>)/gi)) {
106
+ hooks.add(match[1].toLowerCase());
107
+ }
108
+ if (/\bis-loading\b/i.test(markup)) {
109
+ hooks.add("is-loading");
110
+ }
111
+ return Array.from(hooks).sort();
112
+ }
113
+
114
+ function frameworkShape(markup: string, framework: string | undefined) {
115
+ if (framework !== "react") {
116
+ return markup;
117
+ }
118
+
119
+ return markup
120
+ .replace(/\bclass=/g, "className=")
121
+ .replace(/\bfor=/g, "htmlFor=");
122
+ }
123
+
124
+ function formatList(items: string[]) {
125
+ return items.map((item) => `- ${item}`).join("\n");
126
+ }
127
+
128
+ function lookupResource(uri: string) {
129
+ const resource = resourceMap.get(uri);
130
+ if (!resource) {
131
+ throw new Error(`Unknown resource: ${uri}`);
132
+ }
133
+ return resource;
134
+ }
135
+
136
+ function joinPathPrefix(basePathPrefix: string, pathname: string) {
137
+ return basePathPrefix ? `${basePathPrefix}${pathname}` : pathname;
138
+ }
139
+
140
+ function normalizeConfiguredPathPrefix(value: string | undefined) {
141
+ if (value === undefined) {
142
+ return defaultPathPrefix;
143
+ }
144
+
145
+ const trimmed = value.trim();
146
+ if (!trimmed) {
147
+ return "";
148
+ }
149
+
150
+ const stripped = trimmed.replace(/^\/+|\/+$/g, "");
151
+ return stripped ? `/${stripped}` : "";
152
+ }
153
+
154
+ function stripPathPrefix(pathname: string, configuredPathPrefix: string) {
155
+ if (configuredPathPrefix === "") {
156
+ return pathname;
157
+ }
158
+
159
+ if (pathname === configuredPathPrefix) {
160
+ return "/";
161
+ }
162
+
163
+ if (pathname.startsWith(`${configuredPathPrefix}/`)) {
164
+ return pathname.slice(configuredPathPrefix.length);
165
+ }
166
+
167
+ return pathname;
168
+ }
169
+
170
+ function resolvePathPrefix(env?: WorkerEnv) {
171
+ return normalizeConfiguredPathPrefix(env?.MCP_PATH_PREFIX);
172
+ }
173
+
174
+ function normalizeIncomingRequest(request: Request, configuredPathPrefix: string) {
175
+ const url = new URL(request.url);
176
+ const { pathname } = url;
177
+ const hasPathPrefix = configuredPathPrefix !== "" && (pathname === configuredPathPrefix || pathname.startsWith(`${configuredPathPrefix}/`));
178
+
179
+ url.pathname = stripPathPrefix(pathname, configuredPathPrefix);
180
+
181
+ return {
182
+ url,
183
+ basePathPrefix: hasPathPrefix ? configuredPathPrefix : "",
184
+ };
185
+ }
186
+
187
+ function renderDocsIndexHtml(basePathPrefix = "") {
188
+ const groups = [
189
+ ["Guides", guideResources],
190
+ ["Components", componentResources],
191
+ ["Patterns", patternResources],
192
+ ["Specs", specResources],
193
+ ["Examples", exampleResources],
194
+ ["Curated files", fileResources],
195
+ ] as const;
196
+
197
+ const groupHtml = groups
198
+ .map(([title, group]) => {
199
+ const items = group
200
+ .slice()
201
+ .sort((left, right) => left.title.localeCompare(right.title))
202
+ .map(
203
+ (resource) => `
204
+ <li>
205
+ <a href="${joinPathPrefix(basePathPrefix, `/mcp/resource/${encodeURIComponent(resource.uri)}`)}">${escapeHtml(resource.title)}</a>
206
+ <div class="meta">${escapeHtml(resource.summary)}</div>
207
+ </li>
208
+ `,
209
+ )
210
+ .join("");
211
+
212
+ return `
213
+ <section class="group">
214
+ <h2>${escapeHtml(title)}</h2>
215
+ <ul>${items}</ul>
216
+ </section>
217
+ `;
218
+ })
219
+ .join("");
220
+
221
+ return `<!doctype html>
222
+ <html lang="en">
223
+ <head>
224
+ <meta charset="utf-8">
225
+ <meta name="viewport" content="width=device-width, initial-scale=1">
226
+ <title>Incursa UI Kit MCP</title>
227
+ <style>
228
+ :root { color-scheme: light dark; font-family: Inter, Segoe UI, sans-serif; }
229
+ body { margin: 0; padding: 2rem; background: #0f172a; color: #e2e8f0; }
230
+ main { max-width: 1080px; margin: 0 auto; }
231
+ h1, h2 { margin: 0 0 0.75rem; }
232
+ .intro { max-width: 70ch; color: #cbd5e1; }
233
+ .group { margin-top: 2rem; padding: 1.25rem; border: 1px solid #334155; border-radius: 1rem; background: rgba(15, 23, 42, 0.7); }
234
+ ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.75rem; }
235
+ a { color: #7dd3fc; text-decoration: none; font-weight: 600; }
236
+ .meta { color: #94a3b8; font-size: 0.94rem; margin-top: 0.2rem; }
237
+ code { color: #f8fafc; }
238
+ </style>
239
+ </head>
240
+ <body>
241
+ <main>
242
+ <h1>Incursa UI Kit MCP</h1>
243
+ <p class="intro">Deterministic, stateless Model Context Protocol surface for the UI kit. POST to the MCP endpoint for protocol traffic or open a resource page below.</p>
244
+ ${groupHtml}
245
+ </main>
246
+ </body>
247
+ </html>`;
248
+ }
249
+
250
+ function renderResourcePage(resource: ResourceRecord, basePathPrefix = "") {
251
+ return `<!doctype html>
252
+ <html lang="en">
253
+ <head>
254
+ <meta charset="utf-8">
255
+ <meta name="viewport" content="width=device-width, initial-scale=1">
256
+ <title>${escapeHtml(resource.title)} - Incursa UI Kit MCP</title>
257
+ <style>
258
+ :root { color-scheme: light dark; font-family: Inter, Segoe UI, sans-serif; }
259
+ body { margin: 0; padding: 2rem; background: #0f172a; color: #e2e8f0; }
260
+ main { max-width: 1080px; margin: 0 auto; }
261
+ .card { padding: 1.25rem; border: 1px solid #334155; border-radius: 1rem; background: rgba(15, 23, 42, 0.7); }
262
+ .meta { color: #94a3b8; font-size: 0.94rem; }
263
+ pre { overflow: auto; padding: 1rem; border-radius: 0.85rem; background: #020617; border: 1px solid #334155; white-space: pre-wrap; }
264
+ a { color: #7dd3fc; text-decoration: none; }
265
+ .chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1rem 0; }
266
+ .chip { padding: 0.25rem 0.55rem; border-radius: 999px; background: #1e293b; color: #cbd5e1; font-size: 0.85rem; }
267
+ </style>
268
+ </head>
269
+ <body>
270
+ <main>
271
+ <p><a href="${joinPathPrefix(basePathPrefix, "/mcp")}">Back to index</a></p>
272
+ <div class="card">
273
+ <h1>${escapeHtml(resource.title)}</h1>
274
+ <p class="meta">${escapeHtml(resource.uri)}</p>
275
+ <p>${escapeHtml(resource.summary)}</p>
276
+ <div class="chips">
277
+ <span class="chip">${escapeHtml(resource.group)}</span>
278
+ <span class="chip">${escapeHtml(resource.kind)}</span>
279
+ <span class="chip">${escapeHtml(resource.mimeType)}</span>
280
+ </div>
281
+ <p class="meta">Source: ${escapeHtml(resource.sourcePaths.join(", "))}</p>
282
+ <pre>${escapeHtml(resource.body)}</pre>
283
+ </div>
284
+ </main>
285
+ </body>
286
+ </html>`;
287
+ }
288
+
289
+ function normalizeLookupKey(value: string) {
290
+ return slugify(value);
291
+ }
292
+
293
+ function buildComponentLookup() {
294
+ const lookup = new Map<string, ResourceRecord>();
295
+ for (const resource of componentResources) {
296
+ lookup.set(normalizeLookupKey(resource.title), resource);
297
+ lookup.set(normalizeLookupKey(resource.uri), resource);
298
+ lookup.set(normalizeLookupKey(resource.uri.replace("ui-kit://components/", "")), resource);
299
+ for (const alias of resource.aliases ?? []) {
300
+ lookup.set(normalizeLookupKey(alias), resource);
301
+ }
302
+ }
303
+ return lookup;
304
+ }
305
+
306
+ const componentLookup = buildComponentLookup();
307
+
308
+ function scoreEntry(entry: SearchIndexEntry, query: string, tokens: string[]) {
309
+ const text = normalizeText(entry.searchText);
310
+ const title = normalizeText(entry.title);
311
+ const uri = normalizeText(entry.uri);
312
+ let score = 0;
313
+ let matched = false;
314
+
315
+ if (!query) {
316
+ return entry.priority ?? 0;
317
+ }
318
+
319
+ if (query === title) {
320
+ score += 1000;
321
+ matched = true;
322
+ }
323
+ if (query === uri) {
324
+ score += 1200;
325
+ matched = true;
326
+ }
327
+ if (title.startsWith(query)) {
328
+ score += 500;
329
+ matched = true;
330
+ }
331
+ if (uri.startsWith(query)) {
332
+ score += 400;
333
+ matched = true;
334
+ }
335
+ if (text.includes(query)) {
336
+ score += 200;
337
+ matched = true;
338
+ }
339
+
340
+ for (const token of tokens) {
341
+ if (!token) continue;
342
+ if (title.includes(token)) {
343
+ score += 80;
344
+ matched = true;
345
+ }
346
+ if (uri.includes(token)) {
347
+ score += 70;
348
+ matched = true;
349
+ }
350
+ if (text.includes(token)) {
351
+ score += 20;
352
+ matched = true;
353
+ }
354
+ if ((entry.aliases ?? []).some((alias) => normalizeText(alias).includes(token))) {
355
+ score += 60;
356
+ matched = true;
357
+ }
358
+ }
359
+
360
+ if (tokens.length > 1 && tokens.every((token) => text.includes(token))) {
361
+ score += 100;
362
+ matched = true;
363
+ }
364
+
365
+ return matched ? score + (entry.priority ?? 0) : 0;
366
+ }
367
+
368
+ function searchUiKit({
369
+ query,
370
+ kind = "any",
371
+ include_examples = true,
372
+ include_installation = true,
373
+ max_results = 8,
374
+ }: {
375
+ query: string;
376
+ kind?: string;
377
+ include_examples?: boolean;
378
+ include_installation?: boolean;
379
+ max_results?: number;
380
+ }) {
381
+ const normalizedQuery = normalizeText(query);
382
+ const tokens = normalizedQuery.match(/[a-z0-9]+/g) ?? [];
383
+
384
+ let candidates = searchEntries.slice();
385
+
386
+ if (!include_examples) {
387
+ candidates = candidates.filter((entry) => entry.kind !== "example");
388
+ }
389
+
390
+ if (!include_installation) {
391
+ candidates = candidates.filter((entry) => entry.kind !== "install");
392
+ }
393
+
394
+ if (kind !== "any") {
395
+ candidates = candidates.filter((entry) => entry.kind === kind);
396
+ }
397
+
398
+ const ranked = candidates
399
+ .map((entry) => ({
400
+ ...entry,
401
+ score: searchEntryScore(entry, normalizedQuery, tokens),
402
+ }))
403
+ .filter((entry) => entry.score > 0 || normalizedQuery.length === 0)
404
+ .sort((left, right) => right.score - left.score || left.title.localeCompare(right.title) || left.uri.localeCompare(right.uri))
405
+ .slice(0, Math.max(1, Math.min(max_results ?? 8, 20)));
406
+
407
+ return {
408
+ query,
409
+ kind,
410
+ include_examples,
411
+ include_installation,
412
+ max_results,
413
+ results: ranked.map((entry) => ({
414
+ uri: entry.uri,
415
+ title: entry.title,
416
+ kind: entry.kind,
417
+ summary: entry.summary,
418
+ sourcePaths: entry.sourcePaths,
419
+ score: entry.score,
420
+ relatedUris: resourceMap.get(entry.uri)?.relatedUris ?? [],
421
+ })),
422
+ starterSuggestions: ranked.slice(0, 3).map((entry) => ({
423
+ uri: entry.uri,
424
+ title: entry.title,
425
+ kind: entry.kind,
426
+ })),
427
+ };
428
+ }
429
+
430
+ function searchEntryScore(entry: SearchIndexEntry, query: string, tokens: string[]) {
431
+ return scoreEntry(entry, query, tokens);
432
+ }
433
+
434
+ function buildSearchText(results: ReturnType<typeof searchUiKit>) {
435
+ if (results.results.length === 0) {
436
+ return "No matches found.";
437
+ }
438
+
439
+ return [
440
+ `Matches for "${results.query || "(empty query)"}":`,
441
+ ...results.results.map((result) => `- ${result.title} (${result.uri})${result.summary ? ` - ${result.summary}` : ""}`),
442
+ ].join("\n");
443
+ }
444
+
445
+ function chooseComponentMarkup(resource: ResourceRecord, variant?: string) {
446
+ if (!resource.canonicalMarkup) {
447
+ return resource.body;
448
+ }
449
+
450
+ if (variant && resource.canonicalMarkup.variants?.[variant]) {
451
+ return resource.canonicalMarkup.variants[variant];
452
+ }
453
+
454
+ return resource.canonicalMarkup.default;
455
+ }
456
+
457
+ function getComponentMarkup({
458
+ component_name,
459
+ variant,
460
+ framework = "html",
461
+ }: {
462
+ component_name: string;
463
+ variant?: string;
464
+ framework?: string;
465
+ }) {
466
+ const lookupKey = normalizeLookupKey(component_name);
467
+ const resource = componentLookup.get(lookupKey);
468
+
469
+ if (!resource) {
470
+ throw new Error(`Unknown component name: ${component_name}`);
471
+ }
472
+
473
+ const chosenMarkup = chooseComponentMarkup(resource, variant);
474
+ const shapedMarkup = frameworkShape(chosenMarkup, framework);
475
+ const requiredClasses = collectClasses(chosenMarkup);
476
+ const helperHooks = collectHelperHooks(chosenMarkup);
477
+
478
+ return {
479
+ component_name,
480
+ variant: variant ?? "default",
481
+ framework,
482
+ markup: shapedMarkup,
483
+ notes: [
484
+ `Source: ${resource.sourcePaths.join(", ")}`,
485
+ variant && chosenMarkup !== resource.canonicalMarkup?.default ? `Variant "${variant}" selected from the canonical snippet set.` : "Canonical snippet from the source docs.",
486
+ framework === "react" ? "class attributes were converted to className and for to htmlFor." : "No framework-specific transform was applied.",
487
+ ],
488
+ requiredClasses,
489
+ helperHooks,
490
+ relatedUris: Array.from(new Set([resource.uri, ...(resource.relatedUris ?? [])])),
491
+ };
492
+ }
493
+
494
+ function buildInstallCommands(packageManager: string, useCase: string) {
495
+ const packageInstall =
496
+ packageManager === "npm"
497
+ ? `npm install ${packageName}`
498
+ : packageManager === "pnpm"
499
+ ? `pnpm add ${packageName}`
500
+ : `yarn add ${packageName}`;
501
+
502
+ const scssInstall =
503
+ packageManager === "npm"
504
+ ? `npm install ${packageName} bootstrap sass`
505
+ : packageManager === "pnpm"
506
+ ? `pnpm add ${packageName} bootstrap sass`
507
+ : `yarn add ${packageName} bootstrap sass`;
508
+
509
+ switch (useCase) {
510
+ case "scss":
511
+ return scssInstall;
512
+ default:
513
+ return packageInstall;
514
+ }
515
+ }
516
+
517
+ function buildUpdateCommands(packageManager: string) {
518
+ if (packageManager === "npm") {
519
+ return `npm install ${packageName}@latest`;
520
+ }
521
+ if (packageManager === "pnpm") {
522
+ return `pnpm up ${packageName}`;
523
+ }
524
+ return `yarn upgrade ${packageName}`;
525
+ }
526
+
527
+ function getInstallationInstructions({
528
+ framework = "html",
529
+ use_case = "css-only",
530
+ package_manager = "npm",
531
+ }: {
532
+ framework?: string;
533
+ use_case?: string;
534
+ package_manager?: string;
535
+ }) {
536
+ const primary = {
537
+ "css-only": installData.importExamples.cssOnly,
538
+ scss: installData.importExamples.scss,
539
+ "js-helper": installData.importExamples.jsHelper,
540
+ "web-components": installData.importExamples.webComponents,
541
+ }[use_case];
542
+
543
+ const installCommands = [buildInstallCommands(package_manager, use_case)];
544
+ const updateCommands = [buildUpdateCommands(package_manager)];
545
+
546
+ const importExamples = [
547
+ `Compiled CSS: ${installData.importExamples.cssOnly}`,
548
+ `JS helper: ${installData.importExamples.jsHelper}`,
549
+ `Web components: ${installData.importExamples.webComponents}`,
550
+ `SCSS source: ${installData.importExamples.scss}`,
551
+ ];
552
+
553
+ return {
554
+ framework,
555
+ use_case,
556
+ package_manager,
557
+ install_commands: installCommands,
558
+ update_commands: updateCommands,
559
+ import_examples: [
560
+ `Primary for ${use_case}: ${primary}`,
561
+ ...importExamples,
562
+ ],
563
+ notes: [
564
+ ...installData.notes,
565
+ framework === "react" ? "React callers should adapt class attributes to className when they copy snippets." : "HTML, Razor, and Blazor can reuse the HTML snippets directly.",
566
+ ],
567
+ relatedUris: installData.relatedUris,
568
+ };
569
+ }
570
+
571
+ function buildInstallationText(result: ReturnType<typeof getInstallationInstructions>) {
572
+ return [
573
+ `Use case: ${result.use_case}`,
574
+ `Package manager: ${result.package_manager}`,
575
+ "",
576
+ "Install commands:",
577
+ ...result.install_commands.map((command) => `- ${command}`),
578
+ "",
579
+ "Update commands:",
580
+ ...result.update_commands.map((command) => `- ${command}`),
581
+ "",
582
+ "Import examples:",
583
+ ...result.import_examples.map((line) => `- ${line}`),
584
+ "",
585
+ "Notes:",
586
+ ...result.notes.map((line) => `- ${line}`),
587
+ ].join("\n");
588
+ }
589
+
590
+ function registerResourceHandlers(server: McpServer) {
591
+ for (const resource of resources) {
592
+ server.registerResource(
593
+ resource.title,
594
+ resource.uri,
595
+ {
596
+ description: resource.summary,
597
+ mimeType: resource.mimeType,
598
+ },
599
+ async () => ({
600
+ contents: [
601
+ {
602
+ uri: resource.uri,
603
+ mimeType: resource.mimeType,
604
+ text: resource.body,
605
+ },
606
+ ],
607
+ }),
608
+ );
609
+ }
610
+
611
+ const templateGroups: Array<[string, ResourceRecord["group"]]> = [
612
+ ["ui-kit://component/{name}", "components"],
613
+ ["ui-kit://pattern/{name}", "patterns"],
614
+ ["ui-kit://spec/{id}", "specs"],
615
+ ["ui-kit://example/{name}", "examples"],
616
+ ["ui-kit://file/{path}", "files"],
617
+ ];
618
+
619
+ for (const [template, group] of templateGroups) {
620
+ const groupResources = resources.filter((resource) => resource.group === group);
621
+ server.registerResource(
622
+ `${group}-template`,
623
+ new ResourceTemplate(template, {
624
+ list: async () => ({
625
+ resources: groupResources.map((resource) => ({
626
+ uri: resource.uri,
627
+ name: resource.title,
628
+ title: resource.title,
629
+ description: resource.summary,
630
+ mimeType: resource.mimeType,
631
+ })),
632
+ }),
633
+ }),
634
+ {
635
+ description: `${group} resource template`,
636
+ mimeType: "text/markdown; charset=utf-8",
637
+ },
638
+ async (uri: URL, _variables: Record<string, string>, _extra: unknown) => {
639
+ const resource = lookupResource(uri.toString());
640
+ return {
641
+ contents: [
642
+ {
643
+ uri: resource.uri,
644
+ mimeType: resource.mimeType,
645
+ text: resource.body,
646
+ },
647
+ ],
648
+ };
649
+ },
650
+ );
651
+ }
652
+ }
653
+
654
+ function registerTools(server: McpServer) {
655
+ server.registerTool(
656
+ "search_ui_kit",
657
+ {
658
+ description: "Search the precompiled UI kit manifest.",
659
+ inputSchema: {
660
+ query: z.string().describe("Search text"),
661
+ kind: z.enum(["component", "pattern", "spec", "guide", "install", "any"]).default("any"),
662
+ include_examples: z.boolean().default(true),
663
+ include_installation: z.boolean().default(true),
664
+ max_results: z.number().int().positive().max(20).default(8),
665
+ },
666
+ outputSchema: {
667
+ query: z.string(),
668
+ kind: z.string(),
669
+ include_examples: z.boolean(),
670
+ include_installation: z.boolean(),
671
+ max_results: z.number(),
672
+ results: z.array(
673
+ z.object({
674
+ uri: z.string(),
675
+ title: z.string(),
676
+ kind: z.string(),
677
+ summary: z.string(),
678
+ sourcePaths: z.array(z.string()),
679
+ score: z.number(),
680
+ relatedUris: z.array(z.string()),
681
+ }),
682
+ ),
683
+ starterSuggestions: z.array(
684
+ z.object({
685
+ uri: z.string(),
686
+ title: z.string(),
687
+ kind: z.string(),
688
+ }),
689
+ ),
690
+ },
691
+ },
692
+ async (args) => {
693
+ const result = searchUiKit(args);
694
+ return {
695
+ content: [{ type: "text", text: buildSearchText(result) }],
696
+ structuredContent: result,
697
+ };
698
+ },
699
+ );
700
+
701
+ server.registerTool(
702
+ "get_component_markup",
703
+ {
704
+ description: "Return canonical starter markup for a known UI kit component.",
705
+ inputSchema: {
706
+ component_name: z.string().describe("Component name"),
707
+ variant: z.string().optional(),
708
+ framework: z.enum(["html", "razor", "blazor", "react"]).default("html"),
709
+ },
710
+ outputSchema: {
711
+ component_name: z.string(),
712
+ variant: z.string(),
713
+ framework: z.string(),
714
+ markup: z.string(),
715
+ notes: z.array(z.string()),
716
+ requiredClasses: z.array(z.string()),
717
+ helperHooks: z.array(z.string()),
718
+ relatedUris: z.array(z.string()),
719
+ },
720
+ },
721
+ async (args) => {
722
+ const result = getComponentMarkup(args);
723
+ return {
724
+ content: [
725
+ {
726
+ type: "text",
727
+ text: result.markup,
728
+ },
729
+ ],
730
+ structuredContent: result,
731
+ };
732
+ },
733
+ );
734
+
735
+ server.registerTool(
736
+ "get_installation_instructions",
737
+ {
738
+ description: "Return package installation and update instructions.",
739
+ inputSchema: {
740
+ framework: z.enum(["html", "razor", "blazor", "react"]).default("html"),
741
+ use_case: z.enum(["css-only", "scss", "js-helper", "web-components"]).default("css-only"),
742
+ package_manager: z.enum(["npm", "pnpm", "yarn"]).default("npm"),
743
+ },
744
+ outputSchema: {
745
+ framework: z.string(),
746
+ use_case: z.string(),
747
+ package_manager: z.string(),
748
+ install_commands: z.array(z.string()),
749
+ update_commands: z.array(z.string()),
750
+ import_examples: z.array(z.string()),
751
+ notes: z.array(z.string()),
752
+ relatedUris: z.array(z.string()),
753
+ },
754
+ },
755
+ async (args) => {
756
+ const result = getInstallationInstructions(args);
757
+ return {
758
+ content: [{ type: "text", text: buildInstallationText(result) }],
759
+ structuredContent: result,
760
+ };
761
+ },
762
+ );
763
+ }
764
+
765
+ function createServer() {
766
+ const server = new McpServer({ name: "incursa-ui-kit-mcp", version: packageVersion }, { capabilities: { logging: {} } });
767
+ registerResourceHandlers(server);
768
+ registerTools(server);
769
+ return server;
770
+ }
771
+
772
+ async function handleMcpRequest(request: Request) {
773
+ const server = createServer();
774
+ const transport = new WebStandardStreamableHTTPServerTransport({
775
+ sessionIdGenerator: undefined,
776
+ enableJsonResponse: true,
777
+ });
778
+
779
+ await server.connect(transport);
780
+ return transport.handleRequest(request);
781
+ }
782
+
783
+ function isResourcePath(pathname: string) {
784
+ return pathname === "/mcp" || pathname === "/mcp/" || pathname === "/";
785
+ }
786
+
787
+ function findResourceForRequest(url: URL) {
788
+ if (isResourcePath(url.pathname)) {
789
+ return null;
790
+ }
791
+
792
+ const resourcePrefix = "/mcp/resource/";
793
+ const rawResourcePrefix = "/resource/";
794
+ let encodedUri = "";
795
+
796
+ if (url.pathname.startsWith(resourcePrefix)) {
797
+ encodedUri = url.pathname.slice(resourcePrefix.length);
798
+ } else if (url.pathname.startsWith(rawResourcePrefix)) {
799
+ encodedUri = url.pathname.slice(rawResourcePrefix.length);
800
+ } else if (url.searchParams.has("uri")) {
801
+ encodedUri = url.searchParams.get("uri") ?? "";
802
+ }
803
+
804
+ if (!encodedUri) {
805
+ return null;
806
+ }
807
+
808
+ const decodedUri = decodeURIComponent(encodedUri);
809
+ return lookupResource(decodedUri);
810
+ }
811
+
812
+ export async function fetch(request: Request, env?: WorkerEnv): Promise<Response> {
813
+ const configuredPathPrefix = resolvePathPrefix(env);
814
+ const { url, basePathPrefix } = normalizeIncomingRequest(request, configuredPathPrefix);
815
+
816
+ if (request.method === "POST" && url.pathname === "/mcp") {
817
+ return handleMcpRequest(request);
818
+ }
819
+
820
+ if (request.method === "GET" && isResourcePath(url.pathname)) {
821
+ return new Response(renderDocsIndexHtml(basePathPrefix), {
822
+ headers: {
823
+ "content-type": "text/html; charset=utf-8",
824
+ },
825
+ });
826
+ }
827
+
828
+ if (request.method === "GET") {
829
+ try {
830
+ const resource = findResourceForRequest(url);
831
+ if (resource) {
832
+ return new Response(renderResourcePage(resource, basePathPrefix), {
833
+ headers: {
834
+ "content-type": "text/html; charset=utf-8",
835
+ },
836
+ });
837
+ }
838
+ } catch (error) {
839
+ return new Response(String(error instanceof Error ? error.message : error), {
840
+ status: 404,
841
+ headers: {
842
+ "content-type": "text/plain; charset=utf-8",
843
+ },
844
+ });
845
+ }
846
+ }
847
+
848
+ return new Response("Not found", {
849
+ status: 404,
850
+ headers: {
851
+ "content-type": "text/plain; charset=utf-8",
852
+ },
853
+ });
854
+ }
855
+
856
+ export default {
857
+ fetch,
858
+ };