@urbicon-ui/mcp-server 6.19.3 → 6.21.1

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.19.3",
3
+ "version": "6.21.1",
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,13 +32,13 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
- "@urbicon-ui/design-content": "6.19.3",
36
- "@urbicon-ui/design-engine": "6.19.3",
35
+ "@urbicon-ui/design-content": "6.21.1",
36
+ "@urbicon-ui/design-engine": "6.21.1",
37
37
  "zod": "^4.3.6"
38
38
  },
39
39
  "devDependencies": {
40
40
  "typescript": "^6.0.3",
41
- "@types/node": "^25.9.4",
41
+ "@types/node": "^25.9.5",
42
42
  "vitest": "^4.1.9"
43
43
  }
44
44
  }
@@ -88,8 +88,8 @@ function extractDescription(content: string): string {
88
88
  if (titleIdx === -1) return '';
89
89
 
90
90
  for (let i = titleIdx + 1; i < lines.length; i++) {
91
- const line = lines[i]!.trim();
92
- if (line === '') continue;
91
+ const line = lines[i]?.trim();
92
+ if (!line) continue;
93
93
  if (line.startsWith('#')) break;
94
94
  return line;
95
95
  }
@@ -115,7 +115,7 @@ export function extractPrincipleSection(content: string, topic: PrincipleTopic):
115
115
 
116
116
  let endIdx = lines.length;
117
117
  for (let i = startIdx + 1; i < lines.length; i++) {
118
- if (/^## /.test(lines[i]!)) {
118
+ if (/^## /.test(lines[i] ?? '')) {
119
119
  endIdx = i;
120
120
  break;
121
121
  }
@@ -36,8 +36,9 @@ export function registerFindIconsTool(server: McpServer): void {
36
36
  const byCategory = new Map<string, typeof icons>();
37
37
  for (const icon of icons) {
38
38
  for (const cat of icon.categories) {
39
- if (!byCategory.has(cat)) byCategory.set(cat, []);
40
- byCategory.get(cat)!.push(icon);
39
+ const bucket = byCategory.get(cat);
40
+ if (bucket) bucket.push(icon);
41
+ else byCategory.set(cat, [icon]);
41
42
  }
42
43
  }
43
44
 
@@ -16,11 +16,15 @@ import { OVERVIEW, SECTIONS } from './get-css-reference.js';
16
16
  * assert each is documented, so a newly added token can no longer disappear.
17
17
  *
18
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`.
19
+ * per token), plus a lighter check that every intent is at least *named* (its base
20
+ * `--color-<intent>` token or a `bg-<intent>` utility appears in the prose) — the
21
+ * F-B drift where a whole intent (`info`, a real ramp behind `bg-info` /
22
+ * `--color-feedback-info`) went undocumented while the six others were listed. It
23
+ * deliberately does NOT require every intent scale step (`primary-50 … primary-950`,
24
+ * documented via shorthand), feedback/interactive, chart, or internal-only token
25
+ * (e.g. `skeleton-shimmer`, used by the Skeleton wave, never a consumer utility) to
26
+ * be spelled out. Whole-set token validity is already guarded by design-engine's
27
+ * `tokens.test.ts`.
24
28
  */
25
29
 
26
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -55,6 +59,15 @@ function deriveSemanticCores(family: string): string[] {
55
59
  return [...cores].sort();
56
60
  }
57
61
 
62
+ /** Intent names from the `=== X INTENT ===` section markers in the CSS — robust to
63
+ * the multi-line `--color-neutral` definition a self-referential regex would miss. */
64
+ function deriveIntents(): string[] {
65
+ const css = readFileSync(semantic, 'utf-8');
66
+ const cores = new Set<string>();
67
+ for (const m of css.matchAll(/=== ([A-Z-]+) INTENT ===/g)) cores.add(m[1]!.toLowerCase());
68
+ return [...cores].sort();
69
+ }
70
+
58
71
  describe.skipIf(!cssAvailable)('get_css_reference token drift guard', () => {
59
72
  for (const { family } of TABLED_FAMILIES) {
60
73
  it(`documents every semantic \`${family}-*\` token defined in the CSS`, () => {
@@ -74,4 +87,14 @@ describe.skipIf(!cssAvailable)('get_css_reference token drift guard', () => {
74
87
  );
75
88
  });
76
89
  }
90
+
91
+ it('names every intent defined in the CSS (base token or utility)', () => {
92
+ const missing = deriveIntents().filter(
93
+ (c) => !ALL_CONTENT.includes(`--color-${c}`) && !ALL_CONTENT.includes(`bg-${c}`)
94
+ );
95
+ expect(
96
+ missing,
97
+ `Intents in the CSS but absent from get_css_reference: ${missing.join(', ')}`
98
+ ).toEqual([]);
99
+ });
77
100
  });
@@ -5,7 +5,7 @@ export const OVERVIEW = `# Urbicon UI — CSS Design Tokens
5
5
 
6
6
  ## Architecture
7
7
  Three CSS layers, imported in order:
8
- 1. \`foundation.css\` — Raw OKLCH color scales (neutral, primary, secondary, success, warning, danger)
8
+ 1. \`foundation.css\` — Raw OKLCH color scales (neutral, warm-neutral, primary, secondary, success, warning, danger, info)
9
9
  2. \`semantic.css\` — Purpose-based tokens that reference foundation (\`--color-surface-base\`, \`--color-text-primary\`, etc.)
10
10
  3. \`interaction.css\` — Animation timing, easing, shadows, focus rings
11
11
 
@@ -50,7 +50,7 @@ The \`@theme\` block sets the Tailwind utility value. The \`:root\` rule overrid
50
50
  → \`get_css_reference(section="surfaces")\` — 11 surface background tokens
51
51
  → \`get_css_reference(section="text")\` — 9 text color tokens
52
52
  → \`get_css_reference(section="borders")\` — 5 border color tokens
53
- → \`get_css_reference(section="intents")\` — 6 intent palettes (primary, success, danger, etc.)
53
+ → \`get_css_reference(section="intents")\` — 6 component intents + the \`info\` status colour, feedback + interactive tokens
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
56
56
  `;
@@ -137,7 +137,7 @@ Also available for intent-colored borders:
137
137
 
138
138
  const INTENTS = `# Intent Color System
139
139
 
140
- 6 intent palettes, each with 5 semantic variants + 11 foundation steps.
140
+ 6 component intents (primary, secondary, success, warning, danger, neutral) plus a status \`info\` colour — each a full palette with 5 semantic variants + 11 foundation steps.
141
141
 
142
142
  ## Semantic Intent Tokens (auto dark mode)
143
143
 
@@ -151,7 +151,9 @@ Each intent has these variants (example: \`primary\`):
151
151
  | \`--color-primary-subtle\` | \`bg-primary-subtle\` | Soft background | primary-50 | primary-900 |
152
152
  | \`--color-primary-emphasis\` | \`bg-primary-emphasis\` | Strong/dark variant | primary-900 | — |
153
153
 
154
- Same pattern applies to: \`success-*\`, \`warning-*\`, \`danger-*\`, \`secondary-*\`, \`neutral-*\`
154
+ Same pattern applies to: \`success-*\`, \`warning-*\`, \`danger-*\`, \`secondary-*\`, \`neutral-*\`.
155
+
156
+ \`info-*\` has the identical shape (\`--color-info\`, \`-hover\`, \`-active\`, \`-subtle\`, \`-emphasis\` → \`bg-info\`, \`text-info\`, \`bg-info-subtle\`, …) — the status/feedback blue (hue 220) behind Alert/Toast/Tooltip's info state, \`--color-feedback-info\`, and \`--color-chart-5\`. It is NOT in the global \`ComponentIntent\` union above: the feedback components with a built-in info state (Alert, Toast, Tooltip) do accept \`intent="info"\`, but generic components (Button, Badge, …) take the six-value union, so on those reach for the \`bg-info\`/\`text-info\` utilities rather than \`intent="info"\`.
155
157
 
156
158
  ## Foundation Intent Scales
157
159
 
@@ -163,16 +165,21 @@ Each intent has 11 numbered steps (50–950) for granular control:
163
165
  --color-danger-50 through --color-danger-950
164
166
  --color-secondary-50 through --color-secondary-950
165
167
  --color-neutral-50 through --color-neutral-950
168
+ --color-info-50 through --color-info-950
166
169
  \`\`\`
167
170
 
171
+ Two ramps carry stops beyond the standard 50–950:
172
+ - \`neutral\` adds finer steps — \`--color-neutral-0\` (pure white), \`-25\`, \`-650\`, \`-750\`, \`-850\` — so \`bg-neutral-650\` and friends are real tokens (they drive the surface/border light↔dark mappings), not hallucinations.
173
+ - \`--color-warm-neutral-50\` through \`-950\` is a separate warm-tinted greyscale ramp; the \`neutral\` intent borrows its lightness profile (see the theming section) rather than exposing it as a surface.
174
+
168
175
  Tailwind usage: \`bg-primary-500\`, \`text-danger-700\`, \`border-success-300\`, etc.
169
176
 
170
177
  ## Feedback Tokens (for status messages)
171
178
 
172
179
  | CSS Variable | Tailwind Utility | Maps to |
173
180
  |---|---|---|
174
- | \`--color-feedback-info\` | \`bg-feedback-info\` / \`text-feedback-info\` | primary-500 |
175
- | \`--color-feedback-info-subtle\` | \`bg-feedback-info-subtle\` | primary-50 |
181
+ | \`--color-feedback-info\` | \`bg-feedback-info\` / \`text-feedback-info\` | info-500 |
182
+ | \`--color-feedback-info-subtle\` | \`bg-feedback-info-subtle\` | info-50 |
176
183
  | \`--color-feedback-success\` | \`bg-feedback-success\` / \`text-feedback-success\` | success-500 |
177
184
  | \`--color-feedback-success-subtle\` | \`bg-feedback-success-subtle\` | success-50 |
178
185
  | \`--color-feedback-warning\` | \`bg-feedback-warning\` / \`text-feedback-warning\` | warning-500 |
@@ -229,8 +236,8 @@ Shadows automatically increase opacity in dark mode (0.05 → 0.2 for xs, etc.).
229
236
  | \`--z-hide\` | -1 | Hidden |
230
237
  | \`--z-base\` | 0 | Default |
231
238
  | \`--z-docked\` | 10 | Docked elements |
232
- | \`--z-dropdown\` | 9999 | Menus, listboxes, floating panels |
233
239
  | \`--z-sticky\` | 1100 | Sticky headers |
240
+ | \`--z-dropdown\` | 1150 | Menus, listboxes, floating panels (above sticky, below banner) |
234
241
  | \`--z-banner\` | 1200 | Banners |
235
242
  | \`--z-overlay\` | 1300 | Overlays |
236
243
  | \`--z-sidebar\` | 1350 | Sidebars |
@@ -338,7 +345,7 @@ For a temperature-free, true grayscale chassis (content-focused UIs), set chroma
338
345
 
339
346
  ## Intent-hue collisions
340
347
 
341
- If your accent hue lands near an intent hue, the two become hard to tell apart. The library intents sit at: success 140, warning 80, danger 25, info 220, secondary 280. A green brand (~140) collides with success; an amber brand (~80) collides with warning. When that happens, re-tune the colliding intent ramp away from the accent (push the hue ±15–25° and/or drop its lightness so it reads as "status", not "brand"). \`apps/docs/src/lib/style/editorial.css\` is a worked example (green brand → success pushed to 150 + darkened, warning pulled to amber 55).
348
+ If your accent hue lands near an intent hue, the two become hard to tell apart. The library intents sit at: success 140, warning 80, danger 25, info 220, secondary 280. A green brand (~140) collides with success; an amber brand (~80) collides with warning. When that happens, re-tune the colliding intent ramp away from the accent (push the hue ±15–25° and/or drop its lightness so it reads as "status", not "brand"). \`apps/docs/src/lib/style/rooms-docs.css\` is a worked example (green brand → success pushed to 150 + darkened, warning pulled to amber 55).
342
349
 
343
350
  ## Overriding Semantic Tokens
344
351
 
@@ -383,7 +390,7 @@ Fix: inside the scoped block, re-declare the derived tokens too, so substitution
383
390
  /* ...and the same for any neutral-derived surface/text/border tokens you rely on */
384
391
  }
385
392
  \`\`\`
386
- A global \`@theme\` block (the built-in themes, the Theme Builder output) does NOT hit this — everything lands on \`:root\`, the same element where the derived tokens compute, so re-declaration is unnecessary. Prefer global themes unless you genuinely need multiple themes live on one page. \`apps/docs/src/lib/style/editorial.css\` is the canonical scoped example.
393
+ A global \`@theme\` block (the built-in themes, the Theme Builder output) does NOT hit this — everything lands on \`:root\`, the same element where the derived tokens compute, so re-declaration is unnecessary. Prefer global themes unless you genuinely need multiple themes live on one page. \`apps/docs/src/lib/style/rooms-docs.css\` is the canonical scoped example.
387
394
 
388
395
  ## Component-Level Overrides
389
396
 
@@ -81,7 +81,7 @@ const IMPLEMENTATION_RULES = `## Implementation Rules
81
81
  - **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"\`.
82
82
  - **Color = meaning** — Neutral surfaces dominate (80–90%). Use \`intent\` colors ONLY for semantic meaning (status, severity, actions) — never as decoration.
83
83
  - **Spacing = hierarchy** — Tight (\`gap-2\`/\`gap-3\`) within related items. Generous (\`gap-8\`/\`gap-10\`) between sections. Don't use uniform spacing everywhere.
84
- - **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.
84
+ - **Commit to a radius** — Pick one radius philosophy and apply it consistently — cleanest via the semantic tier tokens (\`--radius-commit\`/\`-modify\`/\`-contain\`) or \`BlocksProvider\` defaults, so a whole component family moves together. Don't round a single component out of step with its peers, and don't just leave the bland defaults untouched.
85
85
  - **Data-driven styling** — Different states/severities should look visually distinct (vary padding, font-weight, Badge variant, text color) — not just carry a different label.
86
86
  - **Don't copy recipe styling** — Recipes show ONE interpretation. Create YOUR visual identity with your own spacing rhythm, color distribution, and layout density.
87
87
  `;
@@ -17,8 +17,9 @@ export async function startHttpTransport(port: number): Promise<void> {
17
17
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
18
18
 
19
19
  if (req.method === 'POST') {
20
- if (sessionId && sessions.has(sessionId)) {
21
- await sessions.get(sessionId)!.handleRequest(req, res);
20
+ const session = sessionId ? sessions.get(sessionId) : undefined;
21
+ if (session) {
22
+ await session.handleRequest(req, res);
22
23
  } else if (!sessionId) {
23
24
  const transport = new StreamableHTTPServerTransport({
24
25
  sessionIdGenerator: () => crypto.randomUUID()
@@ -41,8 +42,9 @@ export async function startHttpTransport(port: number): Promise<void> {
41
42
  );
42
43
  }
43
44
  } else if (req.method === 'GET') {
44
- if (sessionId && sessions.has(sessionId)) {
45
- await sessions.get(sessionId)!.handleRequest(req, res);
45
+ const session = sessionId ? sessions.get(sessionId) : undefined;
46
+ if (session) {
47
+ await session.handleRequest(req, res);
46
48
  } else {
47
49
  res.writeHead(400, { 'Content-Type': 'application/json' });
48
50
  res.end(
@@ -54,8 +56,9 @@ export async function startHttpTransport(port: number): Promise<void> {
54
56
  );
55
57
  }
56
58
  } else if (req.method === 'DELETE') {
57
- if (sessionId && sessions.has(sessionId)) {
58
- await sessions.get(sessionId)!.handleRequest(req, res);
59
+ const session = sessionId ? sessions.get(sessionId) : undefined;
60
+ if (session && sessionId) {
61
+ await session.handleRequest(req, res);
59
62
  sessions.delete(sessionId);
60
63
  } else {
61
64
  res.writeHead(204);
@@ -46,8 +46,9 @@ export function formatCompactCatalog(
46
46
  for (const comp of filtered) {
47
47
  const primaryTag = comp.tags[0];
48
48
  if (primaryTag && TAG_LABELS[primaryTag]) {
49
- if (!grouped.has(primaryTag)) grouped.set(primaryTag, []);
50
- grouped.get(primaryTag)!.push(comp);
49
+ const bucket = grouped.get(primaryTag);
50
+ if (bucket) bucket.push(comp);
51
+ else grouped.set(primaryTag, [comp]);
51
52
  } else {
52
53
  ungrouped.push(comp);
53
54
  }