@urbicon-ui/mcp-server 6.2.0 → 6.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/mcp-server",
3
- "version": "6.2.0",
3
+ "version": "6.3.3",
4
4
  "description": "Model Context Protocol server exposing the Urbicon UI component catalog, recipes and design intelligence to LLM agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
- "@urbicon-ui/design-content": "6.2.0",
36
- "@urbicon-ui/design-engine": "6.2.0",
35
+ "@urbicon-ui/design-content": "6.3.3",
36
+ "@urbicon-ui/design-engine": "6.3.3",
37
37
  "zod": "^4.3.6"
38
38
  },
39
39
  "devDependencies": {
@@ -2,7 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { matchComponents } from '@urbicon-ui/design-engine/search';
3
3
  import { z } from 'zod';
4
4
  import { loadCatalog } from '../data/catalog-loader.js';
5
- import { formatCompactCatalog } from '../utils/format-catalog.js';
5
+ import { formatCompactCatalog, formatComponentLine } from '../utils/format-catalog.js';
6
6
 
7
7
  export function registerFindComponentsTool(server: McpServer): void {
8
8
  server.tool(
@@ -43,20 +43,11 @@ export function registerFindComponentsTool(server: McpServer): void {
43
43
  let md = `# Search Results for "${query}"\n\n`;
44
44
  md += `> ${results.length} matching components.\n\n`;
45
45
 
46
+ // Same line format as the browse view — including the origin-package tag for
47
+ // non-blocks components, so a match like `Table` (from @urbicon-ui/table) is
48
+ // never mistaken for a blocks export.
46
49
  for (const comp of results) {
47
- const variants = comp.variants
48
- .filter(
49
- (v) => !['true', 'false'].every((b) => v.values.includes(b) && v.values.length <= 2)
50
- )
51
- .map((v) => `${v.name}: ${v.values.join('/')}`)
52
- .join(' · ');
53
-
54
- md += `- **${comp.name}** — ${comp.description}`;
55
- if (variants) md += ` | ${variants}`;
56
- if (comp.relatedComponents.length > 0) {
57
- md += ` | Related: ${comp.relatedComponents.join(', ')}`;
58
- }
59
- md += '\n';
50
+ md += formatComponentLine(comp);
60
51
  }
61
52
 
62
53
  md += '\n> Use `get_component` with the component slug for full API docs.\n';
@@ -34,7 +34,7 @@ export function registerGetChecklistTool(server: McpServer): void {
34
34
  md +=
35
35
  '- [ ] Use OKLCH interaction tokens for hover/active states (`bg-surface-hover`, `bg-surface-active`)\n';
36
36
  md +=
37
- '- [ ] Dark mode is automatic via `prefers-color-scheme` — semantic tokens switch automatically. For manual control, use `ThemeSwitcher` or set `data-theme="dark"` on `<html>`. Do NOT add `dark:` overrides\n';
37
+ '- [ ] Dark mode is automatic via the CSS `light-dark()` function (`:root` sets `color-scheme: light dark`, following the OS `prefers-color-scheme`) — semantic tokens switch automatically. For manual control, use `ThemeSwitcher` or add a `.light`/`.dark` class to `<html>`. Do NOT add `dark:` overrides\n';
38
38
  md +=
39
39
  '- [ ] Use z-index tokens via CSS custom properties: `z-[var(--z-modal)]`, `z-[var(--z-dropdown)]`, etc.\n';
40
40
  md += '\n';
@@ -44,6 +44,8 @@ export function registerGetChecklistTool(server: McpServer): void {
44
44
  md += '- [ ] Use `slotClasses` for targeted style overrides on specific sub-elements\n';
45
45
  md +=
46
46
  '- [ ] For reusable variant sets, register named `preset`s via `BlocksProvider` (and select with the `preset` prop) — not external variant libraries; Urbicon UI ships its own zero-dependency variant engine\n';
47
+ md +=
48
+ '- [ ] To style only one variant/intent/state project-wide (e.g. just `variant="outlined"`), register prop-conditional `overrides` on `BlocksProvider` `defaults`/`presets` — what an unconditional `slotClasses` cannot express\n';
47
49
  md +=
48
50
  "- [ ] For conditional classes, pass an array to `class` (`class={['base', condition && 'extra']}`) — not concatenated conditional class strings\n";
49
51
  md += '- [ ] Use `class` prop for simple additions — merges with defaults automatically\n';
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { OVERVIEW, SECTIONS } from './get-css-reference.js';
6
+
7
+ /**
8
+ * Drift guard for the hand-maintained CSS token reference.
9
+ *
10
+ * `get_css_reference` inlines its token tables as TS strings — the MCP server
11
+ * ships standalone (no blocks CSS at runtime), the same constraint that keeps
12
+ * design-engine's `VALID_TOKEN_CORES` inline. The hazard of any hand-copied list
13
+ * is silent drift: `--color-border-hairline` was added to the CSS and went
14
+ * unmirrored here for a while. When the blocks CSS is present (i.e. running
15
+ * in-repo) we re-derive the semantic surface / text / border token cores and
16
+ * assert each is documented, so a newly added token can no longer disappear.
17
+ *
18
+ * Scope: the three families `get_css_reference` enumerates exhaustively (one row
19
+ * per token). It deliberately does NOT require every intent scale step
20
+ * (`primary-50 … primary-950`, documented via shorthand), feedback/interactive,
21
+ * chart, or internal-only token (e.g. `skeleton-shimmer`, used by the Skeleton
22
+ * wave, never a consumer utility) to be spelled out. Whole-set token validity is
23
+ * already guarded by design-engine's `tokens.test.ts`.
24
+ */
25
+
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const semantic = resolve(
28
+ __dirname,
29
+ '..',
30
+ '..',
31
+ '..',
32
+ 'blocks',
33
+ 'src',
34
+ 'lib',
35
+ 'style',
36
+ 'semantic.css'
37
+ );
38
+ const cssAvailable = existsSync(semantic);
39
+
40
+ const ALL_CONTENT = [OVERVIEW, ...Object.values(SECTIONS)].join('\n');
41
+
42
+ /** Families `get_css_reference` tables exhaustively, with the prose count to verify. */
43
+ const TABLED_FAMILIES = [
44
+ { family: 'surface', section: SECTIONS.surfaces! },
45
+ { family: 'text', section: SECTIONS.text! },
46
+ { family: 'border', section: SECTIONS.borders! }
47
+ ] as const;
48
+
49
+ /** Unique semantic `--color-<family>-*` cores in the CSS (scoped re-declarations collapse). */
50
+ function deriveSemanticCores(family: string): string[] {
51
+ const css = readFileSync(semantic, 'utf-8');
52
+ const re = new RegExp(`--color-(${family}-[a-z-]+)\\s*:`, 'g');
53
+ const cores = new Set<string>();
54
+ for (const m of css.matchAll(re)) cores.add(m[1]!);
55
+ return [...cores].sort();
56
+ }
57
+
58
+ describe.skipIf(!cssAvailable)('get_css_reference token drift guard', () => {
59
+ for (const { family } of TABLED_FAMILIES) {
60
+ it(`documents every semantic \`${family}-*\` token defined in the CSS`, () => {
61
+ const missing = deriveSemanticCores(family).filter((c) => !ALL_CONTENT.includes(c));
62
+ expect(
63
+ missing,
64
+ `Semantic ${family} tokens in the CSS but absent from get_css_reference: ${missing.join(', ')}`
65
+ ).toEqual([]);
66
+ });
67
+ }
68
+
69
+ for (const { family, section } of TABLED_FAMILIES) {
70
+ it(`states the correct ${family}-token count`, () => {
71
+ const stated = Number(section.match(/(\d+)\s+tokens for/)?.[1]);
72
+ expect(stated, 'prose count drifted from the real token count').toBe(
73
+ deriveSemanticCores(family).length
74
+ );
75
+ });
76
+ }
77
+ });
@@ -1,7 +1,7 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
 
4
- const OVERVIEW = `# Urbicon UI — CSS Design Tokens
4
+ export const OVERVIEW = `# Urbicon UI — CSS Design Tokens
5
5
 
6
6
  ## Architecture
7
7
  Three CSS layers, imported in order:
@@ -32,8 +32,8 @@ Override tokens in your app CSS using Tailwind's \`@theme\` block:
32
32
  \`\`\`
33
33
 
34
34
  ## Dark Mode
35
- Mechanism: \`@media (prefers-color-scheme: dark)\` on \`:root\` in \`semantic.css\`.
36
- Manual override: add \`.light\` or \`.dark\` class to \`<html>\` (via \`ThemeSwitcher\` component).
35
+ Mechanism: semantic tokens use the CSS \`light-dark()\` function; \`:root\` declares \`color-scheme: light dark\` so the browser resolves the matching branch automatically (following the OS \`prefers-color-scheme\`). No \`dark:\` overrides.
36
+ Manual override: add a \`.light\` or \`.dark\` class to \`<html>\` (via the \`ThemeSwitcher\` component) — the class only flips \`color-scheme\`, and \`light-dark()\` re-resolves on its own.
37
37
 
38
38
  To override a token for ALL modes (light, dark, and manual overrides):
39
39
  \`\`\`css
@@ -49,7 +49,7 @@ The \`@theme\` block sets the Tailwind utility value. The \`:root\` rule overrid
49
49
  ## Available Sections
50
50
  → \`get_css_reference(section="surfaces")\` — 11 surface background tokens
51
51
  → \`get_css_reference(section="text")\` — 9 text color tokens
52
- → \`get_css_reference(section="borders")\` — 4 border color tokens
52
+ → \`get_css_reference(section="borders")\` — 5 border color tokens
53
53
  → \`get_css_reference(section="intents")\` — 6 intent palettes (primary, success, danger, etc.)
54
54
  → \`get_css_reference(section="shadows")\` — 5 shadow tokens + z-index scale
55
55
  → \`get_css_reference(section="theming")\` — How to create custom themes, available presets
@@ -114,16 +114,18 @@ Light → Dark mapping:
114
114
 
115
115
  const BORDERS = `# Border Tokens
116
116
 
117
- 4 tokens for border colors. All auto-switch in dark mode.
117
+ 5 tokens for border colors. All auto-switch in dark mode.
118
118
 
119
119
  | CSS Variable | Tailwind Utility | Purpose |
120
120
  |---|---|---|
121
+ | \`--color-border-hairline\` | \`border-border-hairline\` | Faintest divider — translucent (alpha), not a neutral step |
121
122
  | \`--color-border-subtle\` | \`border-border-subtle\` | Gentle grouping |
122
123
  | \`--color-border-default\` | \`border-border-default\` | Standard borders |
123
124
  | \`--color-border-emphasis\` | \`border-border-emphasis\` | Emphasized borders |
124
125
  | \`--color-border-strong\` | \`border-border-strong\` | High-contrast borders |
125
126
 
126
127
  Light → Dark mapping:
128
+ - \`border-hairline\`: black 8% → white 6% (translucent, blends onto any surface)
127
129
  - \`border-subtle\`: neutral-200 → neutral-700
128
130
  - \`border-default\`: neutral-300 → neutral-600
129
131
  - \`border-emphasis\`: neutral-400 → neutral-500
@@ -385,13 +387,16 @@ A global \`@theme\` block (the built-in themes, the Theme Builder output) does N
385
387
 
386
388
  ## Component-Level Overrides
387
389
 
388
- Use \`BlocksProvider\` to change component defaults globally:
390
+ Use \`BlocksProvider\` to style components project-wide — unconditional \`defaults\`, named \`presets\` (opt-in via the \`preset\` prop), and prop-conditional \`overrides\`:
389
391
  \`\`\`svelte
390
392
  <BlocksProvider defaults={{
391
393
  Card: { slotClasses: { base: 'rounded-2xl shadow-lg' } },
392
- Button: { slotClasses: { base: 'rounded-full font-bold uppercase' } }
394
+ Button: { slotClasses: { base: 'rounded-full font-bold uppercase' } },
395
+ // prop-conditional: style ONLY the outlined variant — what an unconditional slotClasses cannot express
396
+ Badge: { overrides: [{ variant: 'outlined', class: { base: 'border' } }] }
393
397
  }}>
394
398
  \`\`\`
399
+ Cascade (conflict-resolved per Tailwind bucket, later wins): \`defaults.slotClasses → defaults.overrides → preset.slotClasses → preset.overrides → instance slotClasses → instance class\`.
395
400
 
396
401
  Or override per-instance:
397
402
  \`\`\`svelte
@@ -413,7 +418,7 @@ So the only requirement is to import \`index.css\` (your app owns the Tailwind i
413
418
  **Do NOT add manual \`@source\` directives, and do NOT import the \`foundation\`/\`semantic\`/\`interaction\` subfiles instead of \`index.css\`** — the subfiles omit the \`@source\` directives (and global classes), which is the usual cause of "responsive layouts break in production".
414
419
  `;
415
420
 
416
- const SECTIONS: Record<string, string> = {
421
+ export const SECTIONS: Record<string, string> = {
417
422
  surfaces: SURFACES,
418
423
  text: TEXT,
419
424
  borders: BORDERS,
@@ -10,7 +10,7 @@ export function registerGetRecipeTool(server: McpServer): void {
10
10
  scenario: z
11
11
  .string()
12
12
  .describe(
13
- 'Recipe id: login, settings, dashboard, pricing, profile-card, data-table, command-palette'
13
+ 'Recipe id — e.g. login, settings, dashboard, pricing, profile-card, onboarding-flow, wizard, notification-center, or an auth flow (auth-invitation-register, auth-passkey-login, auth-password-reset). Pass any unrecognised id to get the full, current list.'
14
14
  )
15
15
  },
16
16
  { readOnlyHint: true },
@@ -69,11 +69,11 @@ const IMPLEMENTATION_RULES = `## Implementation Rules
69
69
 
70
70
  - **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\`
71
71
  - **Semantic tokens only** — Use \`bg-surface-elevated\`, \`text-text-primary\`, \`border-border-default\` — never raw Tailwind colors
72
- - **Dark mode** — Automatic via \`prefers-color-scheme\`. Do NOT add \`dark:\` overrides
72
+ - **Dark mode** — Automatic via the CSS \`light-dark()\` function (\`:root\` sets \`color-scheme: light dark\`, following the OS \`prefers-color-scheme\`). For manual control use \`ThemeSwitcher\` (toggles a \`.light\`/\`.dark\` class). Do NOT add \`dark:\` overrides
73
73
  - **Focus** — Always \`focus-visible:\` (not \`focus:\`) for keyboard-only focus rings
74
74
  - **State binding** — \`bind:value\`, \`bind:checked\`, \`bind:open\` for two-way state; callback props (\`onValueChange\`) for side effects
75
75
  - **Custom content** — Use Svelte 5 snippets (\`{#snippet name()}...{/snippet}\`), not legacy slots
76
- - **Styling overrides** — \`class\` for simple additions, \`slotClasses\` for per-slot targeting, \`unstyled\` to strip all defaults
76
+ - **Styling overrides** — per instance: \`class\` for simple additions, \`slotClasses\` for per-slot targeting, \`unstyled\` to strip all defaults. Project-wide via \`BlocksProvider\`: \`defaults\` (every instance), named \`presets\`, and prop-conditional \`overrides\` (style only one variant, e.g. \`variant="outlined"\`)
77
77
  - **Mint** — Add \`mint="scale"\` or \`mint="ripple"\` sparingly on primary CTAs only
78
78
 
79
79
  ## Design Quality
@@ -69,7 +69,7 @@ export function formatCompactCatalog(
69
69
  md += '```\n';
70
70
  md += "Import: `import { Button, Card } from '@urbicon-ui/blocks';` (always from package root)\n";
71
71
  md +=
72
- 'Dark mode: automatic via `prefers-color-scheme` — semantic tokens switch automatically. For manual control, use `ThemeSwitcher` or `data-theme="dark"` on `<html>`.\n\n';
72
+ 'Dark mode: automatic via the CSS `light-dark()` function (`:root` sets `color-scheme: light dark`, following the OS `prefers-color-scheme`) — semantic tokens switch automatically. For manual control, use `ThemeSwitcher` or add a `.light`/`.dark` class to `<html>`.\n\n';
73
73
 
74
74
  for (const tag of TAG_ORDER) {
75
75
  const comps = grouped.get(tag);
@@ -110,7 +110,7 @@ function isBooleanVariant(values: string[]): boolean {
110
110
  );
111
111
  }
112
112
 
113
- function formatComponentLine(comp: ComponentCatalogEntry): string {
113
+ export function formatComponentLine(comp: ComponentCatalogEntry): string {
114
114
  const variants = comp.variants
115
115
  .filter((v) => !isBooleanVariant(v.values))
116
116
  .map((v) => `${v.name}: ${v.values.join('/')}`)