@urbicon-ui/mcp-server 6.44.0 → 6.45.0

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 CHANGED
@@ -86,7 +86,7 @@ All tools are read-only (`readOnlyHint: true`) — this server never touches the
86
86
  | `find_icons` | Browse the 315-icon catalog by keyword, category, or name. |
87
87
  | `get_design_principles` | Design heuristics (Layer 5): visual hierarchy, interaction, component selection, layout, accessibility, theming (paradigms, change decision tree). Call first when generating UI. `as="rubric"` returns the 8-criterion 1–5 scoring rubric for judging a generated UI. |
88
88
  | `get_pattern` | Composition patterns (Layer 4) for page archetypes — settings-page, dashboard, form-page, tab-navigation, onboarding-guide. |
89
- | `validate_design` | Lint generated markup on two axes — **correctness** (raw colours, `dark:`/`focus:` misuse, hardcoded z-index, broken dynamic classes, hallucinated tokens, foreign-library component APIs, unlabelled icon buttons; the blocking gate) and the **slop-floor** (20 system-agnostic "looks generic" heuristics: generic fonts, animated dimensions, grey-on-colour, touch targets, …; advisory). Returns a correctness score + a slop-floor score and per-finding fixes for a generate → validate → fix loop. |
89
+ | `validate_design` | Lint generated markup on two axes — **correctness** (raw colours, `dark:`/`focus:` misuse, hardcoded z-index, broken dynamic classes, hallucinated tokens, foreign-library component APIs, unlabelled icon buttons; the blocking gate) and the **craft** axis (20 system-agnostic "looks generic" heuristics: generic fonts, animated dimensions, grey-on-colour, touch targets, …; advisory). Returns a correctness score + a craft score and per-finding fixes for a generate → validate → fix loop. |
90
90
 
91
91
  ## Resources
92
92
 
@@ -111,8 +111,8 @@ The full design-verb table (DESIGN-MCP-V2 §8) — client-agnostic workflows you
111
111
  | `adopt` | `brief?` | Brownfield: infer the design language from code, measure drift, seed the manifest. |
112
112
  | `compose` | `brief?`, `variants?` | New page via generate → validate → judge → synthesise (variants + rubric + linter gate). |
113
113
  | `redesign` | `brief?`, `code?`, `variants?` | Diagnose with linter + rubric, fix exactly the flagged weaknesses, preserve behaviour. |
114
- | `polish` | `brief?`, `code?` | Small token-level fixes that raise the slop-floor score without restructuring. |
115
- | `critique` | `brief?`, `code?` | Judge without changing: correctness + slop + rubric → a prioritised, verb-tagged fix-list. |
114
+ | `polish` | `brief?`, `code?` | Small token-level fixes that raise the craft score without restructuring. |
115
+ | `critique` | `brief?`, `code?` | Judge without changing: correctness + craft + rubric → a prioritised, verb-tagged fix-list. |
116
116
  | `fix` | `brief?`, `code?` | Repair correctness defects (raw colours, `dark:`/`focus:`, z-index, hallucinated tokens). |
117
117
  | `retheme` | `brief?` | Rebrand: change the token layer once, propagate across every affected file. |
118
118
  | `audit` | `brief?` | App-wide sweep: validate the tree, check pattern cohorts, report drift over time. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/mcp-server",
3
- "version": "6.44.0",
3
+ "version": "6.45.0",
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": {
@@ -39,8 +39,8 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
- "@urbicon-ui/design-content": "6.44.0",
43
- "@urbicon-ui/design-engine": "6.44.0",
42
+ "@urbicon-ui/design-content": "6.45.0",
43
+ "@urbicon-ui/design-engine": "6.45.0",
44
44
  "zod": "^4.3.6"
45
45
  },
46
46
  "devDependencies": {
@@ -66,13 +66,13 @@ const VERBS: VerbSpec[] = [
66
66
  {
67
67
  name: 'polish',
68
68
  summary:
69
- 'Tighten a near-final page: small token-level fixes that raise the slop-floor score without restructuring.',
69
+ 'Tighten a near-final page: small token-level fixes that raise the craft score without restructuring.',
70
70
  args: ['brief', 'code']
71
71
  },
72
72
  {
73
73
  name: 'critique',
74
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.',
75
+ 'Judge a page without changing it: correctness + craft + rubric → a prioritised fix-list, each item tagged with the verb that repairs it.',
76
76
  args: ['brief', 'code']
77
77
  },
78
78
  {
package/src/server.ts CHANGED
@@ -18,7 +18,8 @@ import { registerValidateDesignTool } from './tools/validate-design.js';
18
18
  * read-only tools, and the design-verb prompts. Deliberately stateless — it
19
19
  * never reads or writes a consumer's design manifest (that lives in the
20
20
  * consumer repo, via the `urbicon` CLI or the agent's own file tools), so a
21
- * fresh instance can be created per HTTP session. Called by both transports.
21
+ * fresh instance can be built per HTTP *request* (~0.2 ms; the catalog behind
22
+ * it is cached module-globally). Called by both transports.
22
23
  *
23
24
  * @returns A ready-to-connect `McpServer`; the caller attaches a transport
24
25
  * (`startStdioTransport` / `startHttpTransport`).
@@ -24,7 +24,7 @@ export function registerGetCssReferenceTool(server: McpServer): void {
24
24
  .enum(CSS_REFERENCE_SECTION_NAMES)
25
25
  .optional()
26
26
  .describe(
27
- 'Token category. Omit for overview with naming conventions and dark mode mechanism.'
27
+ 'Token category — the z-index scale is part of `shadows`. Omit for overview with naming conventions and dark mode mechanism.'
28
28
  )
29
29
  },
30
30
  { readOnlyHint: true },
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  const SEVERITY_LABEL: Record<Severity, string> = {
7
7
  error: '🔴 Errors',
8
8
  warning: '🟠 Warnings',
9
- info: '🔵 Slop-floor'
9
+ info: '🔵 Craft'
10
10
  };
11
11
 
12
12
  function renderFindings(findings: Finding[], severity: Severity): string {
@@ -43,13 +43,13 @@ function renderReport(report: LintReport): string {
43
43
 
44
44
  let md = `# Design Validation — ${verdict}\n\n`;
45
45
  if (filename) md += `> \`${filename}\`\n\n`;
46
- md += `**Correctness ${scores.correctness}/100 · Slop-floor ${scores.slop}/100** · ${counts.error} error(s), ${counts.warning} warning(s), ${counts.info} slop note(s)\n\n`;
46
+ md += `**Correctness ${scores.correctness}/100 · Craft ${scores.craft}/100** · ${counts.error} error(s), ${counts.warning} warning(s), ${counts.info} craft note(s)\n\n`;
47
47
  md +=
48
- 'Two axes, never mixed: **correctness** is the blocking gate (fix every error/warning to pass); **slop-floor** is advisory — system-agnostic "looks generic" signals to raise distinctiveness.\n\n';
48
+ 'Two axes, never mixed: **correctness** is the blocking gate (fix every error/warning to pass); **craft** is advisory — system-agnostic "looks generic" signals to raise distinctiveness.\n\n';
49
49
 
50
50
  if (findings.length === 0) {
51
51
  md +=
52
- 'No issues found. Tokens are valid, no `dark:`/`focus:`/hardcoded z-index, and the slop-floor heuristics are satisfied.\n';
52
+ 'No issues found. Tokens are valid, no `dark:`/`focus:`/hardcoded z-index, and the craft heuristics are satisfied.\n';
53
53
  md += renderSuppressed(report);
54
54
  return md;
55
55
  }
@@ -81,7 +81,7 @@ function renderReport(report: LintReport): string {
81
81
  export function registerValidateDesignTool(server: McpServer): void {
82
82
  server.tool(
83
83
  'validate_design',
84
- 'Lint generated Svelte/HTML markup against the Urbicon UI design rules. Two axes, never mixed: (1) **correctness** — deterministic defects (raw Tailwind colours, `dark:`/`focus:` misuse, hardcoded z-index, broken dynamic classes, hallucinated tokens, foreign-library component APIs like `tone=`/`variant="outline"`, icon-only buttons with no accessible name), the blocking gate; (2) **slop-floor** — system-agnostic "looks generic" heuristics (generic fonts, animated width/height, magic-number sizes, low-contrast text on colour, inline styles, `!important`, placeholder copy, emoji-as-icon, heading-level skips, small touch targets, intent-colour rainbow, uniform spacing/weights, identical Cards), advisory. Returns a correctness score and a slop-floor score (both 0–100; correctness −10/error, −5/warning; slop −10 per signal; floored) plus per-finding fixes. Run in a generate → validate → fix loop after producing UI code. Pass `extraTokens` to whitelist semantic tokens your project defines on top of Urbicon’s so they are not flagged as hallucinated. Class rules scan class attributes, slotClasses and tv()/script literals — prose that merely *quotes* an anti-pattern (docs, before/after examples) is not flagged. A deliberately off-system surface can exempt specific rules in-file via `<!-- urbicon-ignore rule-id … — reason -->`; suppressions are always reported, never silent.',
84
+ 'Lint generated Svelte/HTML markup against the Urbicon UI design rules. Two axes, never mixed: (1) **correctness** — deterministic defects (raw Tailwind colours, `dark:`/`focus:` misuse, hardcoded z-index, broken dynamic classes, hallucinated tokens, foreign-library component APIs like `tone=`/`variant="outline"`, icon-only buttons with no accessible name), the blocking gate; (2) **craft** — system-agnostic "looks generic" heuristics (generic fonts, animated width/height, magic-number sizes, low-contrast text on colour, inline styles, `!important`, placeholder copy, emoji-as-icon, heading-level skips, small touch targets, intent-colour rainbow, uniform spacing/weights, identical Cards), advisory. Returns a correctness score and a craft score (both 0–100; correctness −10/error, −5/warning; craft −10 per signal; floored) plus per-finding fixes. Run in a generate → validate → fix loop after producing UI code. Pass `extraTokens` to whitelist semantic tokens your project defines on top of Urbicon’s so they are not flagged as hallucinated. Class rules scan class attributes, slotClasses and tv()/script literals — prose that merely *quotes* an anti-pattern (docs, before/after examples) is not flagged. A deliberately off-system surface can exempt specific rules in-file via `<!-- urbicon-ignore rule-id … — reason -->`; suppressions are always reported, never silent.',
85
85
  {
86
86
  code: z
87
87
  .string()
@@ -1,24 +1,36 @@
1
- import { createServer as createHttpServer } from 'node:http';
1
+ import { createServer as createHttpServer, type ServerResponse } from 'node:http';
2
2
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
3
  import { createServer as createMcpServer } from '../server.js';
4
4
 
5
+ function respondError(res: ServerResponse, status: number, code: number, message: string): void {
6
+ res.writeHead(status, { 'Content-Type': 'application/json' });
7
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code, message }, id: null }));
8
+ }
9
+
5
10
  /**
6
- * Serve the MCP over Streamable HTTP on `/mcp`, multiplexing clients by the
7
- * `mcp-session-id` header. Each session gets its own server + transport pair:
8
- * - `POST` **without** a session id opens one (fresh {@link createServer}, a
9
- * generated id) and remains registered until the transport closes;
10
- * - `POST`/`GET`/`DELETE` **with** a known id are routed to that session;
11
- * - a `POST` carrying an **unknown** id, or a session-less `GET`/`DELETE`, is a
12
- * `400` the stateless server never resurrects a session it did not open.
11
+ * Serve the MCP over Streamable HTTP on `/mcp`, **session-less**: every request
12
+ * is self-contained, so each `POST` gets a throw-away {@link createServer} +
13
+ * transport pair that is closed again when the response ends.
14
+ *
15
+ * The SDK's session mode (`sessionIdGenerator: () => randomUUID()`) is
16
+ * deliberately **not** used. It would keep a server instance per session in a
17
+ * map that only a client's explicit `DELETE` — or a clean transport close
18
+ * ever empties, so every client that just goes away (crash, kill, dropped
19
+ * connection) strands ~0.4 MB of heap forever; on an unauthenticated endpoint a
20
+ * single anonymous `POST` is enough to do it. That leak reached ~1 GB in
21
+ * production. Sessions bought nothing here: the server sends no notifications
22
+ * and holds no per-client state, and rebuilding it per request costs ~0.2 ms.
13
23
  *
14
- * Any non-`/mcp` path returns a plain-text banner (a lightweight liveness ping).
15
- * Runs until the process exits; there is no returned stop handle.
24
+ * Consequently `GET` (the standalone SSE stream) and `DELETE` (session
25
+ * teardown) answer `405` both explicitly permitted by the MCP spec for a
26
+ * server that offers no sessions. Any non-`/mcp` path returns a plain-text
27
+ * banner (a lightweight liveness ping).
16
28
  *
17
29
  * @param port - TCP port to listen on (binds `http://localhost:<port>/mcp`).
30
+ * @returns A stop handle; `index.ts` ignores it and runs until the process
31
+ * exits, the transport test uses it to shut the listener down.
18
32
  */
19
- export async function startHttpTransport(port: number): Promise<void> {
20
- const sessions = new Map<string, StreamableHTTPServerTransport>();
21
-
33
+ export async function startHttpTransport(port: number): Promise<{ close: () => Promise<void> }> {
22
34
  const httpServer = createHttpServer(async (req, res) => {
23
35
  const url = new URL(req.url || '/', `http://localhost:${port}`);
24
36
 
@@ -28,69 +40,50 @@ export async function startHttpTransport(port: number): Promise<void> {
28
40
  return;
29
41
  }
30
42
 
31
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
43
+ if (req.method !== 'POST') {
44
+ respondError(
45
+ res,
46
+ 405,
47
+ -32600,
48
+ 'Method not allowed — this server is session-less; every request must be a self-contained POST'
49
+ );
50
+ return;
51
+ }
32
52
 
33
- if (req.method === 'POST') {
34
- const session = sessionId ? sessions.get(sessionId) : undefined;
35
- if (session) {
36
- await session.handleRequest(req, res);
37
- } else if (!sessionId) {
38
- const transport = new StreamableHTTPServerTransport({
39
- sessionIdGenerator: () => crypto.randomUUID()
40
- });
41
- transport.onclose = () => {
42
- if (transport.sessionId) sessions.delete(transport.sessionId);
43
- };
44
- const server = createMcpServer();
45
- await server.connect(transport);
46
- await transport.handleRequest(req, res);
47
- if (transport.sessionId) sessions.set(transport.sessionId, transport);
48
- } else {
49
- res.writeHead(400, { 'Content-Type': 'application/json' });
50
- res.end(
51
- JSON.stringify({
52
- jsonrpc: '2.0',
53
- error: { code: -32600, message: 'Invalid session' },
54
- id: null
55
- })
56
- );
57
- }
58
- } else if (req.method === 'GET') {
59
- const session = sessionId ? sessions.get(sessionId) : undefined;
60
- if (session) {
61
- await session.handleRequest(req, res);
62
- } else {
63
- res.writeHead(400, { 'Content-Type': 'application/json' });
64
- res.end(
65
- JSON.stringify({
66
- jsonrpc: '2.0',
67
- error: { code: -32600, message: 'Missing or invalid session' },
68
- id: null
69
- })
70
- );
71
- }
72
- } else if (req.method === 'DELETE') {
73
- const session = sessionId ? sessions.get(sessionId) : undefined;
74
- if (session && sessionId) {
75
- await session.handleRequest(req, res);
76
- sessions.delete(sessionId);
77
- } else {
78
- res.writeHead(204);
53
+ const server = createMcpServer();
54
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
55
+
56
+ // Tear down when the response ends — whether it completed or the client
57
+ // hung up. This is the only lifetime the pair has; nothing outlives it.
58
+ res.on('close', () => {
59
+ void transport.close();
60
+ void server.close();
61
+ });
62
+
63
+ try {
64
+ await server.connect(transport);
65
+ await transport.handleRequest(req, res);
66
+ } catch (err) {
67
+ console.error('MCP request failed:', err);
68
+ if (res.headersSent) {
79
69
  res.end();
70
+ } else {
71
+ respondError(res, 500, -32603, 'Internal server error');
80
72
  }
81
- } else {
82
- res.writeHead(405, { 'Content-Type': 'application/json' });
83
- res.end(
84
- JSON.stringify({
85
- jsonrpc: '2.0',
86
- error: { code: -32600, message: 'Method not allowed' },
87
- id: null
88
- })
89
- );
90
73
  }
91
74
  });
92
75
 
93
- httpServer.listen(port, () => {
94
- console.error(`Urbicon UI MCP Server listening on http://localhost:${port}/mcp`);
76
+ await new Promise<void>((resolve) => {
77
+ httpServer.listen(port, () => {
78
+ console.error(`Urbicon UI MCP Server listening on http://localhost:${port}/mcp`);
79
+ resolve();
80
+ });
95
81
  });
82
+
83
+ return {
84
+ close: () =>
85
+ new Promise<void>((resolve, reject) => {
86
+ httpServer.close((err) => (err ? reject(err) : resolve()));
87
+ })
88
+ };
96
89
  }