@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 +3 -3
- package/package.json +3 -3
- package/src/prompts/design-prompts.ts +2 -2
- package/src/server.ts +2 -1
- package/src/tools/get-css-reference.ts +1 -1
- package/src/tools/validate-design.ts +5 -5
- package/src/transports/http.ts +64 -71
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 **
|
|
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
|
|
115
|
-
| `critique` | `brief?`, `code?` | Judge without changing: correctness +
|
|
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.
|
|
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.
|
|
43
|
-
"@urbicon-ui/design-engine": "6.
|
|
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
|
|
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 +
|
|
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
|
|
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
|
|
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: '🔵
|
|
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 ·
|
|
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); **
|
|
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
|
|
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) **
|
|
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()
|
package/src/transports/http.ts
CHANGED
|
@@ -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`,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
}
|