@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuctra/ui-mcp",
3
- "version": "0.4.6",
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/aiDesignRules.json": "./data/aiDesignRules.json",
34
- "./data/seoGuide.json": "./data/seoGuide.json",
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("aiDesignRules.json"),
17
- seoGuide: readJson("seoGuide.json"),
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
- export function getDesignRulesContext(theme, aiDesignRules, sectionNumbers) {
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
- if (!sectionNumbers) {
6
- return { stylingRules: rules, antiAiLookRules, productDesignGuide: aiDesignRules };
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
- antiAiLookRules,
13
- productDesignGuide: {
14
- title: aiDesignRules.title,
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
- "Get every design rule generated UI must follow when using @neuctra/ui. Covers two levels: (1) styling rules — token usage, surface/background conventions, component composition requirements like CardBody being compulsory whenever Card has body content, component-specific gotchas like Dropdown's trigger propagation, and the full set of rules for avoiding the visual tells that make UI look AI-generated (gradients, decorative shadows/blurs/glows, emoji-as-icons, etc.); (2) product/UX design rules — a numbered 63-section guide covering design philosophy, visual hierarchy, page/sidebar/navigation structure, per-component usage guidance (when to use Card, Modal vs Drawer, Table vs List, etc.), spacing/color/border/radius/shadow conventions, responsive design, accessibility, interaction design, a final UI quality checklist, the rule against overwriting a component's built-in design, paragraph text color (text-secondary for paragraphs, text-muted-foreground only for metadata/hints), and modern layout guidance: a fixed responsive type scale, readable measure, spacing rhythm, grid and hero patterns, focal point and section variety, a breakpoint checklist, imagery, iconography, motion timing and microcopy. Call this before generating any UI — not just neuctra-ui-theme-setup.",
29
- inputSchema: {},
30
- },
31
- async () => {
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?.layouts ?? [];
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
- if (normalized.includes(alias.toLowerCase())) {
22
- score += alias.includes(" ") ? 5 : 3;
23
- evidence.push(alias);
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 pattern = new RegExp(`\\b${keyword}\\b`, "i");
28
- if (pattern.test(normalized)) {
29
- score += 1;
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?.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, infer its page type (for example tool, blog, dashboard, form, ecommerce, or docs), select a matching editable recipe from data/layouts.json, then compose matching @neuctra/ui component specs, design rules, and framework-aware SEO rules. Use this as the page-planning entry point. It plans the page; the assistant implements it using the returned layout and exact component APIs. Layout recipes can be added or edited in data/layouts.json.",
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("Page type or auto to infer the closest layout from the request."),
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
- const selectedRules = ruleIds
7
- ? seoGuide.seoRules.filter((rule) => ruleIds.includes(rule.id))
8
- : seoGuide.seoRules;
9
- const knownFramework = ["nextjs-app", "nextjs-pages", "react-vite", "react-router", "remix"].includes(framework);
10
- const implementation = knownFramework
11
- ? seoGuide.implementationSteps.frameworkSpecific[framework]
12
- : framework === "auto"
13
- ? seoGuide.implementationSteps.frameworkSpecific
14
- : seoGuide.implementationSteps.frameworkSpecific.other;
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
- title: seoGuide.title,
18
- intro: seoGuide.intro,
19
- seoRules: selectedRules,
20
- implementationSteps: seoGuide.implementationSteps.steps,
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
- implementation,
24
- assistantGuidance: seoGuide.implementationSteps.assistantGuidance,
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 rules and implementation guide",
72
+ title: "Neuctra UI SEO setup guide",
34
73
  description:
35
- "Return one complete SEO, AEO, and GEO guide for a Neuctra UI project. The guide has two portions: comprehensive SEO rules and ordered implementation steps, including framework-specific metadata, rendering, JSON-LD, sitemap, and robots guidance. This is an assistant reference tool: inspect the project and apply the rules to its actual framework; it does not generate a single page snippet or audit input.",
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(["auto", ...FRAMEWORKS, "remix"])
77
+ .enum(FRAMEWORK_OPTIONS)
39
78
  .default("auto")
40
- .describe("Project framework. Use auto to inspect repository context and infer it; framework changes how metadata and rendering are implemented."),
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/framework config or brief project details to help select the right implementation steps."),
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 }) => {