@urbicon-ui/mcp-server 6.1.4

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 (130) hide show
  1. package/README.md +161 -0
  2. package/dist/data/catalog-loader.d.ts +37 -0
  3. package/dist/data/catalog-loader.d.ts.map +1 -0
  4. package/dist/data/catalog-loader.js +15 -0
  5. package/dist/data/catalog-loader.js.map +1 -0
  6. package/dist/data/component-loader.d.ts +2 -0
  7. package/dist/data/component-loader.d.ts.map +1 -0
  8. package/dist/data/component-loader.js +17 -0
  9. package/dist/data/component-loader.js.map +1 -0
  10. package/dist/data/recipe-loader.d.ts +4 -0
  11. package/dist/data/recipe-loader.d.ts.map +1 -0
  12. package/dist/data/recipe-loader.js +102 -0
  13. package/dist/data/recipe-loader.js.map +1 -0
  14. package/dist/data/template-loader.d.ts +8 -0
  15. package/dist/data/template-loader.d.ts.map +1 -0
  16. package/dist/data/template-loader.js +33 -0
  17. package/dist/data/template-loader.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +57 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/resources/catalog.d.ts +3 -0
  23. package/dist/resources/catalog.d.ts.map +1 -0
  24. package/dist/resources/catalog.js +20 -0
  25. package/dist/resources/catalog.js.map +1 -0
  26. package/dist/resources/component.d.ts +3 -0
  27. package/dist/resources/component.d.ts.map +1 -0
  28. package/dist/resources/component.js +29 -0
  29. package/dist/resources/component.js.map +1 -0
  30. package/dist/resources/guides.d.ts +3 -0
  31. package/dist/resources/guides.d.ts.map +1 -0
  32. package/dist/resources/guides.js +36 -0
  33. package/dist/resources/guides.js.map +1 -0
  34. package/dist/server.d.ts +3 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/tools/find-components.d.ts +3 -0
  38. package/dist/tools/find-components.d.ts.map +1 -0
  39. package/dist/tools/find-components.js +21 -0
  40. package/dist/tools/find-components.js.map +1 -0
  41. package/dist/tools/get-recipe.d.ts +3 -0
  42. package/dist/tools/get-recipe.d.ts.map +1 -0
  43. package/dist/tools/get-recipe.js +48 -0
  44. package/dist/tools/get-recipe.js.map +1 -0
  45. package/dist/tools/suggest-implementation.d.ts +3 -0
  46. package/dist/tools/suggest-implementation.d.ts.map +1 -0
  47. package/dist/tools/suggest-implementation.js +178 -0
  48. package/dist/tools/suggest-implementation.js.map +1 -0
  49. package/dist/transports/http.d.ts +2 -0
  50. package/dist/transports/http.d.ts.map +1 -0
  51. package/dist/transports/http.js +77 -0
  52. package/dist/transports/http.js.map +1 -0
  53. package/dist/transports/stdio.d.ts +3 -0
  54. package/dist/transports/stdio.d.ts.map +1 -0
  55. package/dist/transports/stdio.js +6 -0
  56. package/dist/transports/stdio.js.map +1 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -0
  58. package/dist/utils/format-catalog.d.ts +7 -0
  59. package/dist/utils/format-catalog.d.ts.map +1 -0
  60. package/dist/utils/format-catalog.js +93 -0
  61. package/dist/utils/format-catalog.js.map +1 -0
  62. package/dist/utils/paths.d.ts +7 -0
  63. package/dist/utils/paths.d.ts.map +1 -0
  64. package/dist/utils/paths.js +23 -0
  65. package/dist/utils/paths.js.map +1 -0
  66. package/dist/utils/search.d.ts +3 -0
  67. package/dist/utils/search.d.ts.map +1 -0
  68. package/dist/utils/search.js +44 -0
  69. package/dist/utils/search.js.map +1 -0
  70. package/package.json +42 -0
  71. package/src/data/catalog-loader.test.ts +42 -0
  72. package/src/data/catalog-loader.ts +78 -0
  73. package/src/data/component-loader.ts +68 -0
  74. package/src/data/design-system-loader.test.ts +82 -0
  75. package/src/data/design-system-loader.ts +125 -0
  76. package/src/data/icon-loader.test.ts +85 -0
  77. package/src/data/icon-loader.ts +90 -0
  78. package/src/data/recipe-loader.test.ts +49 -0
  79. package/src/data/recipe-loader.ts +131 -0
  80. package/src/data/template-loader.ts +55 -0
  81. package/src/design-linter/heuristics.ts +162 -0
  82. package/src/design-linter/index.ts +14 -0
  83. package/src/design-linter/linter.test.ts +257 -0
  84. package/src/design-linter/linter.ts +62 -0
  85. package/src/design-linter/rules.ts +348 -0
  86. package/src/design-linter/tokens.test.ts +80 -0
  87. package/src/design-linter/tokens.ts +203 -0
  88. package/src/design-linter/types.ts +66 -0
  89. package/src/design-manifest/index.ts +20 -0
  90. package/src/design-manifest/manifest.test.ts +175 -0
  91. package/src/design-manifest/manifest.ts +250 -0
  92. package/src/design-manifest/scan.test.ts +51 -0
  93. package/src/design-manifest/scan.ts +74 -0
  94. package/src/design-manifest/types.ts +40 -0
  95. package/src/design-rubric/rubric.test.ts +43 -0
  96. package/src/design-rubric/rubric.ts +140 -0
  97. package/src/eval/briefs.ts +104 -0
  98. package/src/eval/eval.test.ts +99 -0
  99. package/src/eval/index.ts +11 -0
  100. package/src/eval/score.ts +112 -0
  101. package/src/index.ts +75 -0
  102. package/src/prompts/design-prompts.test.ts +51 -0
  103. package/src/prompts/design-prompts.ts +127 -0
  104. package/src/resources/catalog.ts +23 -0
  105. package/src/resources/guides.ts +60 -0
  106. package/src/server.test.ts +69 -0
  107. package/src/server.ts +48 -0
  108. package/src/tools/find-components.ts +83 -0
  109. package/src/tools/find-icons.ts +77 -0
  110. package/src/tools/get-checklist.ts +139 -0
  111. package/src/tools/get-component.ts +204 -0
  112. package/src/tools/get-css-reference.ts +446 -0
  113. package/src/tools/get-design-context.ts +43 -0
  114. package/src/tools/get-design-principles.ts +72 -0
  115. package/src/tools/get-pattern.ts +69 -0
  116. package/src/tools/get-recipe.ts +80 -0
  117. package/src/tools/record-design-decision.ts +99 -0
  118. package/src/tools/suggest-implementation.ts +251 -0
  119. package/src/tools/sync-design-manifest.ts +92 -0
  120. package/src/tools/validate-design.ts +84 -0
  121. package/src/transports/http.ts +79 -0
  122. package/src/transports/stdio.ts +7 -0
  123. package/src/utils/format-catalog.test.ts +144 -0
  124. package/src/utils/format-catalog.ts +130 -0
  125. package/src/utils/paths.test.ts +101 -0
  126. package/src/utils/paths.ts +78 -0
  127. package/src/utils/search.test.ts +141 -0
  128. package/src/utils/search.ts +106 -0
  129. package/tsconfig.json +27 -0
  130. package/vitest.config.ts +15 -0
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { z } from 'zod';
4
+ import { emptyManifest, formatContext, parseManifest } from '../design-manifest/index.js';
5
+ import { getProjectManifestPath } from '../utils/paths.js';
6
+
7
+ export function registerGetDesignContextTool(server: McpServer): void {
8
+ server.tool(
9
+ 'get_design_context',
10
+ "Read this project's design manifest (design.manifest.md): the chosen paradigm / theme / density, which pages use which composition patterns, and the recorded design decisions (ADRs). Call this at the START of any UI task so generated code stays consistent with what the project has already committed to.",
11
+ {
12
+ manifestPath: z
13
+ .string()
14
+ .optional()
15
+ .describe(
16
+ 'Path to design.manifest.md. Defaults to ./design.manifest.md in the project root.'
17
+ )
18
+ },
19
+ { readOnlyHint: true },
20
+ async ({ manifestPath }) => {
21
+ const path = manifestPath ?? getProjectManifestPath();
22
+
23
+ let manifest: ReturnType<typeof emptyManifest>;
24
+ try {
25
+ manifest = parseManifest(await readFile(path, 'utf-8'));
26
+ } catch {
27
+ manifest = emptyManifest();
28
+ }
29
+
30
+ let text = formatContext(manifest);
31
+ if (!manifest.exists) {
32
+ text +=
33
+ `\n\n> No manifest found at \`${path}\`. Scaffold one with \`sync_design_manifest\`, ` +
34
+ 'or record the first decision with `record_design_decision`.\n';
35
+ }
36
+ text += '\n---\n\n**Next steps:**\n';
37
+ text += '- `get_pattern("<name>")` — the rules behind a pattern listed above\n';
38
+ text += '- `get_design_principles(topic="theming")` — the paradigm token profile\n';
39
+
40
+ return { content: [{ type: 'text' as const, text }] };
41
+ }
42
+ );
43
+ }
@@ -0,0 +1,72 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import {
4
+ extractPrincipleSection,
5
+ loadPrinciples,
6
+ PRINCIPLE_TOPICS
7
+ } from '../data/design-system-loader.js';
8
+ import { renderRubric } from '../design-rubric/rubric.js';
9
+
10
+ export function registerGetDesignPrinciplesTool(server: McpServer): void {
11
+ server.tool(
12
+ 'get_design_principles',
13
+ 'Get design heuristics and rules for building UIs with Urbicon UI. Covers visual hierarchy, interaction patterns, component selection heuristics, layout rules, accessibility, theming (token hierarchy, paradigms, change decision tree). Call this FIRST when generating new UI — before selecting components. Pass `as="rubric"` to instead get the 1–5 scoring rubric for judging a generated UI.',
14
+ {
15
+ topic: z
16
+ .enum(PRINCIPLE_TOPICS)
17
+ .optional()
18
+ .describe(
19
+ 'Filter to a specific topic. "theming" includes the design change decision tree, paradigm profiles, and token override guide. Omit for all principles.'
20
+ ),
21
+ as: z
22
+ .enum(['guide', 'rubric'])
23
+ .optional()
24
+ .describe(
25
+ 'Output mode. "guide" (default) returns the heuristics for building UI. "rubric" returns the 8-criterion 1–5 scoring rubric for judging a generated UI (ignores `topic`).'
26
+ )
27
+ },
28
+ { readOnlyHint: true },
29
+ async ({ topic, as }) => {
30
+ if (as === 'rubric') {
31
+ let text = renderRubric();
32
+ text += '\n---\n\n**Next steps:**\n';
33
+ text +=
34
+ '- `validate_design(code)` — deterministic correctness check that anchors criterion 8\n';
35
+ text +=
36
+ '- `get_design_principles(topic="theming")` — paradigm profiles to judge fidelity against\n';
37
+ return { content: [{ type: 'text' as const, text }] };
38
+ }
39
+
40
+ const principles = await loadPrinciples();
41
+
42
+ if (!principles) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: 'text' as const,
47
+ text: 'Design principles not found. Ensure design-system/principles.md exists at the monorepo root.'
48
+ }
49
+ ]
50
+ };
51
+ }
52
+
53
+ let text: string;
54
+
55
+ if (topic) {
56
+ const section = extractPrincipleSection(principles, topic);
57
+ text = section ?? principles;
58
+ } else {
59
+ text = principles;
60
+ }
61
+
62
+ text += '\n\n---\n\n**Next steps:**\n';
63
+ text += '- `get_pattern("<name>")` — composition patterns for specific page types\n';
64
+ text += '- `get_css_reference()` — CSS token names and override patterns\n';
65
+ text += '- `find_components()` — browse the component catalog\n';
66
+
67
+ return {
68
+ content: [{ type: 'text' as const, text }]
69
+ };
70
+ }
71
+ );
72
+ }
@@ -0,0 +1,69 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { getPatternByName, loadPatterns } from '../data/design-system-loader.js';
4
+
5
+ export function registerGetPatternTool(server: McpServer): void {
6
+ server.tool(
7
+ 'get_pattern',
8
+ 'Get a composition pattern for a specific page type. Patterns describe layout structure, component selection, spacing, and behavioral rules for common page archetypes (settings-page, dashboard, form-page). Use after consulting design principles.',
9
+ {
10
+ name: z
11
+ .string()
12
+ .optional()
13
+ .describe(
14
+ 'Pattern name (e.g. "settings-page", "dashboard", "form-page"). Omit to list all available patterns.'
15
+ )
16
+ },
17
+ { readOnlyHint: true },
18
+ async ({ name }) => {
19
+ if (!name) {
20
+ const patterns = await loadPatterns();
21
+
22
+ if (patterns.length === 0) {
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text' as const,
27
+ text: 'No composition patterns found. Ensure design-system/patterns/*.md exists at the monorepo root.'
28
+ }
29
+ ]
30
+ };
31
+ }
32
+
33
+ let md = '# Available Composition Patterns\n\n';
34
+ for (const p of patterns) {
35
+ md += `- **${p.title}** (\`${p.name}\`) — ${p.description}\n`;
36
+ }
37
+ md += '\n> Use `get_pattern("<name>")` to get the full pattern.\n';
38
+ md += '\n---\n\n**Next steps:**\n';
39
+ md += '- `get_design_principles()` — design heuristics and rules\n';
40
+ md += '- `get_css_reference()` — CSS token names and override patterns\n';
41
+ return { content: [{ type: 'text' as const, text: md }] };
42
+ }
43
+
44
+ const pattern = await getPatternByName(name);
45
+
46
+ if (!pattern) {
47
+ const all = await loadPatterns();
48
+ const available = all.map((p) => `\`${p.name}\``).join(', ');
49
+ return {
50
+ content: [
51
+ {
52
+ type: 'text' as const,
53
+ text: `Pattern "${name}" not found. Available patterns: ${available}`
54
+ }
55
+ ]
56
+ };
57
+ }
58
+
59
+ let md = pattern.content;
60
+ md += '\n\n---\n\n**Next steps:**\n';
61
+ md += '- `get_design_principles()` — design heuristics and rules\n';
62
+ md += '- `get_css_reference()` — CSS token names and override patterns\n';
63
+ md += '- `suggest_implementation("<description>")` — generate a component skeleton\n';
64
+ md += '- `get_recipe("<id>")` — get a complete code recipe\n';
65
+
66
+ return { content: [{ type: 'text' as const, text: md }] };
67
+ }
68
+ );
69
+ }
@@ -0,0 +1,80 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { getRecipeById, loadRecipes } from '../data/recipe-loader.js';
4
+
5
+ export function registerGetRecipeTool(server: McpServer): void {
6
+ server.tool(
7
+ 'get_recipe',
8
+ 'Get a complete, production-ready Svelte 5 code recipe.',
9
+ {
10
+ scenario: z
11
+ .string()
12
+ .describe(
13
+ 'Recipe id: login, settings, dashboard, pricing, profile-card, data-table, command-palette'
14
+ )
15
+ },
16
+ { readOnlyHint: true },
17
+ async ({ scenario }) => {
18
+ const recipe = await getRecipeById(scenario);
19
+
20
+ if (!recipe) {
21
+ const allRecipes = await loadRecipes();
22
+ const available = allRecipes.map((r) => r.id).join(', ');
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text' as const,
27
+ text: `Recipe "${scenario}" not found. Available recipes: ${available}`
28
+ }
29
+ ]
30
+ };
31
+ }
32
+
33
+ let md = `# Recipe: ${recipe.title}\n\n`;
34
+ if (recipe.description) md += `${recipe.description}\n\n`;
35
+ if (recipe.pattern) {
36
+ md += `**Composition pattern:** \`${recipe.pattern}\` — this recipe is one instance of that page archetype. Call \`get_pattern("${recipe.pattern}")\` for the layout/spacing/component-selection rules behind it.\n\n`;
37
+ }
38
+ if (recipe.components.length > 0) {
39
+ md += `**Components used:** ${recipe.components.join(', ')}\n\n`;
40
+ }
41
+ if (recipe.features.length > 0) {
42
+ md += '**Features:**\n';
43
+ for (const f of recipe.features) {
44
+ md += `- ${f}\n`;
45
+ }
46
+ md += '\n';
47
+ }
48
+ if (recipe.code) {
49
+ md += `\`\`\`svelte\n${recipe.code}\n\`\`\`\n`;
50
+ }
51
+
52
+ // Cross-references
53
+ md += '\n---\n\n**Next steps:**\n';
54
+ if (recipe.components.length > 0) {
55
+ for (const comp of recipe.components.slice(0, 5)) {
56
+ const slug = comp.replace(
57
+ /([A-Z])/g,
58
+ (_, c: string, i: number) => (i > 0 ? '-' : '') + c.toLowerCase()
59
+ );
60
+ md += `- \`get_component("${slug}")\` — ${comp} API docs\n`;
61
+ }
62
+ }
63
+ if (recipe.pattern) {
64
+ md += `- \`get_pattern("${recipe.pattern}")\` — the composition pattern this recipe follows\n`;
65
+ }
66
+ md += '- `get_implementation_checklist()` — design quality rules\n';
67
+ md += '- `get_css_reference()` — CSS token names and override patterns\n';
68
+ md += '- `validate_design(code)` — lint your adaptation before shipping\n';
69
+
70
+ return {
71
+ content: [
72
+ {
73
+ type: 'text' as const,
74
+ text: md
75
+ }
76
+ ]
77
+ };
78
+ }
79
+ );
80
+ }
@@ -0,0 +1,99 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { z } from 'zod';
4
+ import { appendDecision, createManifestTemplate, parseManifest } from '../design-manifest/index.js';
5
+ import { getProjectManifestPath, isWithinProjectDir } from '../utils/paths.js';
6
+
7
+ export function registerRecordDesignDecisionTool(server: McpServer): void {
8
+ server.tool(
9
+ 'record_design_decision',
10
+ 'Append a design decision (ADR) to the project design manifest — record a deliberate deviation from a pattern or principle so future sessions and other developers honour it. Creates design.manifest.md if it does not exist yet.',
11
+ {
12
+ title: z
13
+ .string()
14
+ .min(1)
15
+ .describe('Short decision title, e.g. "Tabs for settings instead of sidebar".'),
16
+ decision: z.string().min(1).describe('What was decided — concrete and imperative.'),
17
+ rationale: z
18
+ .string()
19
+ .optional()
20
+ .describe('Why — the trade-off that justifies the deviation.'),
21
+ status: z
22
+ .enum(['accepted', 'proposed', 'superseded'])
23
+ .optional()
24
+ .describe('Decision status. Defaults to "accepted".'),
25
+ date: z
26
+ .string()
27
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
28
+ .optional()
29
+ .describe('ISO date (YYYY-MM-DD). Defaults to today.'),
30
+ manifestPath: z
31
+ .string()
32
+ .optional()
33
+ .describe('Path to design.manifest.md. Defaults to the project root.')
34
+ },
35
+ { readOnlyHint: false },
36
+ async ({ title, decision, rationale, status, date, manifestPath }) => {
37
+ const path = manifestPath ?? getProjectManifestPath();
38
+ if (!path.endsWith('.md')) {
39
+ return {
40
+ content: [
41
+ { type: 'text' as const, text: `Refusing to write: "${path}" is not a .md file.` }
42
+ ],
43
+ isError: true
44
+ };
45
+ }
46
+ if (manifestPath && !isWithinProjectDir(manifestPath)) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text' as const,
51
+ text: `Refusing to write outside the project root: "${path}".`
52
+ }
53
+ ],
54
+ isError: true
55
+ };
56
+ }
57
+
58
+ let content: string;
59
+ let created = false;
60
+ try {
61
+ content = await readFile(path, 'utf-8');
62
+ } catch {
63
+ content = createManifestTemplate({});
64
+ created = true;
65
+ }
66
+
67
+ const today = date ?? new Date().toISOString().slice(0, 10);
68
+ const updated = appendDecision(content, {
69
+ date: today,
70
+ title,
71
+ status: status ?? 'accepted',
72
+ decision,
73
+ rationale
74
+ });
75
+
76
+ try {
77
+ await writeFile(path, updated, 'utf-8');
78
+ } catch (err) {
79
+ return {
80
+ content: [
81
+ { type: 'text' as const, text: `Failed to write ${path}: ${(err as Error).message}` }
82
+ ],
83
+ isError: true
84
+ };
85
+ }
86
+
87
+ const total = parseManifest(updated).decisions.length;
88
+ const note = created ? ' (created the manifest)' : '';
89
+ return {
90
+ content: [
91
+ {
92
+ type: 'text' as const,
93
+ text: `Recorded ADR "${title}" dated ${today} in \`${path}\`${note}. ${total} decision(s) on record.`
94
+ }
95
+ ]
96
+ };
97
+ }
98
+ );
99
+ }
@@ -0,0 +1,251 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { ComponentCatalogEntry } from '../data/catalog-loader.js';
4
+ import { loadCatalog } from '../data/catalog-loader.js';
5
+ import { loadRecipes } from '../data/recipe-loader.js';
6
+ import { matchComponents } from '../utils/search.js';
7
+
8
+ /** Default props and skeleton hints per component type */
9
+ const SKELETON_HINTS: Record<string, { attrs: string; children?: string; selfClosing?: boolean }> =
10
+ {
11
+ Button: { attrs: 'onclick={handleClick}', children: 'Click me' },
12
+ Input: { attrs: 'label="Label" bind:value={value} placeholder="..."', selfClosing: true },
13
+ Textarea: { attrs: 'label="Label" bind:value={text} placeholder="..."', selfClosing: true },
14
+ Card: { attrs: 'variant="outlined"', children: ' <!-- card content -->' },
15
+ Checkbox: { attrs: 'label="Label" bind:checked={checked}', selfClosing: true },
16
+ Toggle: { attrs: 'label="Enable" bind:checked={enabled}', selfClosing: true },
17
+ Select: { attrs: 'label="Choose" bind:value={selected}', children: '...' },
18
+ Combobox: { attrs: 'label="Search" bind:value={selected}', selfClosing: true },
19
+ RadioGroup: { attrs: 'bind:value={selected}', children: '...' },
20
+ Slider: { attrs: 'bind:value={sliderValue}', selfClosing: true },
21
+ Alert: { attrs: 'intent="info"', children: 'Message text' },
22
+ Toast: { attrs: '', children: '' },
23
+ Badge: { attrs: '', children: 'Label' },
24
+ Dialog: { attrs: 'bind:open={showDialog} title="Title"', children: ' <!-- dialog body -->' },
25
+ Drawer: {
26
+ attrs: 'bind:open={showDrawer} placement="right"',
27
+ children: ' <!-- drawer content -->'
28
+ },
29
+ Avatar: { attrs: 'src="/avatar.jpg" alt="User"', selfClosing: true },
30
+ Spinner: { attrs: '', selfClosing: true },
31
+ Progress: { attrs: 'value={progress}', selfClosing: true },
32
+ Skeleton: { attrs: 'class="h-4 w-full"', selfClosing: true },
33
+ Separator: { attrs: '', selfClosing: true },
34
+ Tab: { attrs: '', children: '...' },
35
+ Breadcrumb: { attrs: '', children: '...' },
36
+ Pagination: { attrs: 'total={100} bind:page={page}', selfClosing: true },
37
+ Stepper: { attrs: 'bind:activeStep={step}', children: '...' },
38
+ Accordion: { attrs: '', children: '...' },
39
+ Collapsible: { attrs: '', children: '...' },
40
+ Sidebar: { attrs: 'bind:open={sidebarOpen}', children: ' <!-- nav links -->' },
41
+ Menu: { attrs: '', children: '...' },
42
+ Toolbar: { attrs: 'aria-label="Actions"', children: '...' },
43
+ Tooltip: { attrs: 'label="Tooltip text"', children: '...' },
44
+ Popover: { attrs: '', children: '...' },
45
+ Calendar: { attrs: 'bind:value={date}', selfClosing: true },
46
+ Planner: {
47
+ attrs: 'view="week" items={items} getDate={(item) => item.date}',
48
+ children:
49
+ ' {#snippet cell({ items, isoDate })}\n <!-- your own per-day content; items are bucketed + typed -->\n {/snippet}'
50
+ },
51
+ CommandPalette: { attrs: 'bind:open={showPalette} items={commands}', selfClosing: true },
52
+ Table: { attrs: 'data={rows} columns={columns}', selfClosing: true },
53
+ ButtonGroup: { attrs: 'selection="single" bind:value={selected}', children: '...' },
54
+ DatePicker: { attrs: 'label="Date" bind:value={date}', selfClosing: true },
55
+ DateRangePicker: {
56
+ attrs: 'label="Date range" bind:startDate={start} bind:endDate={end}',
57
+ selfClosing: true
58
+ },
59
+ SegmentGroup: { attrs: 'bind:value={selected}', children: '...' },
60
+ LocaleSwitcher: { attrs: '', selfClosing: true },
61
+ ThemeSwitcher: { attrs: '', selfClosing: true },
62
+ FileUpload: {
63
+ attrs: 'bind:files multiple title="Drop files here"',
64
+ selfClosing: true
65
+ }
66
+ };
67
+
68
+ /** Compact implementation rules — embedded to avoid an extra get_implementation_checklist call */
69
+ const IMPLEMENTATION_RULES = `## Implementation Rules
70
+
71
+ - **CSS imports** — Add to root layout, Tailwind first: \`@import 'tailwindcss';\` then \`@import '@urbicon-ui/blocks/style/index.css';\` (ships tokens + the \`@source\` directives). Import \`index.css\`, NOT the \`foundation\`/\`semantic\`/\`interaction\` subfiles, and add no manual \`@source\`
72
+ - **Semantic tokens only** — Use \`bg-surface-elevated\`, \`text-text-primary\`, \`border-border-default\` — never raw Tailwind colors
73
+ - **Dark mode** — Automatic via \`prefers-color-scheme\`. Do NOT add \`dark:\` overrides
74
+ - **Focus** — Always \`focus-visible:\` (not \`focus:\`) for keyboard-only focus rings
75
+ - **State binding** — \`bind:value\`, \`bind:checked\`, \`bind:open\` for two-way state; callback props (\`onValueChange\`) for side effects
76
+ - **Custom content** — Use Svelte 5 snippets (\`{#snippet name()}...{/snippet}\`), not legacy slots
77
+ - **Styling overrides** — \`class\` for simple additions, \`slotClasses\` for per-slot targeting, \`unstyled\` to strip all defaults
78
+ - **Mint** — Add \`mint="scale"\` or \`mint="ripple"\` sparingly on primary CTAs only
79
+
80
+ ## Design Quality
81
+
82
+ - **Vary visual weight** — Don't use the same Card variant/padding everywhere. Reading-flow content → \`variant="quiet"\` (default) + \`padding="md"\`. Architectural delineation → \`variant="outlined"\` + \`padding="md"\`. Lifted content (cards-on-page) → \`variant="elevated"\` + \`padding="lg"\`. Popover-family floating surfaces → \`variant="floating"\`.
83
+ - **Color = meaning** — Neutral surfaces dominate (80–90%). Use \`intent\` colors ONLY for semantic meaning (status, severity, actions) — never as decoration.
84
+ - **Spacing = hierarchy** — Tight (\`gap-2\`/\`gap-3\`) within related items. Generous (\`gap-8\`/\`gap-10\`) between sections. Don't use uniform spacing everywhere.
85
+ - **Commit to a radius** — Pick a radius philosophy (\`rounded-lg\`, \`rounded-xl\`, or \`rounded-2xl\`) and apply it consistently via \`class\` or \`slotClasses\`. Don't rely solely on component defaults.
86
+ - **Data-driven styling** — Different states/severities should look visually distinct (vary padding, font-weight, Badge variant, text color) — not just carry a different label.
87
+ - **Don't copy recipe styling** — Recipes show ONE interpretation. Create YOUR visual identity with your own spacing rhythm, color distribution, and layout density.
88
+ `;
89
+
90
+ export function registerSuggestImplementationTool(server: McpServer): void {
91
+ server.tool(
92
+ 'suggest_implementation',
93
+ 'Generate a Svelte 5 skeleton using Urbicon UI components. Either specify component names directly (preferred — pick them from the catalog first) or describe what you want to build.',
94
+ {
95
+ description: z.string().describe('What you want to build, e.g. "login form with validation"'),
96
+ components: z
97
+ .array(z.string())
98
+ .optional()
99
+ .describe(
100
+ 'Specific component names to use (e.g. ["Input", "Button", "Card"]). If omitted, components are auto-matched from the description.'
101
+ ),
102
+ style: z.enum(['minimal', 'polished']).default('polished')
103
+ },
104
+ { readOnlyHint: true, openWorldHint: true },
105
+ async ({ description, components: requestedComponents, style }) => {
106
+ const catalog = await loadCatalog();
107
+ const recipes = await loadRecipes();
108
+
109
+ let matched: ComponentCatalogEntry[];
110
+
111
+ if (requestedComponents && requestedComponents.length > 0) {
112
+ // Use explicitly requested components — look them up in catalog
113
+ const catalogMap = new Map(catalog.components.map((c) => [c.name.toLowerCase(), c]));
114
+ matched = requestedComponents
115
+ .map((name) => catalogMap.get(name.toLowerCase()))
116
+ .filter((c): c is ComponentCatalogEntry => c !== undefined);
117
+ } else {
118
+ // Fall back to search-based matching
119
+ matched = matchComponents(catalog.components, description, undefined, 8);
120
+ }
121
+
122
+ if (matched.length === 0) {
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text' as const,
127
+ text: `No matching components found for "${description}". Try specifying component names directly via the \`components\` parameter, or browse the catalog with \`find_components\`.`
128
+ }
129
+ ]
130
+ };
131
+ }
132
+
133
+ // Find matching recipes — ID substring match OR ≥40% component overlap
134
+ const descLower = description.toLowerCase();
135
+ const matchedNames = new Set(matched.map((c) => c.name));
136
+ const matchingRecipes = recipes.filter((r) => {
137
+ // Direct ID match: description contains the recipe ID (e.g. "login" in "login form")
138
+ if (descLower.includes(r.id)) return true;
139
+ // Component overlap: at least 40% of recipe's components are in matched set
140
+ if (r.components.length > 0) {
141
+ const overlap = r.components.filter((c) => matchedNames.has(c)).length;
142
+ if (overlap / r.components.length >= 0.4) return true;
143
+ }
144
+ return false;
145
+ });
146
+
147
+ const imports = matched.map((c) => c.name);
148
+
149
+ let md = '# Implementation Suggestion\n\n';
150
+ md += `> ${description}\n\n`;
151
+
152
+ // Imports
153
+ md += '## Imports\n\n';
154
+ md += '```svelte\n<script lang="ts">\n';
155
+ md += ` import { ${imports.join(', ')} } from '@urbicon-ui/blocks';\n`;
156
+ md += '</script>\n```\n\n';
157
+
158
+ // Component details with prop types
159
+ md += '## Components\n\n';
160
+ for (const comp of matched) {
161
+ const meaningfulVariants = comp.variants
162
+ .filter((v) => {
163
+ const sorted = [...v.values].sort();
164
+ return !(
165
+ (sorted.length === 1 && (sorted[0] === 'true' || sorted[0] === 'false')) ||
166
+ (sorted.length === 2 && sorted[0] === 'false' && sorted[1] === 'true')
167
+ );
168
+ })
169
+ .map((v) => {
170
+ const def = v.default ? ` (default: ${v.default})` : '';
171
+ return `${v.name}: ${v.values.join('/')}${def}`;
172
+ })
173
+ .join(' · ');
174
+
175
+ md += `- **${comp.name}** — ${comp.description}`;
176
+ if (meaningfulVariants) md += ` | ${meaningfulVariants}`;
177
+ md += '\n';
178
+
179
+ // Show non-primitive prop types so LLMs understand the API shape
180
+ const propTypes = comp.keyPropTypes || {};
181
+ const typeEntries = Object.entries(propTypes);
182
+ if (typeEntries.length > 0) {
183
+ const formatted = typeEntries.map(([name, type]) => `\`${name}: ${type}\``).join(', ');
184
+ md += ` Key props: ${formatted}\n`;
185
+ }
186
+
187
+ if (comp.slots.length > 0) {
188
+ md += ` Slots: \`${comp.slots.join('`, `')}\`\n`;
189
+ }
190
+ }
191
+ md += '\n';
192
+
193
+ // Skeleton
194
+ md += '## Skeleton\n\n';
195
+ md += '```svelte\n<script lang="ts">\n';
196
+ md += ` import { ${imports.join(', ')} } from '@urbicon-ui/blocks';\n`;
197
+ md += '</script>\n\n';
198
+ md += generateSkeleton(matched, style);
199
+ md += '```\n\n';
200
+
201
+ // Embedded implementation rules (replaces separate checklist call)
202
+ md += IMPLEMENTATION_RULES;
203
+ md += '\n';
204
+
205
+ // Related recipes
206
+ if (matchingRecipes.length > 0) {
207
+ md += '## Related Recipes\n\n';
208
+ md += 'Use `get_recipe` tool to get full production-ready code:\n\n';
209
+ for (const recipe of matchingRecipes) {
210
+ md += `- **${recipe.title}** (\`${recipe.id}\`) — ${recipe.description}\n`;
211
+ }
212
+ md += '\n';
213
+ }
214
+
215
+ // Drill-down hint
216
+ md += '## Next Steps\n\n';
217
+ md += `For full API docs on any component, use \`get_component\`:\n`;
218
+ for (const comp of matched.slice(0, 5)) {
219
+ md += `- \`get_component("${comp.slug}")\`\n`;
220
+ }
221
+ md += '\nOther useful tools:\n';
222
+ md += '- `get_design_principles()` — design heuristics and theming guide\n';
223
+ md += '- `get_pattern("<name>")` — composition pattern for this page type\n';
224
+ md += '- `get_css_reference()` — CSS token names and override patterns\n';
225
+ md += '- `find_icons()` — browse all available icons\n';
226
+
227
+ return {
228
+ content: [{ type: 'text' as const, text: md }]
229
+ };
230
+ }
231
+ );
232
+ }
233
+
234
+ function generateSkeleton(components: ComponentCatalogEntry[], _style: string): string {
235
+ const lines: string[] = [];
236
+
237
+ for (const comp of components.slice(0, 6)) {
238
+ const hints = SKELETON_HINTS[comp.name];
239
+ const attrs = hints?.attrs || '';
240
+ const attrsStr = attrs ? ` ${attrs}` : '';
241
+
242
+ if (hints?.selfClosing) {
243
+ lines.push(`<${comp.name}${attrsStr} />`);
244
+ } else {
245
+ const children = hints?.children || '...';
246
+ lines.push(`<${comp.name}${attrsStr}>${children}</${comp.name}>`);
247
+ }
248
+ }
249
+
250
+ return `${lines.join('\n')}\n`;
251
+ }