@neuctra/ui-mcp 0.4.6 → 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/README.md +1 -1
- package/data/components.json +40 -40
- package/data/design-rules.json +352 -0
- package/data/layouts.json +3286 -765
- package/data/seo-guide.json +367 -0
- package/data/variants.json +1113 -0
- package/package.json +5 -3
- package/src/load-data.mjs +4 -3
- package/src/tools/get-design-rules/index.mjs +46 -24
- package/src/tools/neuctra-ui-page-builder/index.mjs +35 -14
- package/src/tools/neuctra-ui-page-builder/variants.mjs +102 -0
- package/src/tools/neuctra-ui-seo-setup/index.mjs +59 -20
- package/data/aiDesignRules.json +0 -325
- package/data/seoGuide.json +0 -180
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",
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
".": "./src/server.mjs",
|
|
31
31
|
"./data/components.json": "./data/components.json",
|
|
32
32
|
"./data/theme.json": "./data/theme.json",
|
|
33
|
-
"./data/
|
|
34
|
-
"./data/
|
|
33
|
+
"./data/design-rules.json": "./data/design-rules.json",
|
|
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");
|
|
@@ -13,8 +13,9 @@ export function loadDataFromDisk() {
|
|
|
13
13
|
return {
|
|
14
14
|
registry: readJson("components.json"),
|
|
15
15
|
theme: readJson("theme.json"),
|
|
16
|
-
aiDesignRules: readJson("
|
|
17
|
-
seoGuide: readJson("
|
|
16
|
+
aiDesignRules: readJson("design-rules.json"),
|
|
17
|
+
seoGuide: readJson("seo-guide.json"),
|
|
18
18
|
layouts: readJson("layouts.json"),
|
|
19
|
+
variants: readJson("variants.json"),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const BLOCK_NAMES = [
|
|
4
|
+
"core",
|
|
5
|
+
"layout",
|
|
6
|
+
"components",
|
|
7
|
+
"accessibilityInteraction",
|
|
8
|
+
"modernDesign",
|
|
9
|
+
"antiAiDesignRules",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function makeProductDesignRules(aiDesignRules, blockNames, sectionNumbers) {
|
|
13
|
+
const selected = sectionNumbers ? new Set(sectionNumbers) : null;
|
|
14
|
+
return Object.fromEntries(
|
|
15
|
+
blockNames
|
|
16
|
+
.filter((name) => name !== "antiAiDesignRules")
|
|
17
|
+
.map((name) => [
|
|
18
|
+
name,
|
|
19
|
+
(aiDesignRules.blocks?.[name] ?? []).filter((section) => !selected || selected.has(section.number)),
|
|
20
|
+
])
|
|
21
|
+
.filter(([, sections]) => sections.length),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getDesignRulesContext(theme, aiDesignRules, sectionNumbers, requestedBlocks) {
|
|
4
26
|
const { rules, antiAiLookRules } = theme;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
27
|
+
const availableBlocks = aiDesignRules.blocks ?? {};
|
|
28
|
+
const requested = requestedBlocks?.length
|
|
29
|
+
? requestedBlocks
|
|
30
|
+
: sectionNumbers
|
|
31
|
+
? BLOCK_NAMES.filter((name) => name === "antiAiDesignRules" || (availableBlocks[name] ?? []).some((section) => sectionNumbers.includes(section.number)))
|
|
32
|
+
: ["core", "antiAiDesignRules"];
|
|
33
|
+
const blockNames = [...new Set(requested)];
|
|
8
34
|
|
|
9
|
-
const selected = new Set(sectionNumbers);
|
|
10
35
|
return {
|
|
11
36
|
stylingRules: rules,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
intro: aiDesignRules.intro,
|
|
16
|
-
sections: aiDesignRules.sections.filter((section) => selected.has(section.number)),
|
|
17
|
-
goldenRule: aiDesignRules.goldenRule,
|
|
37
|
+
antiAiDesignRules: {
|
|
38
|
+
...(availableBlocks.antiAiDesignRules?.length && { productDesign: availableBlocks.antiAiDesignRules }),
|
|
39
|
+
theme: antiAiLookRules,
|
|
18
40
|
},
|
|
41
|
+
productDesignRules: makeProductDesignRules(aiDesignRules, blockNames, sectionNumbers),
|
|
42
|
+
...(aiDesignRules.goldenRule && { goldenRule: aiDesignRules.goldenRule }),
|
|
19
43
|
};
|
|
20
44
|
}
|
|
21
45
|
|
|
@@ -25,18 +49,16 @@ export function registerGetDesignRulesTool(server, { theme, aiDesignRules }) {
|
|
|
25
49
|
{
|
|
26
50
|
title: "Get Neuctra UI design rules",
|
|
27
51
|
description:
|
|
28
|
-
"
|
|
29
|
-
inputSchema: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
content: [
|
|
34
|
-
{
|
|
35
|
-
type: "text",
|
|
36
|
-
text: JSON.stringify(getDesignRulesContext(theme, aiDesignRules), null, 2),
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
};
|
|
52
|
+
"Return structured Neuctra UI design guidance in focused blocks: core, layout, components, accessibilityInteraction, modernDesign, and antiAiDesignRules. By default returns core guidance and anti-AI rules; request additional blocks when needed. The page builder supplies only the sections relevant to its selected layout.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
blocks: z.array(z.enum(BLOCK_NAMES)).optional().describe("Named design rule blocks to return. Defaults to core and antiAiDesignRules."),
|
|
55
|
+
},
|
|
40
56
|
},
|
|
41
|
-
|
|
57
|
+
async ({ blocks }) => ({
|
|
58
|
+
content: [{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: JSON.stringify(getDesignRulesContext(theme, aiDesignRules, undefined, blocks), null, 2),
|
|
61
|
+
}],
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
42
64
|
}
|
|
@@ -3,36 +3,47 @@ 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";
|
|
7
|
+
|
|
8
|
+
function getLayouts(layoutData) {
|
|
9
|
+
const grouped = (layoutData?.layoutGroups ?? []).flatMap((group) =>
|
|
10
|
+
(group.layouts ?? []).map((layout) => ({ ...layout, category: layout.category ?? group.id, categoryName: group.name })),
|
|
11
|
+
);
|
|
12
|
+
const legacy = layoutData?.layouts ?? [];
|
|
13
|
+
return [...grouped, ...legacy];
|
|
14
|
+
}
|
|
6
15
|
|
|
7
16
|
function resolveLayout(request, requestedType, layoutData) {
|
|
8
|
-
const layouts = layoutData
|
|
17
|
+
const layouts = getLayouts(layoutData);
|
|
9
18
|
if (requestedType !== "auto") {
|
|
10
19
|
const explicit = layouts.find((layout) => layout.id === requestedType);
|
|
11
20
|
if (explicit) return { layout: explicit, confidence: 1, evidence: [`Explicit page type: ${requestedType}`] };
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
const normalized = request.toLowerCase();
|
|
23
|
+
const normalized = request.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
15
24
|
const scored = layouts
|
|
16
25
|
.filter((layout) => layout.id !== "custom")
|
|
17
26
|
.map((layout) => {
|
|
18
27
|
let score = 0;
|
|
19
28
|
const evidence = [];
|
|
20
29
|
for (const alias of layout.aliases ?? []) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
const phrase = alias.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
31
|
+
if (phrase && (` ${normalized} `).includes(` ${phrase} `)) {
|
|
32
|
+
const weight = 5 + Math.min(phrase.split(" ").length, 4) * 2;
|
|
33
|
+
score += weight;
|
|
34
|
+
evidence.push(`layout phrase: ${alias}`);
|
|
24
35
|
}
|
|
25
36
|
}
|
|
26
37
|
for (const keyword of layout.keywords ?? []) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
score +=
|
|
30
|
-
evidence.push(keyword);
|
|
38
|
+
const term = keyword.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
39
|
+
if (term && (` ${normalized} `).includes(` ${term} `)) {
|
|
40
|
+
score += 2;
|
|
41
|
+
evidence.push(`topic: ${keyword}`);
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
|
-
return { layout, score, evidence };
|
|
44
|
+
return { layout, score, evidence, specificity: Math.max(...(layout.aliases ?? []).map((alias) => alias.split(/\s+/).length), 0) };
|
|
34
45
|
})
|
|
35
|
-
.sort((a, b) => b.score - a.score);
|
|
46
|
+
.sort((a, b) => b.score - a.score || b.specificity - a.specificity);
|
|
36
47
|
|
|
37
48
|
const best = scored[0];
|
|
38
49
|
const custom = layouts.find((layout) => layout.id === "custom") ?? layouts[0];
|
|
@@ -71,7 +82,7 @@ function getComponentGuidance(registry, request, layout) {
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
74
|
-
const layouts = data.layouts
|
|
85
|
+
const layouts = getLayouts(data.layouts);
|
|
75
86
|
const pageTypeOptions = ["auto", ...layouts.map((layout) => layout.id)];
|
|
76
87
|
|
|
77
88
|
server.registerTool(
|
|
@@ -79,10 +90,10 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
79
90
|
{
|
|
80
91
|
title: "Analyze and plan a Neuctra UI page",
|
|
81
92
|
description:
|
|
82
|
-
"Analyze a page request
|
|
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.",
|
|
83
94
|
inputSchema: {
|
|
84
95
|
request: z.string().min(8).describe("Describe the page to create and the main user task."),
|
|
85
|
-
pageType: z.enum(pageTypeOptions).default("auto").describe("
|
|
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.`),
|
|
86
97
|
framework: z.enum(["auto", ...FRAMEWORKS, "remix"]).default("auto").describe("Framework and router for framework-specific SEO implementation guidance."),
|
|
87
98
|
preferences: z.object({
|
|
88
99
|
visualDirection: z.string().optional(),
|
|
@@ -90,6 +101,12 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
90
101
|
primaryAction: z.string().optional(),
|
|
91
102
|
platform: z.enum(["desktop", "mobile", "responsive"]).default("responsive"),
|
|
92
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."),
|
|
93
110
|
}).default({}).describe("Optional design and audience preferences."),
|
|
94
111
|
},
|
|
95
112
|
},
|
|
@@ -106,6 +123,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
106
123
|
ruleIds: layout.seoRuleIds,
|
|
107
124
|
});
|
|
108
125
|
const signals = detectTaskSignals(request);
|
|
126
|
+
const designVariants = selectDesignVariants(data.variants, { request, layoutId: layout.id, preferences });
|
|
109
127
|
const preferencesToClarify = [];
|
|
110
128
|
if (!preferences.visualDirection) preferencesToClarify.push("visual direction");
|
|
111
129
|
if (!preferences.density) preferencesToClarify.push("content density");
|
|
@@ -116,6 +134,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
116
134
|
request,
|
|
117
135
|
inferredPageType: layout.id,
|
|
118
136
|
layoutName: layout.name,
|
|
137
|
+
layoutCategory: layout.categoryName ?? layout.category ?? "general",
|
|
119
138
|
confidence,
|
|
120
139
|
evidence,
|
|
121
140
|
detectedTaskSignals: signals,
|
|
@@ -135,6 +154,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
135
154
|
matches: components,
|
|
136
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.",
|
|
137
156
|
},
|
|
157
|
+
...(designVariants && { designVariants }),
|
|
138
158
|
designRules: design,
|
|
139
159
|
seoGuidance: seo ? {
|
|
140
160
|
framework: seo.framework,
|
|
@@ -145,6 +165,7 @@ export function registerNeuctraUiPageBuilderTool(server, data) {
|
|
|
145
165
|
implementationInstructions: [
|
|
146
166
|
"Use the selected layout as a starting structure and adapt its sections to the exact request.",
|
|
147
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."] : []),
|
|
148
169
|
"Use Neuctra UI components with the exact props and semantic theme tokens from the supplied context.",
|
|
149
170
|
"Apply the SEO steps only when the page is public and indexable; protect private application pages.",
|
|
150
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
|
+
}
|
|
@@ -1,28 +1,67 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { FRAMEWORKS } from "../../seo.mjs";
|
|
3
3
|
|
|
4
|
+
const FRAMEWORK_OPTIONS = ["auto", ...FRAMEWORKS, "remix"];
|
|
5
|
+
|
|
6
|
+
const RULE_GROUPS = {
|
|
7
|
+
"on-page": ["on_page_checklist"],
|
|
8
|
+
technical: ["technical_seo"],
|
|
9
|
+
performance: ["on_page_checklist"],
|
|
10
|
+
frameworks: ["technical_seo"],
|
|
11
|
+
"launch-checklist": ["validation", "reporting"],
|
|
12
|
+
geo: ["geo_checklist", "off_page_seo"],
|
|
13
|
+
aeo: ["aeo_checklist"],
|
|
14
|
+
local: ["local_seo"],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function selectGuideSections(seoGuide, ruleIds) {
|
|
18
|
+
if (!ruleIds?.length) return {};
|
|
19
|
+
const keys = new Set(ruleIds.flatMap((id) => RULE_GROUPS[id] ?? []));
|
|
20
|
+
return Object.fromEntries([...keys].filter((key) => seoGuide[key] !== undefined).map((key) => [key, seoGuide[key]]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Builds context from the current SEO Agent Rules schema. */
|
|
4
24
|
export function buildSeoGuideContext(seoGuide, { framework = "auto", ruleIds } = {}) {
|
|
5
25
|
if (!seoGuide) return null;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
: seoGuide.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
:
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
|
|
27
|
+
const selected = ruleIds?.length ? selectGuideSections(seoGuide, ruleIds) : {
|
|
28
|
+
on_page_checklist: seoGuide.on_page_checklist,
|
|
29
|
+
technical_seo: seoGuide.technical_seo,
|
|
30
|
+
off_page_seo: seoGuide.off_page_seo,
|
|
31
|
+
local_seo: seoGuide.local_seo,
|
|
32
|
+
geo_checklist: seoGuide.geo_checklist,
|
|
33
|
+
aeo_checklist: seoGuide.aeo_checklist,
|
|
34
|
+
validation: seoGuide.validation,
|
|
35
|
+
reporting: seoGuide.reporting,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const checklists = [
|
|
39
|
+
...(selected.on_page_checklist ?? []),
|
|
40
|
+
...(selected.geo_checklist ?? []),
|
|
41
|
+
...(selected.aeo_checklist ?? []),
|
|
42
|
+
];
|
|
15
43
|
|
|
16
44
|
return {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
version: seoGuide.version,
|
|
46
|
+
name: seoGuide.name,
|
|
47
|
+
role: seoGuide.role,
|
|
48
|
+
workflow: seoGuide.workflow,
|
|
49
|
+
mode: seoGuide.mode,
|
|
21
50
|
framework: {
|
|
22
51
|
requested: framework,
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
detectedFromProjectContext: framework === "auto",
|
|
53
|
+
projectContextRequiredForDetection: framework === "auto",
|
|
54
|
+
frameworkSpecificImplementation: "Follow the detected framework's existing metadata, rendering, routing, and asset conventions. This guide does not prescribe framework-specific code snippets.",
|
|
25
55
|
},
|
|
56
|
+
...selected,
|
|
57
|
+
// Kept for the page builder's existing response contract.
|
|
58
|
+
seoRules: checklists,
|
|
59
|
+
implementationSteps: seoGuide.workflow,
|
|
60
|
+
safeImplementation: seoGuide.safe_implementation,
|
|
61
|
+
validation: selected.validation ?? seoGuide.validation,
|
|
62
|
+
reporting: selected.reporting ?? seoGuide.reporting,
|
|
63
|
+
references: seoGuide.references,
|
|
64
|
+
principle: seoGuide.principle,
|
|
26
65
|
};
|
|
27
66
|
}
|
|
28
67
|
|
|
@@ -30,18 +69,18 @@ export function registerNeuctraUiSeoSetupTool(server, { seoGuide }) {
|
|
|
30
69
|
server.registerTool(
|
|
31
70
|
"neuctra-ui-seo-setup",
|
|
32
71
|
{
|
|
33
|
-
title: "Neuctra UI SEO
|
|
72
|
+
title: "Neuctra UI SEO setup guide",
|
|
34
73
|
description:
|
|
35
|
-
"Return
|
|
74
|
+
"Return the current SEO Agent Rules for planning or carrying out an evidence-based SEO review in a Neuctra UI project. Covers technical and on-page SEO, GEO, AEO, local and off-page SEO, safe implementation, validation, and reporting. The default mode is read-only audit; implementation should follow the guide and existing project conventions. It does not guarantee rankings, indexing, rich results, or AI citations.",
|
|
36
75
|
inputSchema: {
|
|
37
76
|
framework: z
|
|
38
|
-
.enum(
|
|
77
|
+
.enum(FRAMEWORK_OPTIONS)
|
|
39
78
|
.default("auto")
|
|
40
|
-
.describe("Project framework.
|
|
79
|
+
.describe("Project framework. With auto, inspect projectContext and repository files before choosing framework-specific implementation details."),
|
|
41
80
|
projectContext: z
|
|
42
81
|
.string()
|
|
43
82
|
.optional()
|
|
44
|
-
.describe("Optional package.json/
|
|
83
|
+
.describe("Optional package.json, framework/router configuration, or brief project details. Used as evidence when framework is auto."),
|
|
45
84
|
},
|
|
46
85
|
},
|
|
47
86
|
async ({ framework, projectContext }) => {
|