@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.
Files changed (48) hide show
  1. package/README.md +44 -41
  2. package/package.json +3 -1
  3. package/src/data/catalog-loader.test.ts +1 -1
  4. package/src/data/catalog-loader.ts +12 -37
  5. package/src/data/component-loader.ts +5 -40
  6. package/src/data/design-system-loader.test.ts +1 -1
  7. package/src/data/design-system-loader.ts +1 -1
  8. package/src/data/icon-loader.test.ts +25 -80
  9. package/src/data/icon-loader.ts +8 -68
  10. package/src/data/template-loader.ts +1 -1
  11. package/src/data/verb-loader.ts +29 -0
  12. package/src/eval/eval.test.ts +16 -9
  13. package/src/eval/score.ts +26 -10
  14. package/src/index.ts +7 -14
  15. package/src/prompts/design-prompts.test.ts +56 -28
  16. package/src/prompts/design-prompts.ts +135 -104
  17. package/src/server.test.ts +16 -7
  18. package/src/server.ts +4 -7
  19. package/src/tools/find-components.ts +1 -1
  20. package/src/tools/get-design-principles.ts +1 -1
  21. package/src/tools/get-recipe.ts +6 -4
  22. package/src/tools/suggest-implementation.ts +2 -3
  23. package/src/tools/validate-design.ts +17 -9
  24. package/src/data/recipe-loader.test.ts +0 -49
  25. package/src/data/recipe-loader.ts +0 -131
  26. package/src/design-linter/heuristics.ts +0 -162
  27. package/src/design-linter/index.ts +0 -14
  28. package/src/design-linter/linter.test.ts +0 -257
  29. package/src/design-linter/linter.ts +0 -62
  30. package/src/design-linter/rules.ts +0 -348
  31. package/src/design-linter/tokens.test.ts +0 -80
  32. package/src/design-linter/tokens.ts +0 -203
  33. package/src/design-linter/types.ts +0 -66
  34. package/src/design-manifest/index.ts +0 -20
  35. package/src/design-manifest/manifest.test.ts +0 -175
  36. package/src/design-manifest/manifest.ts +0 -250
  37. package/src/design-manifest/scan.test.ts +0 -51
  38. package/src/design-manifest/scan.ts +0 -74
  39. package/src/design-manifest/types.ts +0 -40
  40. package/src/design-rubric/rubric.test.ts +0 -43
  41. package/src/design-rubric/rubric.ts +0 -140
  42. package/src/tools/get-design-context.ts +0 -43
  43. package/src/tools/record-design-decision.ts +0 -99
  44. package/src/tools/sync-design-manifest.ts +0 -92
  45. package/src/utils/paths.test.ts +0 -101
  46. package/src/utils/paths.ts +0 -78
  47. package/src/utils/search.test.ts +0 -141
  48. 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 '../design-linter/index.js';
11
- import { MAX_RUBRIC_SCORE, RUBRIC_CRITERIA } from '../design-rubric/rubric.js';
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
- /** 0–100 deterministic design-linter score. */
15
- score: number;
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
- score: r.score,
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} linter | ${treatment} linter | ${baseline} rubric | ${treatment} rubric |\n`;
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) baseLint.push(b.linter.score);
91
- if (t) treatLint.push(t.linter.score);
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.score ?? '—'} | ${t?.linter.score ?? '—'} | ${rub(b)} | ${rub(t)} |\n`;
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 linter score | ${mean(baseLint).toFixed(1)} | ${mean(treatLint).toFixed(1)} | ${pct(mean(baseLint), mean(treatLint))} |\n`;
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
- dataDir?: string;
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 === '--data-dir' && next) {
36
- result.dataDir = next;
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.dataDir) {
48
- process.env.DATA_DIR = args.dataDir;
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 { designPagePrompt, redesignPrompt, variantCount } from './design-prompts.js';
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('designPagePrompt', () => {
17
- it('embeds the brief and drives the full loop in order', () => {
18
- const p = designPagePrompt('a billing settings page', undefined, '4');
19
- expect(p).toContain('a billing settings page');
20
- for (const tool of [
21
- 'get_design_context',
22
- 'get_design_principles',
23
- 'validate_design',
24
- 'get_design_principles(as="rubric")',
25
- 'sync_design_manifest'
26
- ]) {
27
- expect(p, tool).toContain(tool);
28
- }
29
- expect(p).toContain('Generate 4 variants');
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('pins a specific pattern when given one', () => {
33
- expect(designPagePrompt('x', 'dashboard', undefined)).toContain('get_pattern("dashboard")');
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
- it('offers pattern discovery when none is given', () => {
36
- expect(designPagePrompt('x', undefined, undefined)).toContain('if a composition pattern fits');
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('redesignPrompt', () => {
41
- it('is diagnosis-first and preserves behaviour', () => {
42
- const p = redesignPrompt('the dashboard feels flat', undefined, undefined);
43
- expect(p).toContain('Diagnose');
44
- expect(p).toContain('validate_design');
45
- expect(p).toContain('two lowest-scoring criteria');
46
- expect(p).toContain('read the current implementation');
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
- it('inlines provided code', () => {
49
- expect(redesignPrompt('x', '<div>old</div>', undefined)).toContain('<div>old</div>');
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 generate validate → judge →
6
- * synthesise loop (docs/DESIGN-MCP.md, Option E). MCP prompts are the
7
- * client-agnostic way to deliver a workflow: any MCP client (Claude Code,
8
- * Cursor, …) can invoke them, and they orchestrate the server's own tools
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 creative loop itself runs in the consumer's harness (it needs file access
12
- * and iteration); these prompts encode the steps so a single-shot generation
13
- * doesn't regress to the mean.
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
- function patternStep(pattern: string | undefined): string {
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
- Run this loop. Do not skip steps — a single-shot answer regresses to a generic template.
43
-
44
- 1. **Context.** Call \`get_design_context\` and honour the project's paradigm, theme, density, and recorded decisions (ADRs). Then ${patternStep(pattern)}.
45
- 2. **Ground rules.** Call \`get_design_principles\` for the heuristics and \`get_css_reference\` for the exact token names. Note the paradigm's token profile via \`get_design_principles(topic="theming")\`.
46
- 3. **Generate ${n} variants.** Produce ${n} genuinely different implementations, each taking a distinct compositional approach *within* the paradigm vary density, hierarchy emphasis, and the one signature moment. Do not let them converge. Use only real semantic tokens (no \`bg-status-*\`, no invented names).
47
- 4. **Validate.** Run \`validate_design\` on every variant. Fix each error and warning. A variant that cannot pass is disqualified.
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
- export function redesignPrompt(
56
- brief: string,
57
- code: string | undefined,
58
- variants: string | undefined
59
- ): string {
60
- const n = variantCount(variants);
61
- const current = code
62
- ? `\n\nCurrent implementation:\n\n\`\`\`svelte\n${code}\n\`\`\``
63
- : '\n\nFirst read the current implementation of the page in question.';
64
- return `You are redesigning an existing page in a project built on Urbicon UI:
65
-
66
- > **${brief}**${current}
67
-
68
- Run a diagnosis-first loop:
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
- 1. **Context.** Call \`get_design_context\` to recover the project's paradigm, theme, and prior decisions.
71
- 2. **Diagnose.** Run \`validate_design\` on the current code, then call \`get_design_principles(as="rubric")\` and score the current page /40. Your revision targets are **every linter finding** plus the **two lowest-scoring criteria** — nothing else.
72
- 3. **Generate ${n} variants** that fix exactly those weaknesses. Preserve the page's behaviour, data flow, and overall structure; change only what the diagnosis flagged. Use only real tokens.
73
- 4. **Validate.** Run \`validate_design\` on each; fix every error and warning.
74
- 5. **Judge.** Re-score each variant with the rubric. A redesign that does not beat the original on its target criteria is not shippable.
75
- 6. **Synthesise.** Merge the best result, then run \`validate_design\` once more.
76
- 7. **Record.** Call \`record_design_decision\` for any deliberate deviation; \`sync_design_manifest\` if pattern usage changed.
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
- End with a before/after table of the targeted criteria (old score new score) and ${FOOTER.toLowerCase()}`;
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
- server.prompt(
83
- 'design-page',
84
- 'Design a new page with the full generate → validate → judge → synthesise loop (variant exploration + rubric selection + linter gate). Keeps generation from regressing to a generic template.',
85
- {
86
- brief: z.string().describe('What to build, e.g. "a billing settings page for a SaaS admin".'),
87
- pattern: z
88
- .string()
89
- .optional()
90
- .describe(
91
- 'Composition pattern to follow (settings-page, dashboard, form-page, …). Optional.'
92
- ),
93
- variants: z.string().optional().describe('How many variants to explore (2–5, default 3).')
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
  }
@@ -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-process prompts', () => {
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
- expect(promptNames).toContain('design-page');
67
- expect(promptNames).toContain('redesign');
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(
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { getRecipeById, loadRecipes } from '../data/recipe-loader.js';
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
- const recipe = await getRecipeById(scenario);
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 allRecipes = await loadRecipes();
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 = await loadRecipes();
106
+ const recipes = catalog.recipes;
108
107
 
109
108
  let matched: ComponentCatalogEntry[];
110
109