@neuctra/ui-mcp 0.4.7 → 0.4.8
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/data/components.json +40 -40
- package/data/design-rules.json +19 -4
- package/data/layouts.json +53 -16
- package/data/variants.json +1113 -0
- package/package.json +3 -1
- package/src/load-data.mjs +2 -1
- package/src/tools/neuctra-ui-page-builder/index.mjs +11 -1
- package/src/tools/neuctra-ui-page-builder/variants.mjs +102 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neuctra/ui-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "MCP server exposing the @neuctra/ui component registry (props, defaults, examples, theme tokens) so AI coding tools generate accurate Neuctra UI code, plus SEO/AEO/GEO tools (per-page meta, woven JSON-LD, audits).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"./data/theme.json": "./data/theme.json",
|
|
33
33
|
"./data/design-rules.json": "./data/design-rules.json",
|
|
34
34
|
"./data/seo-guide.json": "./data/seo-guide.json",
|
|
35
|
+
"./data/layouts.json": "./data/layouts.json",
|
|
36
|
+
"./data/variants.json": "./data/variants.json",
|
|
35
37
|
"./package.json": "./package.json"
|
|
36
38
|
},
|
|
37
39
|
"files": [
|
package/src/load-data.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
5
|
// Disk loading is for Node, Bun, and Deno. Edge callers should pass imported
|
|
6
|
-
// data to createServer({ registry, theme, aiDesignRules, seoGuide, layouts }).
|
|
6
|
+
// data to createServer({ registry, theme, aiDesignRules, seoGuide, layouts, variants }).
|
|
7
7
|
export function loadDataFromDisk() {
|
|
8
8
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const dataDirectory = path.resolve(directory, "..", "data");
|
|
@@ -16,5 +16,6 @@ export function loadDataFromDisk() {
|
|
|
16
16
|
aiDesignRules: readJson("design-rules.json"),
|
|
17
17
|
seoGuide: readJson("seo-guide.json"),
|
|
18
18
|
layouts: readJson("layouts.json"),
|
|
19
|
+
variants: readJson("variants.json"),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -3,6 +3,7 @@ import { FRAMEWORKS } from "../../seo.mjs";
|
|
|
3
3
|
import { findNeuctraUiComponents } from "../get-neuctra-ui-component/index.mjs";
|
|
4
4
|
import { getDesignRulesContext } from "../get-design-rules/index.mjs";
|
|
5
5
|
import { buildSeoGuideContext } from "../neuctra-ui-seo-setup/index.mjs";
|
|
6
|
+
import { selectDesignVariants } from "./variants.mjs";
|
|
6
7
|
|
|
7
8
|
function getLayouts(layoutData) {
|
|
8
9
|
const grouped = (layoutData?.layoutGroups ?? []).flatMap((group) =>
|
|
@@ -89,7 +90,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
89
90
|
{
|
|
90
91
|
title: "Analyze and plan a Neuctra UI page",
|
|
91
92
|
description:
|
|
92
|
-
"Analyze a page request and choose from every layout in the grouped real-world catalog in data/layouts.json, including marketing, SaaS dashboards, commerce, publishing, support, portfolios, communities, events, and system pages. Use pageType=auto to select the closest specific layout from the request, or choose an explicit pageType. Returns the selected recipe, matching @neuctra/ui component specs, relevant design rules, and SEO guidance.",
|
|
93
|
+
"Analyze a page request and choose from every layout in the grouped real-world catalog in data/layouts.json, including marketing, SaaS dashboards, commerce, publishing, support, portfolios, communities, events, and system pages. Use pageType=auto to select the closest specific layout from the request, or choose an explicit pageType. Returns the selected recipe, context-matched design variants (navigation shell, page header, stat cards, dashboard grid), matching @neuctra/ui component specs, relevant design rules, and SEO guidance.",
|
|
93
94
|
inputSchema: {
|
|
94
95
|
request: z.string().min(8).describe("Describe the page to create and the main user task."),
|
|
95
96
|
pageType: z.enum(pageTypeOptions).default("auto").describe(`Layout ID from the full catalog (${layouts.map((layout) => layout.id).join(", ")}) or auto to infer the best match from the request.`),
|
|
@@ -100,6 +101,12 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
100
101
|
primaryAction: z.string().optional(),
|
|
101
102
|
platform: z.enum(["desktop", "mobile", "responsive"]).default("responsive"),
|
|
102
103
|
audience: z.string().optional(),
|
|
104
|
+
variants: z.object({
|
|
105
|
+
navigationShell: z.string(),
|
|
106
|
+
pageHeader: z.string(),
|
|
107
|
+
statCard: z.string(),
|
|
108
|
+
dashboardBody: z.string(),
|
|
109
|
+
}).partial().optional().describe("Force a specific variant id per group (see designVariants in the response for ids). Use this to keep an existing app's shell consistent."),
|
|
103
110
|
}).default({}).describe("Optional design and audience preferences."),
|
|
104
111
|
},
|
|
105
112
|
},
|
|
@@ -116,6 +123,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
116
123
|
ruleIds: layout.seoRuleIds,
|
|
117
124
|
});
|
|
118
125
|
const signals = detectTaskSignals(request);
|
|
126
|
+
const designVariants = selectDesignVariants(data.variants, { request, layoutId: layout.id, preferences });
|
|
119
127
|
const preferencesToClarify = [];
|
|
120
128
|
if (!preferences.visualDirection) preferencesToClarify.push("visual direction");
|
|
121
129
|
if (!preferences.density) preferencesToClarify.push("content density");
|
|
@@ -146,6 +154,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
146
154
|
matches: components,
|
|
147
155
|
instruction: "Use these exact exports and documented props. If a required component is absent from the matches, call get-neuctra-ui-component for that need before inventing a pattern.",
|
|
148
156
|
},
|
|
157
|
+
...(designVariants && { designVariants }),
|
|
149
158
|
designRules: design,
|
|
150
159
|
seoGuidance: seo ? {
|
|
151
160
|
framework: seo.framework,
|
|
@@ -156,6 +165,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
156
165
|
implementationInstructions: [
|
|
157
166
|
"Use the selected layout as a starting structure and adapt its sections to the exact request.",
|
|
158
167
|
"Keep the primary task visually clear; do not add sections that do not serve the request.",
|
|
168
|
+
...(designVariants ? ["Build the navigation shell, page header, stat cards and dashboard grid from designVariants.selections. Use the recommended variant unless an alternative clearly fits the brief better; if the project already has an app shell, keep that instead."] : []),
|
|
159
169
|
"Use Neuctra UI components with the exact props and semantic theme tokens from the supplied context.",
|
|
160
170
|
"Apply the SEO steps only when the page is public and indexable; protect private application pages.",
|
|
161
171
|
"When framework is auto, inspect the project files and choose metadata and rendering APIs for the installed framework and router.",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Scores the design variants in data/variants.json against a page request so
|
|
2
|
+
// the page builder recommends a context-appropriate navigation shell, header,
|
|
3
|
+
// stat card and dashboard grid instead of the same default every time.
|
|
4
|
+
|
|
5
|
+
const normalize = (text) => ` ${text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
|
|
6
|
+
|
|
7
|
+
// Small deterministic tie-breaker (0..1) so equally-scored variants rotate
|
|
8
|
+
// with the request text instead of always resolving to the first entry.
|
|
9
|
+
function tieBreak(seed) {
|
|
10
|
+
let hash = 2166136261;
|
|
11
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
12
|
+
hash ^= seed.charCodeAt(i);
|
|
13
|
+
hash = Math.imul(hash, 16777619);
|
|
14
|
+
}
|
|
15
|
+
return ((hash >>> 0) % 1000) / 1000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolvePlatform(request, platform) {
|
|
19
|
+
if (platform !== "responsive") return platform;
|
|
20
|
+
return /\bmobile[ -]?(app|first|only)\b|\bpwa\b/i.test(request) ? "mobile" : platform;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scoreVariant(variant, { text, layoutId, density, platform, seed }) {
|
|
24
|
+
let score = 0;
|
|
25
|
+
const reasons = [];
|
|
26
|
+
for (const signal of variant.signals ?? []) {
|
|
27
|
+
const phrase = normalize(signal);
|
|
28
|
+
if (phrase.trim() && text.includes(phrase)) {
|
|
29
|
+
score += phrase.trim().includes(" ") ? 4 : 3;
|
|
30
|
+
reasons.push(`request mentions "${signal}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (variant.layouts?.includes(layoutId)) {
|
|
34
|
+
score += 3;
|
|
35
|
+
reasons.push(`suits the ${layoutId} layout`);
|
|
36
|
+
}
|
|
37
|
+
if (density) {
|
|
38
|
+
if (variant.density?.includes(density)) {
|
|
39
|
+
score += 2;
|
|
40
|
+
reasons.push(`fits ${density} density`);
|
|
41
|
+
} else {
|
|
42
|
+
score -= 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const platforms = variant.platforms ?? [];
|
|
46
|
+
if (platform === "mobile") {
|
|
47
|
+
if (platforms.includes("mobile")) {
|
|
48
|
+
score += 4;
|
|
49
|
+
reasons.push("works on mobile-first products");
|
|
50
|
+
} else {
|
|
51
|
+
score -= 3;
|
|
52
|
+
}
|
|
53
|
+
} else if (!platforms.includes(platform) && !platforms.includes("responsive")) {
|
|
54
|
+
score -= 10;
|
|
55
|
+
}
|
|
56
|
+
return { score: score + tieBreak(`${seed}:${variant.id}`), reasons };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const summarize = ({ id, name, bestFor, structure }) => ({ id, name, bestFor, structure });
|
|
60
|
+
|
|
61
|
+
export function selectDesignVariants(variantData, { request, layoutId, preferences = {} }) {
|
|
62
|
+
const groups = variantData?.groups ?? {};
|
|
63
|
+
const text = normalize(request);
|
|
64
|
+
const platform = resolvePlatform(request, preferences.platform ?? "responsive");
|
|
65
|
+
const overrides = preferences.variants ?? {};
|
|
66
|
+
const selections = {};
|
|
67
|
+
|
|
68
|
+
for (const [groupId, group] of Object.entries(groups)) {
|
|
69
|
+
if (!group.appliesTo?.includes(layoutId)) continue;
|
|
70
|
+
const ranked = (group.variants ?? [])
|
|
71
|
+
.map((variant) => ({
|
|
72
|
+
variant,
|
|
73
|
+
...scoreVariant(variant, { text, layoutId, density: preferences.density, platform, seed: `${request}:${groupId}` }),
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => b.score - a.score);
|
|
76
|
+
|
|
77
|
+
const override = overrides[groupId] && ranked.find(({ variant }) => variant.id === overrides[groupId]);
|
|
78
|
+
const chosen = override ?? ranked[0];
|
|
79
|
+
if (!chosen) continue;
|
|
80
|
+
|
|
81
|
+
selections[groupId] = {
|
|
82
|
+
title: group.title,
|
|
83
|
+
recommended: {
|
|
84
|
+
...chosen.variant,
|
|
85
|
+
why: override
|
|
86
|
+
? ["explicitly requested in preferences.variants"]
|
|
87
|
+
: chosen.reasons.length
|
|
88
|
+
? chosen.reasons
|
|
89
|
+
: ["no strong context signal; chosen to avoid repeating the default, confirm it fits the brief"],
|
|
90
|
+
},
|
|
91
|
+
alternatives: ranked.filter((entry) => entry !== chosen).slice(0, 2).map(({ variant }) => summarize(variant)),
|
|
92
|
+
otherOptions: ranked.filter((entry) => entry !== chosen).slice(2).map(({ variant }) => variant.id),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Object.keys(selections).length) return null;
|
|
97
|
+
return {
|
|
98
|
+
principles: variantData.principles ?? [],
|
|
99
|
+
effectivePlatform: platform,
|
|
100
|
+
selections,
|
|
101
|
+
};
|
|
102
|
+
}
|