@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.
- package/README.md +161 -0
- package/dist/data/catalog-loader.d.ts +37 -0
- package/dist/data/catalog-loader.d.ts.map +1 -0
- package/dist/data/catalog-loader.js +15 -0
- package/dist/data/catalog-loader.js.map +1 -0
- package/dist/data/component-loader.d.ts +2 -0
- package/dist/data/component-loader.d.ts.map +1 -0
- package/dist/data/component-loader.js +17 -0
- package/dist/data/component-loader.js.map +1 -0
- package/dist/data/recipe-loader.d.ts +4 -0
- package/dist/data/recipe-loader.d.ts.map +1 -0
- package/dist/data/recipe-loader.js +102 -0
- package/dist/data/recipe-loader.js.map +1 -0
- package/dist/data/template-loader.d.ts +8 -0
- package/dist/data/template-loader.d.ts.map +1 -0
- package/dist/data/template-loader.js +33 -0
- package/dist/data/template-loader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/catalog.d.ts +3 -0
- package/dist/resources/catalog.d.ts.map +1 -0
- package/dist/resources/catalog.js +20 -0
- package/dist/resources/catalog.js.map +1 -0
- package/dist/resources/component.d.ts +3 -0
- package/dist/resources/component.d.ts.map +1 -0
- package/dist/resources/component.js +29 -0
- package/dist/resources/component.js.map +1 -0
- package/dist/resources/guides.d.ts +3 -0
- package/dist/resources/guides.d.ts.map +1 -0
- package/dist/resources/guides.js +36 -0
- package/dist/resources/guides.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/find-components.d.ts +3 -0
- package/dist/tools/find-components.d.ts.map +1 -0
- package/dist/tools/find-components.js +21 -0
- package/dist/tools/find-components.js.map +1 -0
- package/dist/tools/get-recipe.d.ts +3 -0
- package/dist/tools/get-recipe.d.ts.map +1 -0
- package/dist/tools/get-recipe.js +48 -0
- package/dist/tools/get-recipe.js.map +1 -0
- package/dist/tools/suggest-implementation.d.ts +3 -0
- package/dist/tools/suggest-implementation.d.ts.map +1 -0
- package/dist/tools/suggest-implementation.js +178 -0
- package/dist/tools/suggest-implementation.js.map +1 -0
- package/dist/transports/http.d.ts +2 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +77 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/stdio.d.ts +3 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +6 -0
- package/dist/transports/stdio.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/format-catalog.d.ts +7 -0
- package/dist/utils/format-catalog.d.ts.map +1 -0
- package/dist/utils/format-catalog.js +93 -0
- package/dist/utils/format-catalog.js.map +1 -0
- package/dist/utils/paths.d.ts +7 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +23 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/search.d.ts +3 -0
- package/dist/utils/search.d.ts.map +1 -0
- package/dist/utils/search.js +44 -0
- package/dist/utils/search.js.map +1 -0
- package/package.json +42 -0
- package/src/data/catalog-loader.test.ts +42 -0
- package/src/data/catalog-loader.ts +78 -0
- package/src/data/component-loader.ts +68 -0
- package/src/data/design-system-loader.test.ts +82 -0
- package/src/data/design-system-loader.ts +125 -0
- package/src/data/icon-loader.test.ts +85 -0
- package/src/data/icon-loader.ts +90 -0
- package/src/data/recipe-loader.test.ts +49 -0
- package/src/data/recipe-loader.ts +131 -0
- package/src/data/template-loader.ts +55 -0
- package/src/design-linter/heuristics.ts +162 -0
- package/src/design-linter/index.ts +14 -0
- package/src/design-linter/linter.test.ts +257 -0
- package/src/design-linter/linter.ts +62 -0
- package/src/design-linter/rules.ts +348 -0
- package/src/design-linter/tokens.test.ts +80 -0
- package/src/design-linter/tokens.ts +203 -0
- package/src/design-linter/types.ts +66 -0
- package/src/design-manifest/index.ts +20 -0
- package/src/design-manifest/manifest.test.ts +175 -0
- package/src/design-manifest/manifest.ts +250 -0
- package/src/design-manifest/scan.test.ts +51 -0
- package/src/design-manifest/scan.ts +74 -0
- package/src/design-manifest/types.ts +40 -0
- package/src/design-rubric/rubric.test.ts +43 -0
- package/src/design-rubric/rubric.ts +140 -0
- package/src/eval/briefs.ts +104 -0
- package/src/eval/eval.test.ts +99 -0
- package/src/eval/index.ts +11 -0
- package/src/eval/score.ts +112 -0
- package/src/index.ts +75 -0
- package/src/prompts/design-prompts.test.ts +51 -0
- package/src/prompts/design-prompts.ts +127 -0
- package/src/resources/catalog.ts +23 -0
- package/src/resources/guides.ts +60 -0
- package/src/server.test.ts +69 -0
- package/src/server.ts +48 -0
- package/src/tools/find-components.ts +83 -0
- package/src/tools/find-icons.ts +77 -0
- package/src/tools/get-checklist.ts +139 -0
- package/src/tools/get-component.ts +204 -0
- package/src/tools/get-css-reference.ts +446 -0
- package/src/tools/get-design-context.ts +43 -0
- package/src/tools/get-design-principles.ts +72 -0
- package/src/tools/get-pattern.ts +69 -0
- package/src/tools/get-recipe.ts +80 -0
- package/src/tools/record-design-decision.ts +99 -0
- package/src/tools/suggest-implementation.ts +251 -0
- package/src/tools/sync-design-manifest.ts +92 -0
- package/src/tools/validate-design.ts +84 -0
- package/src/transports/http.ts +79 -0
- package/src/transports/stdio.ts +7 -0
- package/src/utils/format-catalog.test.ts +144 -0
- package/src/utils/format-catalog.ts +130 -0
- package/src/utils/paths.test.ts +101 -0
- package/src/utils/paths.ts +78 -0
- package/src/utils/search.test.ts +141 -0
- package/src/utils/search.ts +106 -0
- package/tsconfig.json +27 -0
- 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
|
+
}
|