@urbicon-ui/mcp-server 6.1.5 → 6.1.6
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 +44 -41
- package/package.json +3 -1
- package/src/data/catalog-loader.test.ts +1 -1
- package/src/data/catalog-loader.ts +12 -37
- package/src/data/component-loader.ts +5 -40
- package/src/data/design-system-loader.test.ts +1 -1
- package/src/data/design-system-loader.ts +1 -1
- package/src/data/icon-loader.test.ts +25 -80
- package/src/data/icon-loader.ts +8 -68
- package/src/data/template-loader.ts +1 -1
- package/src/data/verb-loader.ts +29 -0
- package/src/eval/eval.test.ts +16 -9
- package/src/eval/score.ts +26 -10
- package/src/index.ts +7 -14
- package/src/prompts/design-prompts.test.ts +56 -28
- package/src/prompts/design-prompts.ts +135 -104
- package/src/server.test.ts +16 -7
- package/src/server.ts +4 -7
- package/src/tools/find-components.ts +1 -1
- package/src/tools/get-design-principles.ts +1 -1
- package/src/tools/get-recipe.ts +6 -4
- package/src/tools/suggest-implementation.ts +2 -3
- package/src/tools/validate-design.ts +17 -9
- package/src/data/recipe-loader.test.ts +0 -49
- package/src/data/recipe-loader.ts +0 -131
- package/src/design-linter/heuristics.ts +0 -162
- package/src/design-linter/index.ts +0 -14
- package/src/design-linter/linter.test.ts +0 -257
- package/src/design-linter/linter.ts +0 -62
- package/src/design-linter/rules.ts +0 -348
- package/src/design-linter/tokens.test.ts +0 -80
- package/src/design-linter/tokens.ts +0 -203
- package/src/design-linter/types.ts +0 -66
- package/src/design-manifest/index.ts +0 -20
- package/src/design-manifest/manifest.test.ts +0 -175
- package/src/design-manifest/manifest.ts +0 -250
- package/src/design-manifest/scan.test.ts +0 -51
- package/src/design-manifest/scan.ts +0 -74
- package/src/design-manifest/types.ts +0 -40
- package/src/design-rubric/rubric.test.ts +0 -43
- package/src/design-rubric/rubric.ts +0 -140
- package/src/tools/get-design-context.ts +0 -43
- package/src/tools/record-design-decision.ts +0 -99
- package/src/tools/sync-design-manifest.ts +0 -92
- package/src/utils/paths.test.ts +0 -101
- package/src/utils/paths.ts +0 -78
- package/src/utils/search.test.ts +0 -141
- package/src/utils/search.ts +0 -106
package/src/eval/score.ts
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
* tables, so a new run is directly comparable to that +33.8% baseline.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { lintDesign } from '
|
|
11
|
-
import { MAX_RUBRIC_SCORE, RUBRIC_CRITERIA } from '
|
|
10
|
+
import { lintDesign } from '@urbicon-ui/design-engine/linter';
|
|
11
|
+
import { MAX_RUBRIC_SCORE, RUBRIC_CRITERIA } from '@urbicon-ui/design-engine/rubric';
|
|
12
12
|
|
|
13
13
|
export interface LinterScore {
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Stage-1 correctness axis, 0–100 (deterministic defects only). The stable A/B
|
|
16
|
+
* headline metric: unaffected by the slop-floor heuristics, so it stays directly
|
|
17
|
+
* comparable to the pre-slop-floor baseline (the +33.8% measurement).
|
|
18
|
+
*/
|
|
19
|
+
correctness: number;
|
|
20
|
+
/** Stage-2 slop-floor axis, 0–100 (system-agnostic "looks generic" heuristics). */
|
|
21
|
+
slop: number;
|
|
16
22
|
errors: number;
|
|
17
23
|
warnings: number;
|
|
18
24
|
infos: number;
|
|
@@ -35,7 +41,8 @@ export interface EvalEntry {
|
|
|
35
41
|
export function scoreImplementation(code: string): LinterScore {
|
|
36
42
|
const r = lintDesign(code);
|
|
37
43
|
return {
|
|
38
|
-
|
|
44
|
+
correctness: r.scores.correctness,
|
|
45
|
+
slop: r.scores.slop,
|
|
39
46
|
errors: r.counts.error,
|
|
40
47
|
warnings: r.counts.warning,
|
|
41
48
|
infos: r.counts.info
|
|
@@ -76,29 +83,38 @@ export function formatAbReport(entries: EvalEntry[], baseline: string, treatment
|
|
|
76
83
|
|
|
77
84
|
let md = `# Eval A/B — ${baseline} vs ${treatment}\n\n`;
|
|
78
85
|
md += '## Per-brief\n\n';
|
|
79
|
-
md += `| Brief | ${baseline}
|
|
86
|
+
md += `| Brief | ${baseline} correctness | ${treatment} correctness | ${baseline} rubric | ${treatment} rubric |\n`;
|
|
80
87
|
md += '|---|---|---|---|---|\n';
|
|
81
88
|
|
|
82
89
|
const baseLint: number[] = [];
|
|
83
90
|
const treatLint: number[] = [];
|
|
91
|
+
const baseSlop: number[] = [];
|
|
92
|
+
const treatSlop: number[] = [];
|
|
84
93
|
const baseRub: number[] = [];
|
|
85
94
|
const treatRub: number[] = [];
|
|
86
95
|
|
|
87
96
|
for (const id of briefIds) {
|
|
88
97
|
const b = pick(id, baseline);
|
|
89
98
|
const t = pick(id, treatment);
|
|
90
|
-
if (b)
|
|
91
|
-
|
|
99
|
+
if (b) {
|
|
100
|
+
baseLint.push(b.linter.correctness);
|
|
101
|
+
baseSlop.push(b.linter.slop);
|
|
102
|
+
}
|
|
103
|
+
if (t) {
|
|
104
|
+
treatLint.push(t.linter.correctness);
|
|
105
|
+
treatSlop.push(t.linter.slop);
|
|
106
|
+
}
|
|
92
107
|
if (b?.rubricTotal !== undefined) baseRub.push(b.rubricTotal);
|
|
93
108
|
if (t?.rubricTotal !== undefined) treatRub.push(t.rubricTotal);
|
|
94
109
|
const rub = (s?: ImplementationScore) =>
|
|
95
110
|
s?.rubricTotal !== undefined ? `${s.rubricTotal}/${MAX_RUBRIC_SCORE}` : '—';
|
|
96
|
-
md += `| ${id} | ${b?.linter.
|
|
111
|
+
md += `| ${id} | ${b?.linter.correctness ?? '—'} | ${t?.linter.correctness ?? '—'} | ${rub(b)} | ${rub(t)} |\n`;
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
md += '\n## Aggregate\n\n';
|
|
100
115
|
md += `| Metric | ${baseline} | ${treatment} | Δ |\n|---|---|---|---|\n`;
|
|
101
|
-
md += `| Mean
|
|
116
|
+
md += `| Mean correctness | ${mean(baseLint).toFixed(1)} | ${mean(treatLint).toFixed(1)} | ${pct(mean(baseLint), mean(treatLint))} |\n`;
|
|
117
|
+
md += `| Mean slop-floor | ${mean(baseSlop).toFixed(1)} | ${mean(treatSlop).toFixed(1)} | ${pct(mean(baseSlop), mean(treatSlop))} |\n`;
|
|
102
118
|
if (baseRub.length && treatRub.length) {
|
|
103
119
|
md += `| Mean rubric /${MAX_RUBRIC_SCORE} | ${mean(baseRub).toFixed(1)} | ${mean(treatRub).toFixed(1)} | ${pct(mean(baseRub), mean(treatRub))} |\n`;
|
|
104
120
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { loadCatalog } from './data/catalog-loader.js';
|
|
4
4
|
import { loadPatterns, loadPrinciples } from './data/design-system-loader.js';
|
|
5
|
-
import { loadRecipes } from './data/recipe-loader.js';
|
|
6
5
|
import { loadTemplateSections } from './data/template-loader.js';
|
|
7
6
|
import { createServer } from './server.js';
|
|
8
7
|
import { startHttpTransport } from './transports/http.js';
|
|
@@ -11,7 +10,7 @@ import { startStdioTransport } from './transports/stdio.js';
|
|
|
11
10
|
interface CliArgs {
|
|
12
11
|
transport: 'stdio' | 'http';
|
|
13
12
|
port: number;
|
|
14
|
-
|
|
13
|
+
contentDir?: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
function parseArgs(args: string[]): CliArgs {
|
|
@@ -32,8 +31,8 @@ function parseArgs(args: string[]): CliArgs {
|
|
|
32
31
|
} else if (arg === '--port' && next) {
|
|
33
32
|
result.port = parseInt(next, 10);
|
|
34
33
|
i++;
|
|
35
|
-
} else if (arg === '--
|
|
36
|
-
result.
|
|
34
|
+
} else if (arg === '--content-dir' && next) {
|
|
35
|
+
result.contentDir = next;
|
|
37
36
|
i++;
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -44,19 +43,13 @@ function parseArgs(args: string[]): CliArgs {
|
|
|
44
43
|
async function main(): Promise<void> {
|
|
45
44
|
const args = parseArgs(process.argv.slice(2));
|
|
46
45
|
|
|
47
|
-
if (args.
|
|
48
|
-
process.env.
|
|
46
|
+
if (args.contentDir) {
|
|
47
|
+
process.env.URBICON_CONTENT_DIR = args.contentDir;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// Pre-load cached data
|
|
50
|
+
// Pre-load cached data (recipes travel inside the catalog).
|
|
52
51
|
try {
|
|
53
|
-
await Promise.all([
|
|
54
|
-
loadCatalog(),
|
|
55
|
-
loadTemplateSections(),
|
|
56
|
-
loadRecipes(),
|
|
57
|
-
loadPrinciples(),
|
|
58
|
-
loadPatterns()
|
|
59
|
-
]);
|
|
52
|
+
await Promise.all([loadCatalog(), loadTemplateSections(), loadPrinciples(), loadPatterns()]);
|
|
60
53
|
} catch (err) {
|
|
61
54
|
console.error('Warning: Failed to pre-load some data:', err);
|
|
62
55
|
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { loadVerb } from '../data/verb-loader.js';
|
|
3
|
+
import { buildVerbPrompt, variantCount } from './design-prompts.js';
|
|
4
|
+
|
|
5
|
+
/** The full §8 verb table — every recipe must be present in the bundle. */
|
|
6
|
+
const VERB_NAMES = [
|
|
7
|
+
'onboard',
|
|
8
|
+
'adopt',
|
|
9
|
+
'compose',
|
|
10
|
+
'redesign',
|
|
11
|
+
'polish',
|
|
12
|
+
'critique',
|
|
13
|
+
'fix',
|
|
14
|
+
'retheme',
|
|
15
|
+
'audit',
|
|
16
|
+
'migrate'
|
|
17
|
+
];
|
|
3
18
|
|
|
4
19
|
describe('variantCount', () => {
|
|
5
20
|
it('defaults to 3 for missing or non-numeric input', () => {
|
|
@@ -13,39 +28,52 @@ describe('variantCount', () => {
|
|
|
13
28
|
});
|
|
14
29
|
});
|
|
15
30
|
|
|
16
|
-
describe('
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
describe('buildVerbPrompt', () => {
|
|
32
|
+
const body = '1. **Context.** Read the manifest.\n2. **Validate.** Run the linter.';
|
|
33
|
+
|
|
34
|
+
it('frames the verb and includes the recipe body', () => {
|
|
35
|
+
const p = buildVerbPrompt('compose', body, {});
|
|
36
|
+
expect(p).toContain('**compose** design recipe');
|
|
37
|
+
expect(p).toContain('Read the manifest');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('embeds the brief as a blockquote when provided', () => {
|
|
41
|
+
expect(buildVerbPrompt('compose', body, { brief: 'a billing page' })).toContain(
|
|
42
|
+
'> **a billing page**'
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('inlines provided code in a svelte fence', () => {
|
|
47
|
+
const p = buildVerbPrompt('redesign', body, { code: '<div>old</div>' });
|
|
48
|
+
expect(p).toContain('```svelte\n<div>old</div>\n```');
|
|
30
49
|
});
|
|
31
50
|
|
|
32
|
-
it('
|
|
33
|
-
expect(
|
|
51
|
+
it('appends a clamped variant instruction when variants are requested', () => {
|
|
52
|
+
expect(buildVerbPrompt('compose', body, { variants: '9' })).toContain('explore exactly 5');
|
|
53
|
+
expect(buildVerbPrompt('compose', body, { variants: '1' })).toContain('explore exactly 2');
|
|
34
54
|
});
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
|
|
56
|
+
it('degrades to a rebuild hint when the body is empty', () => {
|
|
57
|
+
expect(buildVerbPrompt('compose', '', {})).toContain('rebuild the design-content bundle');
|
|
37
58
|
});
|
|
38
59
|
});
|
|
39
60
|
|
|
40
|
-
describe('
|
|
41
|
-
it('
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
describe('loadVerb (against the bundled recipes)', () => {
|
|
62
|
+
it('loads every verb in the §8 table, non-empty', async () => {
|
|
63
|
+
for (const name of VERB_NAMES) {
|
|
64
|
+
const body = await loadVerb(name);
|
|
65
|
+
expect(body.length, name).toBeGreaterThan(0);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('a recipe opens by reading the manifest and references the real tool surface', async () => {
|
|
70
|
+
const compose = await loadVerb('compose');
|
|
71
|
+
expect(compose).toContain('manifest');
|
|
72
|
+
expect(compose).toContain('validate_design');
|
|
73
|
+
expect(compose).toContain('get_design_principles(as="rubric")');
|
|
47
74
|
});
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
|
|
76
|
+
it('returns the empty string for an unknown verb (read tolerant)', async () => {
|
|
77
|
+
expect(await loadVerb('does-not-exist')).toBe('');
|
|
50
78
|
});
|
|
51
79
|
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { loadVerb } from '../data/verb-loader.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* MCP prompts that ship the *process* — the
|
|
6
|
-
*
|
|
7
|
-
* client
|
|
8
|
-
*
|
|
9
|
-
* (get_design_context, get_pattern, validate_design, get_design_principles).
|
|
6
|
+
* MCP prompts that ship the *process* — the full design-verb table (DESIGN-MCP-V2
|
|
7
|
+
* §8). MCP prompts are the client-agnostic way to deliver a workflow: any MCP
|
|
8
|
+
* client (Claude Code, Cursor, …) can invoke them, and they orchestrate the
|
|
9
|
+
* server's read-only tools (get_pattern, validate_design, get_design_principles).
|
|
10
10
|
*
|
|
11
|
-
* The
|
|
12
|
-
* and
|
|
13
|
-
*
|
|
11
|
+
* The recipe BODY is the single source authored under `@urbicon-ui/design`'s
|
|
12
|
+
* `skill/verbs/*.md` and bundled into `@urbicon-ui/design-content` — the same text
|
|
13
|
+
* the local skill ships, so a verb is maintained once and served two ways (§9).
|
|
14
|
+
* Here we only wrap that body with the per-invocation header (brief / current code)
|
|
15
|
+
* and register it. Manifest state lives in the consumer's repo — read/written with
|
|
16
|
+
* the agent's own file tools or the `urbicon` CLI, never by this stateless server.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
/** Clamp the requested variant count to a sane range. Prompt args arrive as strings. */
|
|
@@ -20,108 +23,136 @@ export function variantCount(raw: string | undefined): number {
|
|
|
20
23
|
return Math.min(5, Math.max(2, n));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
return pattern
|
|
25
|
-
? `call \`get_pattern("${pattern}")\` and follow its layout, component-selection, and behavioural rules`
|
|
26
|
-
: 'if a composition pattern fits the brief (settings-page, dashboard, form-page, tab-navigation, onboarding-guide), call `get_pattern("<name>")` to load it';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const FOOTER =
|
|
30
|
-
'Output the final code, then a one-line rationale for each major design choice. Keep the rationale honest — name the trade-offs.';
|
|
31
|
-
|
|
32
|
-
export function designPagePrompt(
|
|
33
|
-
brief: string,
|
|
34
|
-
pattern: string | undefined,
|
|
35
|
-
variants: string | undefined
|
|
36
|
-
): string {
|
|
37
|
-
const n = variantCount(variants);
|
|
38
|
-
return `You are designing a new page for a project built on Urbicon UI:
|
|
39
|
-
|
|
40
|
-
> **${brief}**
|
|
26
|
+
type VerbArg = 'brief' | 'code' | 'variants';
|
|
41
27
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
5. **Judge.** Call \`get_design_principles(as="rubric")\` and score each surviving variant /40. Prefer a panel: judge correctness, hierarchy, paradigm-fidelity, and distinctiveness as separate lenses rather than one overall gut number.
|
|
49
|
-
6. **Synthesise.** Pick the winner, then graft the best ideas from the runners-up. Run \`validate_design\` once more on the merged result — it must come back clean.
|
|
50
|
-
7. **Record.** If the page follows a pattern, add \`data-design-pattern="<name>"\` to its root element and call \`sync_design_manifest\`. If you deviated from a pattern or principle on purpose, call \`record_design_decision\`.
|
|
51
|
-
|
|
52
|
-
${FOOTER}`;
|
|
28
|
+
interface VerbSpec {
|
|
29
|
+
name: string;
|
|
30
|
+
/** The prompt description shown to MCP clients. */
|
|
31
|
+
summary: string;
|
|
32
|
+
/** Which optional inputs this verb takes (drives the schema + the header). */
|
|
33
|
+
args: VerbArg[];
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The full verb table (§8). Names match the `skill/verbs/<name>.md` recipes one to
|
|
38
|
+
* one; `args` is the precise subset each verb uses (so `onboard` doesn't advertise
|
|
39
|
+
* a `code` field it ignores). Order = the router's narrow-to-broad reading order.
|
|
40
|
+
*/
|
|
41
|
+
const VERBS: VerbSpec[] = [
|
|
42
|
+
{
|
|
43
|
+
name: 'onboard',
|
|
44
|
+
summary:
|
|
45
|
+
'Greenfield start: interview the product intent (audience, voice, references) + intake (paradigm/theme/density), then seed design.manifest.md — the anchor every later verb reads.',
|
|
46
|
+
args: ['brief']
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'adopt',
|
|
50
|
+
summary:
|
|
51
|
+
'Brownfield start: infer the design language from existing code (tokens, patterns, intent), measure the drift, and seed design.manifest.md.',
|
|
52
|
+
args: ['brief']
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'compose',
|
|
56
|
+
summary:
|
|
57
|
+
'Design a new page/component with the full generate → validate → judge → synthesise loop (variant exploration + rubric + linter gate). Keeps generation off the generic mean.',
|
|
58
|
+
args: ['brief', 'variants']
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'redesign',
|
|
62
|
+
summary:
|
|
63
|
+
'Redesign an existing page: diagnose with the linter + rubric, then fix exactly the flagged weaknesses through variant exploration. Preserves behaviour and structure.',
|
|
64
|
+
args: ['brief', 'code', 'variants']
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'polish',
|
|
68
|
+
summary:
|
|
69
|
+
'Tighten a near-final page: small token-level fixes that raise the slop-floor score without restructuring.',
|
|
70
|
+
args: ['brief', 'code']
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'critique',
|
|
74
|
+
summary:
|
|
75
|
+
'Judge a page without changing it: correctness + slop-floor + rubric → a prioritised fix-list, each item tagged with the verb that repairs it.',
|
|
76
|
+
args: ['brief', 'code']
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'fix',
|
|
80
|
+
summary:
|
|
81
|
+
'Repair correctness defects (raw colours, dark:/focus:, hardcoded z-index, hallucinated tokens) — mechanical, behaviour-preserving.',
|
|
82
|
+
args: ['brief', 'code']
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'retheme',
|
|
86
|
+
summary:
|
|
87
|
+
'Rebrand the system: change the token layer once and propagate across every affected file via the manifest usage-index. Gated per file.',
|
|
88
|
+
args: ['brief']
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'audit',
|
|
92
|
+
summary:
|
|
93
|
+
'App-wide consistency sweep: validate the tree, check each pattern cohort, score a sample, and report drift over time. Recommends repairs, performs none.',
|
|
94
|
+
args: ['brief']
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'migrate',
|
|
98
|
+
summary:
|
|
99
|
+
'Roll out a pattern or library change across every site, file by file, gated per file.',
|
|
100
|
+
args: ['brief']
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const ARG_DESCRIPTIONS: Record<VerbArg, string> = {
|
|
105
|
+
brief:
|
|
106
|
+
'What to act on — the brief, the page, or the target. Optional; the agent uses the conversation context when omitted.',
|
|
107
|
+
code: 'The current page source. Optional — omit to have the agent read it first.',
|
|
108
|
+
variants: 'How many variants to explore (2–5, default 3).'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Per-invocation inputs (all optional); the recipe body carries the channel-agnostic steps. */
|
|
112
|
+
interface VerbArgs {
|
|
113
|
+
brief?: string;
|
|
114
|
+
code?: string;
|
|
115
|
+
variants?: string;
|
|
116
|
+
}
|
|
69
117
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
118
|
+
/** Wrap a recipe body with the per-invocation header (verb framing, brief, current code, variant count). */
|
|
119
|
+
export function buildVerbPrompt(name: string, body: string, args: VerbArgs): string {
|
|
120
|
+
const parts = [
|
|
121
|
+
`You are running the **${name}** design recipe for a project built on Urbicon UI. Follow it; do not skip steps — a single-shot answer regresses to a generic template.`
|
|
122
|
+
];
|
|
123
|
+
if (args.brief) parts.push(`\n> **${args.brief}**`);
|
|
124
|
+
if (args.code) parts.push(`\nCurrent implementation:\n\n\`\`\`svelte\n${args.code}\n\`\`\``);
|
|
125
|
+
parts.push('\n---\n');
|
|
126
|
+
parts.push(
|
|
127
|
+
body ||
|
|
128
|
+
'_Recipe text unavailable — rebuild the design-content bundle with `bun run docs:gen:all`._'
|
|
129
|
+
);
|
|
130
|
+
if (args.variants) {
|
|
131
|
+
parts.push(
|
|
132
|
+
`\n\nWhere the recipe says "a few variants", explore exactly ${variantCount(args.variants)}.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return parts.join('\n');
|
|
136
|
+
}
|
|
77
137
|
|
|
78
|
-
|
|
138
|
+
function schemaFor(args: VerbArg[]): Record<string, z.ZodString | z.ZodOptional<z.ZodString>> {
|
|
139
|
+
const shape: Record<string, z.ZodOptional<z.ZodString>> = {};
|
|
140
|
+
for (const arg of args) shape[arg] = z.string().optional().describe(ARG_DESCRIPTIONS[arg]);
|
|
141
|
+
return shape;
|
|
79
142
|
}
|
|
80
143
|
|
|
81
144
|
export function registerDesignPrompts(server: McpServer): void {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
({ brief, pattern, variants }) => ({
|
|
96
|
-
messages: [
|
|
97
|
-
{
|
|
98
|
-
role: 'user' as const,
|
|
99
|
-
content: { type: 'text' as const, text: designPagePrompt(brief, pattern, variants) }
|
|
100
|
-
}
|
|
101
|
-
]
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
server.prompt(
|
|
106
|
-
'redesign',
|
|
107
|
-
'Redesign an existing page: diagnose with validate_design + the rubric, then fix exactly the flagged weaknesses through variant exploration. Preserves behaviour and structure.',
|
|
108
|
-
{
|
|
109
|
-
brief: z
|
|
110
|
-
.string()
|
|
111
|
-
.describe('What to redesign and why, e.g. "the dashboard feels flat and generic".'),
|
|
112
|
-
code: z
|
|
113
|
-
.string()
|
|
114
|
-
.optional()
|
|
115
|
-
.describe('The current page source. Optional — omit to have the model read it first.'),
|
|
116
|
-
variants: z.string().optional().describe('How many variants to explore (2–5, default 3).')
|
|
117
|
-
},
|
|
118
|
-
({ brief, code, variants }) => ({
|
|
119
|
-
messages: [
|
|
120
|
-
{
|
|
121
|
-
role: 'user' as const,
|
|
122
|
-
content: { type: 'text' as const, text: redesignPrompt(brief, code, variants) }
|
|
123
|
-
}
|
|
124
|
-
]
|
|
125
|
-
})
|
|
126
|
-
);
|
|
145
|
+
for (const verb of VERBS) {
|
|
146
|
+
server.prompt(verb.name, verb.summary, schemaFor(verb.args), async (args: VerbArgs) => {
|
|
147
|
+
const body = await loadVerb(verb.name);
|
|
148
|
+
return {
|
|
149
|
+
messages: [
|
|
150
|
+
{
|
|
151
|
+
role: 'user' as const,
|
|
152
|
+
content: { type: 'text' as const, text: buildVerbPrompt(verb.name, body, args) }
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
127
158
|
}
|
package/src/server.test.ts
CHANGED
|
@@ -25,10 +25,7 @@ const EXPECTED_TOOLS = [
|
|
|
25
25
|
'find_icons',
|
|
26
26
|
'get_design_principles',
|
|
27
27
|
'get_pattern',
|
|
28
|
-
'validate_design'
|
|
29
|
-
'get_design_context',
|
|
30
|
-
'record_design_decision',
|
|
31
|
-
'sync_design_manifest'
|
|
28
|
+
'validate_design'
|
|
32
29
|
] as const;
|
|
33
30
|
|
|
34
31
|
describe('createServer', () => {
|
|
@@ -60,10 +57,22 @@ describe('createServer', () => {
|
|
|
60
57
|
expect(resourceCount).toBeGreaterThan(0);
|
|
61
58
|
});
|
|
62
59
|
|
|
63
|
-
it('registers the design-
|
|
60
|
+
it('registers the full design-verb table as prompts', () => {
|
|
64
61
|
const server = createServer() as unknown as McpServerInternals;
|
|
65
62
|
const promptNames = Object.keys(server._registeredPrompts);
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
for (const verb of [
|
|
64
|
+
'onboard',
|
|
65
|
+
'adopt',
|
|
66
|
+
'compose',
|
|
67
|
+
'redesign',
|
|
68
|
+
'polish',
|
|
69
|
+
'critique',
|
|
70
|
+
'fix',
|
|
71
|
+
'retheme',
|
|
72
|
+
'audit',
|
|
73
|
+
'migrate'
|
|
74
|
+
]) {
|
|
75
|
+
expect(promptNames, verb).toContain(verb);
|
|
76
|
+
}
|
|
68
77
|
});
|
|
69
78
|
});
|
package/src/server.ts
CHANGED
|
@@ -7,13 +7,10 @@ import { registerFindIconsTool } from './tools/find-icons.js';
|
|
|
7
7
|
import { registerGetChecklistTool } from './tools/get-checklist.js';
|
|
8
8
|
import { registerGetComponentTool } from './tools/get-component.js';
|
|
9
9
|
import { registerGetCssReferenceTool } from './tools/get-css-reference.js';
|
|
10
|
-
import { registerGetDesignContextTool } from './tools/get-design-context.js';
|
|
11
10
|
import { registerGetDesignPrinciplesTool } from './tools/get-design-principles.js';
|
|
12
11
|
import { registerGetPatternTool } from './tools/get-pattern.js';
|
|
13
12
|
import { registerGetRecipeTool } from './tools/get-recipe.js';
|
|
14
|
-
import { registerRecordDesignDecisionTool } from './tools/record-design-decision.js';
|
|
15
13
|
import { registerSuggestImplementationTool } from './tools/suggest-implementation.js';
|
|
16
|
-
import { registerSyncDesignManifestTool } from './tools/sync-design-manifest.js';
|
|
17
14
|
import { registerValidateDesignTool } from './tools/validate-design.js';
|
|
18
15
|
|
|
19
16
|
export function createServer(): McpServer {
|
|
@@ -26,7 +23,10 @@ export function createServer(): McpServer {
|
|
|
26
23
|
registerCatalogResource(server);
|
|
27
24
|
registerGuideResources(server);
|
|
28
25
|
|
|
29
|
-
// Tools
|
|
26
|
+
// Tools — all read-only. Manifest read/write (context · record-decision ·
|
|
27
|
+
// sync-manifest) lives in the consumer's repo via the `urbicon` CLI or the
|
|
28
|
+
// agent's own file tools, not on this stateless remote server.
|
|
29
|
+
// See docs/internal/DESIGN-MCP-V2.md.
|
|
30
30
|
registerFindComponentsTool(server);
|
|
31
31
|
registerGetComponentTool(server);
|
|
32
32
|
registerGetRecipeTool(server);
|
|
@@ -37,9 +37,6 @@ export function createServer(): McpServer {
|
|
|
37
37
|
registerGetDesignPrinciplesTool(server);
|
|
38
38
|
registerGetPatternTool(server);
|
|
39
39
|
registerValidateDesignTool(server);
|
|
40
|
-
registerGetDesignContextTool(server);
|
|
41
|
-
registerRecordDesignDecisionTool(server);
|
|
42
|
-
registerSyncDesignManifestTool(server);
|
|
43
40
|
|
|
44
41
|
// Prompts — the deliverable design process (Option E)
|
|
45
42
|
registerDesignPrompts(server);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { matchComponents } from '@urbicon-ui/design-engine/search';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { loadCatalog } from '../data/catalog-loader.js';
|
|
4
5
|
import { formatCompactCatalog } from '../utils/format-catalog.js';
|
|
5
|
-
import { matchComponents } from '../utils/search.js';
|
|
6
6
|
|
|
7
7
|
export function registerFindComponentsTool(server: McpServer): void {
|
|
8
8
|
server.tool(
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { renderRubric } from '@urbicon-ui/design-engine/rubric';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import {
|
|
4
5
|
extractPrincipleSection,
|
|
5
6
|
loadPrinciples,
|
|
6
7
|
PRINCIPLE_TOPICS
|
|
7
8
|
} from '../data/design-system-loader.js';
|
|
8
|
-
import { renderRubric } from '../design-rubric/rubric.js';
|
|
9
9
|
|
|
10
10
|
export function registerGetDesignPrinciplesTool(server: McpServer): void {
|
|
11
11
|
server.tool(
|
package/src/tools/get-recipe.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { loadCatalog } from '../data/catalog-loader.js';
|
|
4
4
|
|
|
5
5
|
export function registerGetRecipeTool(server: McpServer): void {
|
|
6
6
|
server.tool(
|
|
@@ -15,11 +15,13 @@ export function registerGetRecipeTool(server: McpServer): void {
|
|
|
15
15
|
},
|
|
16
16
|
{ readOnlyHint: true },
|
|
17
17
|
async ({ scenario }) => {
|
|
18
|
-
|
|
18
|
+
// Recipes (with code + pattern) travel in the catalog — single source of truth,
|
|
19
|
+
// no separate read of the recipe source tree.
|
|
20
|
+
const catalog = await loadCatalog();
|
|
21
|
+
const recipe = catalog.recipes.find((r) => r.id === scenario);
|
|
19
22
|
|
|
20
23
|
if (!recipe) {
|
|
21
|
-
const
|
|
22
|
-
const available = allRecipes.map((r) => r.id).join(', ');
|
|
24
|
+
const available = catalog.recipes.map((r) => r.id).join(', ');
|
|
23
25
|
return {
|
|
24
26
|
content: [
|
|
25
27
|
{
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { matchComponents } from '@urbicon-ui/design-engine/search';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import type { ComponentCatalogEntry } from '../data/catalog-loader.js';
|
|
4
5
|
import { loadCatalog } from '../data/catalog-loader.js';
|
|
5
|
-
import { loadRecipes } from '../data/recipe-loader.js';
|
|
6
|
-
import { matchComponents } from '../utils/search.js';
|
|
7
6
|
|
|
8
7
|
/** Default props and skeleton hints per component type */
|
|
9
8
|
const SKELETON_HINTS: Record<string, { attrs: string; children?: string; selfClosing?: boolean }> =
|
|
@@ -104,7 +103,7 @@ export function registerSuggestImplementationTool(server: McpServer): void {
|
|
|
104
103
|
{ readOnlyHint: true, openWorldHint: true },
|
|
105
104
|
async ({ description, components: requestedComponents, style }) => {
|
|
106
105
|
const catalog = await loadCatalog();
|
|
107
|
-
const recipes =
|
|
106
|
+
const recipes = catalog.recipes;
|
|
108
107
|
|
|
109
108
|
let matched: ComponentCatalogEntry[];
|
|
110
109
|
|