@neuctra/ui-mcp 0.4.4 → 0.4.6
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 +106 -383
- package/data/layouts.json +790 -0
- package/data/seoGuide.json +100 -6
- package/data/theme.json +1 -1
- package/package.json +1 -1
- package/src/load-data.mjs +20 -0
- package/src/seo.mjs +3 -3
- package/src/server.mjs +19 -495
- package/src/tools/get-design-rules/index.mjs +42 -0
- package/src/tools/get-neuctra-ui-component/index.mjs +160 -0
- package/src/tools/neuctra-ui-page-builder/index.mjs +157 -0
- package/src/tools/neuctra-ui-seo-setup/index.mjs +62 -0
- package/src/tools/neuctra-ui-setup/index.mjs +114 -0
- package/src/tools/neuctra-ui-setup/register.mjs +28 -0
- package/src/tools/neuctra-ui-theme-setup/index.mjs +17 -0
package/src/server.mjs
CHANGED
|
@@ -1,500 +1,24 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./seo.mjs";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const registry = JSON.parse(
|
|
24
|
-
fs.readFileSync(path.join(DATA_DIR, "components.json"), "utf8"),
|
|
25
|
-
);
|
|
26
|
-
const theme = JSON.parse(
|
|
27
|
-
fs.readFileSync(path.join(DATA_DIR, "theme.json"), "utf8"),
|
|
28
|
-
);
|
|
29
|
-
const aiDesignRules = JSON.parse(
|
|
30
|
-
fs.readFileSync(path.join(DATA_DIR, "aiDesignRules.json"), "utf8"),
|
|
31
|
-
);
|
|
32
|
-
const seoGuide = JSON.parse(
|
|
33
|
-
fs.readFileSync(path.join(DATA_DIR, "seoGuide.json"), "utf8"),
|
|
34
|
-
);
|
|
35
|
-
return { registry, theme, aiDesignRules, seoGuide };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function summarize(component) {
|
|
39
|
-
return {
|
|
40
|
-
name: component.name,
|
|
41
|
-
category: component.category,
|
|
42
|
-
description: component.description,
|
|
43
|
-
propCount: component.props.length,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function matchesQuery(component, query) {
|
|
48
|
-
const q = query.toLowerCase();
|
|
49
|
-
if (component.name.toLowerCase().includes(q)) return true;
|
|
50
|
-
if (component.category.toLowerCase().includes(q)) return true;
|
|
51
|
-
if (component.description.toLowerCase().includes(q)) return true;
|
|
52
|
-
return component.props.some((p) => p.name.toLowerCase().includes(q));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const UI_CATEGORIES = [
|
|
56
|
-
"dashboard", "landing-page", "form", "settings", "authentication",
|
|
57
|
-
"data-table", "ecommerce", "documentation", "custom",
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const CATEGORY_RULES = {
|
|
61
|
-
dashboard: [3, 4, 5, 7, 22, 23, 24, 27, 31, 35, 55, 57, 59],
|
|
62
|
-
"landing-page": [3, 7, 13, 24, 33, 35, 53, 55, 56, 57, 58, 59, 60],
|
|
63
|
-
form: [15, 16, 17, 24, 25, 27, 35, 47, 48, 59],
|
|
64
|
-
settings: [3, 7, 13, 15, 16, 17, 18, 21, 24, 35, 47, 48, 59],
|
|
65
|
-
authentication: [3, 13, 15, 24, 25, 27, 35, 47, 48, 56, 59],
|
|
66
|
-
"data-table": [3, 7, 22, 23, 24, 27, 31, 35, 47, 48, 59],
|
|
67
|
-
ecommerce: [3, 7, 13, 15, 22, 24, 25, 31, 33, 35, 47, 48, 55, 57, 59, 60],
|
|
68
|
-
documentation: [3, 4, 5, 8, 9, 10, 21, 23, 30, 35, 45, 47, 48, 53, 59, 61],
|
|
69
|
-
custom: [3, 35, 44, 47, 48, 49, 53, 59],
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const CATEGORY_TERMS = {
|
|
73
|
-
dashboard: "dashboard analytics metric chart sidebar",
|
|
74
|
-
"landing-page": "hero marketing testimonial feature pricing",
|
|
75
|
-
form: "form input select checkbox radio date picker",
|
|
76
|
-
settings: "settings preferences profile toggle tabs",
|
|
77
|
-
authentication: "login sign up password email verification",
|
|
78
|
-
"data-table": "table data pagination filter sort",
|
|
79
|
-
ecommerce: "product cart checkout price inventory",
|
|
80
|
-
documentation: "docs article navigation breadcrumb code",
|
|
81
|
-
custom: "",
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
function inferUiCategory(request) {
|
|
85
|
-
const text = request.toLowerCase();
|
|
86
|
-
const patterns = [
|
|
87
|
-
["authentication", /\b(login|log in|sign.?up|sign.?in|authentication|password reset)\b/],
|
|
88
|
-
["data-table", /\b(data.?table|table|records|spreadsheet|listing)\b/],
|
|
89
|
-
["ecommerce", /\b(e.?commerce|checkout|cart|product page|storefront|shop)\b/],
|
|
90
|
-
["documentation", /\b(documentation|docs page|knowledge base|article)\b/],
|
|
91
|
-
["settings", /\b(settings|preferences|configuration|profile)\b/],
|
|
92
|
-
["form", /\b(form|survey|wizard|application form)\b/],
|
|
93
|
-
["landing-page", /\b(landing page|homepage|marketing page|hero section)\b/],
|
|
94
|
-
["dashboard", /\b(dashboard|admin panel|analytics|metrics)\b/],
|
|
95
|
-
];
|
|
96
|
-
return patterns.find(([, pattern]) => pattern.test(text))?.[0] ?? "custom";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function makeUiBuildBrief({ request, category, preferences, registry, theme, aiDesignRules }) {
|
|
100
|
-
const resolvedCategory = category === "auto" ? inferUiCategory(request) : category;
|
|
101
|
-
const requestSignals = [
|
|
102
|
-
["create", /\b(create|add|new|invite|register)\b/i],
|
|
103
|
-
["review", /\b(review|view|inspect|monitor|track|activity|analytics)\b/i],
|
|
104
|
-
["search", /\b(search|find|filter|sort)\b/i],
|
|
105
|
-
["edit", /\b(edit|update|change|manage|configure)\b/i],
|
|
106
|
-
["complete", /\b(complete|submit|checkout|send|publish|save)\b/i],
|
|
107
|
-
["compare", /\b(compare|pricing|plan|option|alternative)\b/i],
|
|
108
|
-
].filter(([, pattern]) => pattern.test(request)).map(([signal]) => signal);
|
|
109
|
-
const sections = new Map(aiDesignRules.sections.map((section) => [section.number, section]));
|
|
110
|
-
const sharedRuleNumbers = [1, 2, 35, 37, 43, 44, 47, 48, 49, 50, 51, 59];
|
|
111
|
-
const selectedNumbers = [...new Set([...sharedRuleNumbers, ...(CATEGORY_RULES[resolvedCategory] ?? [])])];
|
|
112
|
-
const relevantRules = selectedNumbers
|
|
113
|
-
.map((number) => sections.get(number))
|
|
114
|
-
.filter(Boolean)
|
|
115
|
-
.map(({ number, title, content }) => ({ number, title, content }));
|
|
116
|
-
const terms = CATEGORY_TERMS[resolvedCategory] ?? "";
|
|
117
|
-
const candidates = terms
|
|
118
|
-
? registry.components
|
|
119
|
-
.filter((component) => terms.split(" ").some((term) => matchesQuery(component, term)))
|
|
120
|
-
.slice(0, 8)
|
|
121
|
-
.map(summarize)
|
|
122
|
-
: [];
|
|
123
|
-
|
|
124
|
-
const missingPreferences = [];
|
|
125
|
-
if (!preferences.visualDirection) missingPreferences.push({
|
|
126
|
-
id: "visualDirection", question: "What visual direction should this use?",
|
|
127
|
-
options: ["Neuctra default", "Minimal", "Editorial", "Dense and utilitarian"],
|
|
128
|
-
});
|
|
129
|
-
if (!preferences.density) missingPreferences.push({
|
|
130
|
-
id: "density", question: "How dense should the interface feel?",
|
|
131
|
-
options: ["Balanced", "Compact", "Spacious"],
|
|
2
|
+
import { loadDataFromDisk } from "./load-data.mjs";
|
|
3
|
+
import { registerNeuctraUiSetupTool } from "./tools/neuctra-ui-setup/register.mjs";
|
|
4
|
+
import { registerGetNeuctraUiComponentTool } from "./tools/get-neuctra-ui-component/index.mjs";
|
|
5
|
+
import { registerNeuctraUiPageBuilderTool } from "./tools/neuctra-ui-page-builder/index.mjs";
|
|
6
|
+
import { registerNeuctraUiThemeSetupTool } from "./tools/neuctra-ui-theme-setup/index.mjs";
|
|
7
|
+
import { registerGetDesignRulesTool } from "./tools/get-design-rules/index.mjs";
|
|
8
|
+
import { registerNeuctraUiSeoSetupTool } from "./tools/neuctra-ui-seo-setup/index.mjs";
|
|
9
|
+
|
|
10
|
+
export function createServer(data = loadDataFromDisk()) {
|
|
11
|
+
const server = new McpServer({ name: "neuctra-ui", version: "0.5.0" });
|
|
12
|
+
|
|
13
|
+
registerNeuctraUiSetupTool(server);
|
|
14
|
+
registerGetNeuctraUiComponentTool(server, data.registry);
|
|
15
|
+
registerNeuctraUiPageBuilderTool(server, data);
|
|
16
|
+
registerNeuctraUiThemeSetupTool(server, { theme: data.theme });
|
|
17
|
+
registerGetDesignRulesTool(server, {
|
|
18
|
+
theme: data.theme,
|
|
19
|
+
aiDesignRules: data.aiDesignRules,
|
|
132
20
|
});
|
|
133
|
-
|
|
134
|
-
id: "primaryAction", question: "What is the main action users should take?",
|
|
135
|
-
options: [],
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const compactTheme = {
|
|
139
|
-
system: theme.system,
|
|
140
|
-
tokens: theme.tokens.map(({ token, className, pairsWith, usage }) => ({ token, className, ...(pairsWith ? { pairsWith } : {}), usage })),
|
|
141
|
-
toastNote: theme.toastNote,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
category: resolvedCategory,
|
|
146
|
-
categoryWasInferred: category === "auto",
|
|
147
|
-
request,
|
|
148
|
-
requestAnalysis: {
|
|
149
|
-
detectedTaskSignals: requestSignals,
|
|
150
|
-
userGoal: requestSignals.length
|
|
151
|
-
? `Support the requested ${requestSignals.join(", ")} task${requestSignals.length > 1 ? "s" : ""} with a clear primary path.`
|
|
152
|
-
: "Use the request's stated outcome to define the primary path; do not add unrelated features.",
|
|
153
|
-
categoryConsiderations: {
|
|
154
|
-
dashboard: "Prioritize the most actionable metrics and provide clear routes to underlying records.",
|
|
155
|
-
"landing-page": "Make the value proposition and primary conversion action clear before secondary details.",
|
|
156
|
-
form: "Group fields by meaning, explain validation, and preserve entered data when errors occur.",
|
|
157
|
-
settings: "Group controls by user mental model and make saved versus unsaved changes clear.",
|
|
158
|
-
authentication: "Keep the entry task focused and make recovery and validation paths easy to find.",
|
|
159
|
-
"data-table": "Make scanning, filtering, row actions, and narrow-screen behavior usable.",
|
|
160
|
-
ecommerce: "Keep product choice, price, availability, and the next purchase action easy to understand.",
|
|
161
|
-
documentation: "Prioritize readable content, table of contents, and predictable navigation.",
|
|
162
|
-
custom: "Choose patterns from the user's task and content; avoid adding interface chrome without a need.",
|
|
163
|
-
}[resolvedCategory],
|
|
164
|
-
},
|
|
165
|
-
preferences,
|
|
166
|
-
uxPlan: {
|
|
167
|
-
objective: "Make the user's primary task obvious, then keep supporting choices and information in a clear hierarchy.",
|
|
168
|
-
layoutApproach: resolvedCategory === "dashboard" || resolvedCategory === "documentation"
|
|
169
|
-
? "Use a responsive app shell with grouped navigation and a focused content area."
|
|
170
|
-
: resolvedCategory === "landing-page"
|
|
171
|
-
? "Lead with the value proposition and primary action, then vary section layouts to support the story."
|
|
172
|
-
: "Keep the primary task prominent, group related content, and avoid panels where whitespace is enough.",
|
|
173
|
-
interactionAndStates: ["Define loading, empty, error, success, and disabled states where applicable.", "Use clear labels, keyboard-accessible controls, visible focus, and touch-friendly targets."],
|
|
174
|
-
responsiveBehavior: preferences.platform === "desktop"
|
|
175
|
-
? "Prioritize desktop workflows while keeping the layout usable at narrower widths and preventing horizontal overflow."
|
|
176
|
-
: preferences.platform === "mobile"
|
|
177
|
-
? "Prioritize the mobile task flow, touch targets, and compact navigation; prevent horizontal overflow."
|
|
178
|
-
: "Design mobile-first; preserve task order, prevent horizontal overflow, and adapt navigation and data views to narrow screens.",
|
|
179
|
-
performanceGuidance: ["Keep client-side state limited to interactive needs and avoid unnecessary rerenders.", "Paginate or virtualize large data sets; lazy-load below-the-fold media."],
|
|
180
|
-
preferenceQuestions: missingPreferences,
|
|
181
|
-
},
|
|
182
|
-
recommendedComponents: candidates,
|
|
183
|
-
designContext: {
|
|
184
|
-
coreStylingRules: theme.rules,
|
|
185
|
-
antiAiLookRules: theme.antiAiLookRules,
|
|
186
|
-
theme: compactTheme,
|
|
187
|
-
relevantProductRules: relevantRules,
|
|
188
|
-
goldenRule: aiDesignRules.goldenRule,
|
|
189
|
-
},
|
|
190
|
-
implementationNotes: [
|
|
191
|
-
"Use semantic Neuctra theme classes so light and dark themes work; do not hardcode palette colors.",
|
|
192
|
-
"Use the selected components' built-in design and exact documented props.",
|
|
193
|
-
"Call get_component for each unfamiliar component before writing its code.",
|
|
194
|
-
"Apply only the included category rules; retrieve get_design_rules for any unresolved edge case.",
|
|
195
|
-
],
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function createServer(data) {
|
|
200
|
-
const { registry, theme, aiDesignRules, seoGuide } = data ?? loadDataFromDisk();
|
|
201
|
-
const componentsByName = new Map(registry.components.map((c) => [c.name, c]));
|
|
202
|
-
|
|
203
|
-
const server = new McpServer({
|
|
204
|
-
name: "neuctra-ui",
|
|
205
|
-
version: "0.5.0",
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
server.registerTool(
|
|
209
|
-
"list_components",
|
|
210
|
-
{
|
|
211
|
-
title: "List Neuctra UI components",
|
|
212
|
-
description:
|
|
213
|
-
"List every component exported by @neuctra/ui, optionally filtered by category. Returns compact summaries — use get_component for full prop details.",
|
|
214
|
-
inputSchema: {
|
|
215
|
-
category: z
|
|
216
|
-
.enum([
|
|
217
|
-
"layout",
|
|
218
|
-
"typography",
|
|
219
|
-
"form",
|
|
220
|
-
"actions",
|
|
221
|
-
"data-display",
|
|
222
|
-
"feedback",
|
|
223
|
-
"overlay",
|
|
224
|
-
"navigation",
|
|
225
|
-
])
|
|
226
|
-
.optional()
|
|
227
|
-
.describe("Restrict results to one category."),
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
async ({ category }) => {
|
|
231
|
-
const list = registry.components
|
|
232
|
-
.filter((c) => !category || c.category === category)
|
|
233
|
-
.map(summarize);
|
|
234
|
-
return {
|
|
235
|
-
content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
server.registerTool(
|
|
241
|
-
"get_component",
|
|
242
|
-
{
|
|
243
|
-
title: "Get a Neuctra UI component's full spec",
|
|
244
|
-
description:
|
|
245
|
-
"Get the full spec for one @neuctra/ui component: every prop with its type, whether it's required, its default value, description, plus a real usage example. Always call this before writing code that uses a component you haven't used yet in this conversation — do not guess prop names.",
|
|
246
|
-
inputSchema: {
|
|
247
|
-
name: z
|
|
248
|
-
.string()
|
|
249
|
-
.describe(
|
|
250
|
-
'Exact component name, e.g. "Input", "Select", "Modal", "CardHeader".',
|
|
251
|
-
),
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
async ({ name }) => {
|
|
255
|
-
const component = componentsByName.get(name);
|
|
256
|
-
if (!component) {
|
|
257
|
-
const suggestions = registry.components
|
|
258
|
-
.map((c) => c.name)
|
|
259
|
-
.filter((n) => n.toLowerCase().includes(name.toLowerCase()))
|
|
260
|
-
.slice(0, 8);
|
|
261
|
-
return {
|
|
262
|
-
isError: true,
|
|
263
|
-
content: [
|
|
264
|
-
{
|
|
265
|
-
type: "text",
|
|
266
|
-
text: `No component named "${name}". ${
|
|
267
|
-
suggestions.length
|
|
268
|
-
? `Did you mean: ${suggestions.join(", ")}?`
|
|
269
|
-
: "Call list_components to see everything available."
|
|
270
|
-
}`,
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
content: [{ type: "text", text: JSON.stringify(component, null, 2) }],
|
|
277
|
-
};
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
server.registerTool(
|
|
282
|
-
"search_components",
|
|
283
|
-
{
|
|
284
|
-
title: "Search Neuctra UI components",
|
|
285
|
-
description:
|
|
286
|
-
'Search components by keyword against name, category, description, and prop names. Use this when you know what you need (e.g. "date picker", "loading state", "icon button") but not the exact component name.',
|
|
287
|
-
inputSchema: {
|
|
288
|
-
query: z.string().describe("Keyword or short phrase to search for."),
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
async ({ query }) => {
|
|
292
|
-
const results = registry.components
|
|
293
|
-
.filter((c) => matchesQuery(c, query))
|
|
294
|
-
.map(summarize);
|
|
295
|
-
return {
|
|
296
|
-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
297
|
-
};
|
|
298
|
-
},
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
server.registerTool(
|
|
302
|
-
"neuctra-ui-builder",
|
|
303
|
-
{
|
|
304
|
-
title: "Plan Neuctra UI with focused design context",
|
|
305
|
-
description:
|
|
306
|
-
"Prepare a compact, category-aware implementation brief for a UI request. Infers or accepts the UI category, identifies relevant Neuctra components and design-guide sections, includes essential theme/accessibility/responsive rules, and returns optional preference questions. Use this instead of loading the full design guide for routine UI generation. It plans and curates context; the host model writes the UI. Call get_component for exact props before using unfamiliar components, and get_design_rules for edge cases.",
|
|
307
|
-
inputSchema: {
|
|
308
|
-
request: z.string().min(8).describe("What UI to build and what users need to accomplish."),
|
|
309
|
-
category: z.enum(["auto", ...UI_CATEGORIES]).default("auto").describe("UI category, or auto to infer it from the request."),
|
|
310
|
-
preferences: z.object({
|
|
311
|
-
visualDirection: z.string().optional().describe("Visual style preference, if known."),
|
|
312
|
-
density: z.enum(["compact", "balanced", "spacious"]).optional(),
|
|
313
|
-
primaryAction: z.string().optional().describe("The main user action, if known."),
|
|
314
|
-
platform: z.enum(["desktop", "mobile", "responsive"]).optional(),
|
|
315
|
-
audience: z.string().optional(),
|
|
316
|
-
}).default({}).describe("Optional user preferences; unanswered high-impact choices are returned as concise questions."),
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
async ({ request, category, preferences }) => ({
|
|
320
|
-
content: [{
|
|
321
|
-
type: "text",
|
|
322
|
-
text: JSON.stringify(makeUiBuildBrief({ request, category, preferences, registry, theme, aiDesignRules }), null, 2),
|
|
323
|
-
}],
|
|
324
|
-
}),
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
server.registerTool(
|
|
328
|
-
"get_theme",
|
|
329
|
-
{
|
|
330
|
-
title: "Get Neuctra UI theming tokens",
|
|
331
|
-
description:
|
|
332
|
-
"Get the semantic color token system (bg-primary, text-foreground, etc.), the recommended `@neuctra/ui-cli` setup command, and how the toast notification API works. Call get_design_rules alongside this before generating any UI — not just for colors.",
|
|
333
|
-
inputSchema: {},
|
|
334
|
-
},
|
|
335
|
-
async () => {
|
|
336
|
-
const { rules, antiAiLookRules, ...themeOnly } = theme;
|
|
337
|
-
return {
|
|
338
|
-
content: [{ type: "text", text: JSON.stringify(themeOnly, null, 2) }],
|
|
339
|
-
};
|
|
340
|
-
},
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
server.registerTool(
|
|
344
|
-
"get_design_rules",
|
|
345
|
-
{
|
|
346
|
-
title: "Get Neuctra UI design rules",
|
|
347
|
-
description:
|
|
348
|
-
"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 get_theme.",
|
|
349
|
-
inputSchema: {},
|
|
350
|
-
},
|
|
351
|
-
async () => {
|
|
352
|
-
const { rules, antiAiLookRules } = theme;
|
|
353
|
-
return {
|
|
354
|
-
content: [
|
|
355
|
-
{
|
|
356
|
-
type: "text",
|
|
357
|
-
text: JSON.stringify(
|
|
358
|
-
{ stylingRules: rules, antiAiLookRules, productDesignGuide: aiDesignRules },
|
|
359
|
-
null,
|
|
360
|
-
2,
|
|
361
|
-
),
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
};
|
|
365
|
-
},
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
const seoTopicIds = seoGuide ? seoGuide.topics.map((t) => t.id) : [];
|
|
369
|
-
|
|
370
|
-
server.registerTool(
|
|
371
|
-
"get_seo_guide",
|
|
372
|
-
{
|
|
373
|
-
title: "Get the SEO, AEO and GEO guide",
|
|
374
|
-
description:
|
|
375
|
-
"Get the playbook for making a site built with @neuctra/ui rank in search (SEO), get chosen as the answer in featured snippets, AI Overviews and voice (AEO), and get cited by ChatGPT, Perplexity, Claude and Gemini (GEO). Topics: technical (SSR, client-only widgets, one URL registry, canonicals, robots.txt, sitemaps), performance (Core Web Vitals), on-page (titles with one brand-suffix rule, descriptions, headings), keywords-intent, eeat, aeo, geo (llms.txt, AI crawler policy, entity signals), schema (weaving one JSON-LD @graph, one node per type, tool/listing pages), faq, meta-checklist, frameworks (where metadata goes in Next.js, Vite, React Router, Astro, TanStack Start, Gatsby, HTML, plus Next.js metadata gotchas), page-templates (tool, category, home), site-wide, ads-readiness, off-page-monitoring and launch-checklist. Call this before building or reviewing any public-facing page.",
|
|
376
|
-
inputSchema: {
|
|
377
|
-
topic: z
|
|
378
|
-
.enum(["all", ...seoTopicIds])
|
|
379
|
-
.optional()
|
|
380
|
-
.describe('One topic id, or "all" (default) for the whole guide.'),
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
async ({ topic }) => {
|
|
384
|
-
if (!seoGuide) {
|
|
385
|
-
return {
|
|
386
|
-
isError: true,
|
|
387
|
-
content: [{ type: "text", text: "SEO guide data was not loaded into this server." }],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
const payload =
|
|
391
|
-
!topic || topic === "all"
|
|
392
|
-
? seoGuide
|
|
393
|
-
: seoGuide.topics.find((t) => t.id === topic);
|
|
394
|
-
return {
|
|
395
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
396
|
-
};
|
|
397
|
-
},
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
server.registerTool(
|
|
401
|
-
"generate_page_seo",
|
|
402
|
-
{
|
|
403
|
-
title: "Generate a page's meta tags and woven JSON-LD",
|
|
404
|
-
description:
|
|
405
|
-
"Generate everything one page needs to rank and be cited: a meta title (60 chars max) and description (155 chars max), canonical URL, robots, OpenGraph and Twitter tags, and one JSON-LD @graph that weaves Organization, WebSite, WebPage, BreadcrumbList, the page-type node (TechArticle, BlogPosting, SoftwareApplication for tools/products, Product with offers), HowTo (from visible steps), ItemList (for category/home pages) and FAQPage together by @id. Returns a ready-to-paste snippet for the chosen framework (Next.js App/Pages Router, Vite React, React Router/Remix, Astro, TanStack Start, Gatsby or plain HTML), plus warnings and AEO/GEO follow-ups. Deterministic: pass real FAQs, dates, prices and authors; it never invents them.",
|
|
406
|
-
inputSchema: {
|
|
407
|
-
framework: z.enum(FRAMEWORKS).default("html").describe("Target framework for the snippet."),
|
|
408
|
-
pageType: z.enum(PAGE_TYPES).describe("What the page is; picks the schema types and description template."),
|
|
409
|
-
siteName: z.string().describe('Brand name, e.g. "Neuctra UI".'),
|
|
410
|
-
siteUrl: z.string().url().describe('Site origin, e.g. "https://ui.neuctra.com".'),
|
|
411
|
-
path: z.string().describe('Route path, e.g. "/docs/card" ("/" for home).'),
|
|
412
|
-
primaryKeyword: z.string().describe('The one query this page should rank for, e.g. "react card component".'),
|
|
413
|
-
pageName: z.string().optional().describe("Short page name used as the headline/breadcrumb, e.g. \"Card Component\"."),
|
|
414
|
-
title: z.string().optional().describe("Custom title. Omit to build one from the keyword and brand."),
|
|
415
|
-
description: z.string().optional().describe("Custom meta description. Strongly recommended; the fallback is a template."),
|
|
416
|
-
breadcrumbs: z
|
|
417
|
-
.array(z.object({ name: z.string(), path: z.string() }))
|
|
418
|
-
.optional()
|
|
419
|
-
.describe("Breadcrumb trail from Home to this page. Omit to derive it from the path."),
|
|
420
|
-
faqs: z
|
|
421
|
-
.array(z.object({ question: z.string(), answer: z.string() }))
|
|
422
|
-
.optional()
|
|
423
|
-
.describe("Real FAQs that are visible on the page; mirrored word for word into FAQPage schema."),
|
|
424
|
-
ogImage: z.string().url().optional().describe("Absolute URL of a 1200x630 social image."),
|
|
425
|
-
logo: z.string().url().optional().describe("Absolute URL of the organization logo."),
|
|
426
|
-
sameAs: z.array(z.string().url()).optional().describe("Official profiles (GitHub, LinkedIn, X, npm, ...) for the Organization entity."),
|
|
427
|
-
author: z.string().optional().describe("Author name for docs/blog pages."),
|
|
428
|
-
datePublished: z.string().optional().describe("ISO date, e.g. 2026-09-27."),
|
|
429
|
-
dateModified: z.string().optional().describe("ISO date of the last real content change."),
|
|
430
|
-
productType: z.enum(["SoftwareApplication", "Product"]).optional().describe("Main entity type for product/pricing pages (default SoftwareApplication)."),
|
|
431
|
-
applicationCategory: z.string().optional().describe('SoftwareApplication category, e.g. "DeveloperApplication" (default).'),
|
|
432
|
-
offers: z
|
|
433
|
-
.array(z.object({ name: z.string().optional(), price: z.union([z.string(), z.number()]), priceCurrency: z.string() }))
|
|
434
|
-
.optional()
|
|
435
|
-
.describe("Real, visible prices for product/pricing pages."),
|
|
436
|
-
ogTitle: z.string().optional().describe("Shorter social title for og:title/twitter:title (defaults to the meta title)."),
|
|
437
|
-
ogDescription: z.string().optional().describe("Social description (defaults to the meta description)."),
|
|
438
|
-
isFree: z.boolean().optional().describe("Tool/product is free: adds isAccessibleForFree and a price 0 Offer."),
|
|
439
|
-
currency: z.string().optional().describe('Currency for the free Offer (default "USD").'),
|
|
440
|
-
features: z.array(z.string()).optional().describe("3 to 8 short capabilities, used as the SoftwareApplication featureList."),
|
|
441
|
-
steps: z.array(z.string()).optional().describe('The visible "How to" steps, in order; builds a HowTo node from the same data as the on-page list.'),
|
|
442
|
-
howToName: z.string().optional().describe('HowTo name, e.g. "How to merge PDF files" (default "How to use <page>").'),
|
|
443
|
-
items: z
|
|
444
|
-
.array(z.object({ name: z.string(), path: z.string() }))
|
|
445
|
-
.optional()
|
|
446
|
-
.describe("Pages a category/home page lists, in visible order; builds an ItemList."),
|
|
447
|
-
language: z.string().default("en").describe("Page language (BCP 47)."),
|
|
448
|
-
noindex: z.boolean().optional().describe("Set for utility pages that should not be indexed."),
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
async (input) => {
|
|
452
|
-
const result = generatePageSeo(input);
|
|
453
|
-
const { snippet, ...rest } = result;
|
|
454
|
-
return {
|
|
455
|
-
content: [
|
|
456
|
-
{ type: "text", text: JSON.stringify({ ...rest, snippetFile: snippet.file }, null, 2) },
|
|
457
|
-
{ type: "text", text: `Snippet for ${snippet.framework} (${snippet.file}):\n\n\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`` },
|
|
458
|
-
],
|
|
459
|
-
};
|
|
460
|
-
},
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
server.registerTool(
|
|
464
|
-
"audit_page_seo",
|
|
465
|
-
{
|
|
466
|
-
title: "Audit a page's SEO, AEO and schema",
|
|
467
|
-
description:
|
|
468
|
-
"Check one page against SEO, AEO and structured-data rules and get a score plus pass/warn/fail items, each with the exact fix: title and description length, a single H1, heading order, question-style H2s, keyword placement (title, H1, first 100 words, URL), URL shape, canonical, noindex, OpenGraph image, brand suffix (missing or doubled), JSON-LD syntax, duplicate schema nodes (e.g. two FAQPage or SoftwareApplication), WebPage/BreadcrumbList/Organization/FAQPage schema (and FAQ markup matching visible FAQs), FAQ count, thin content, missing alt text, and pages that render client-only (no H1 in the HTML). Easiest input: pass the built HTML as html. Otherwise pass whatever you know; checks without input are skipped.",
|
|
469
|
-
inputSchema: {
|
|
470
|
-
url: z.string().describe("Absolute URL of the page."),
|
|
471
|
-
html: z
|
|
472
|
-
.string()
|
|
473
|
-
.optional()
|
|
474
|
-
.describe("The page's built or server-rendered HTML (view-source, or a file from out/ or dist/). Title, description, canonical, headings, og:image, JSON-LD types, alt text and word count are extracted from it; explicit fields override."),
|
|
475
|
-
brandName: z.string().optional().describe('Brand expected once at the end of the title, e.g. "Neuctra Tools".'),
|
|
476
|
-
title: z.string().optional(),
|
|
477
|
-
description: z.string().optional(),
|
|
478
|
-
h1s: z.array(z.string()).optional().describe("Text of every H1 on the page."),
|
|
479
|
-
headings: z
|
|
480
|
-
.array(z.object({ level: z.number().int().min(1).max(6), text: z.string() }))
|
|
481
|
-
.optional()
|
|
482
|
-
.describe("All headings in document order."),
|
|
483
|
-
canonical: z.string().optional().describe('Canonical href; pass "" if the page has none.'),
|
|
484
|
-
noindex: z.boolean().optional(),
|
|
485
|
-
hasOgImage: z.boolean().optional(),
|
|
486
|
-
jsonLdTypes: z.array(z.string()).optional().describe('Every @type found in the page\'s JSON-LD, e.g. ["Organization","WebSite","WebPage","BreadcrumbList"].'),
|
|
487
|
-
faqCount: z.number().int().min(0).optional().describe("Number of FAQs visible on the page."),
|
|
488
|
-
wordCount: z.number().int().min(0).optional(),
|
|
489
|
-
imagesMissingAlt: z.number().int().min(0).optional(),
|
|
490
|
-
primaryKeyword: z.string().optional(),
|
|
491
|
-
firstParagraph: z.string().optional().describe("Opening text of the main content."),
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
async (input) => ({
|
|
495
|
-
content: [{ type: "text", text: JSON.stringify(auditPageSeo(input), null, 2) }],
|
|
496
|
-
}),
|
|
497
|
-
);
|
|
21
|
+
registerNeuctraUiSeoSetupTool(server, { seoGuide: data.seoGuide });
|
|
498
22
|
|
|
499
23
|
return server;
|
|
500
24
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export function getDesignRulesContext(theme, aiDesignRules, sectionNumbers) {
|
|
4
|
+
const { rules, antiAiLookRules } = theme;
|
|
5
|
+
if (!sectionNumbers) {
|
|
6
|
+
return { stylingRules: rules, antiAiLookRules, productDesignGuide: aiDesignRules };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const selected = new Set(sectionNumbers);
|
|
10
|
+
return {
|
|
11
|
+
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,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerGetDesignRulesTool(server, { theme, aiDesignRules }) {
|
|
23
|
+
server.registerTool(
|
|
24
|
+
"get_design_rules",
|
|
25
|
+
{
|
|
26
|
+
title: "Get Neuctra UI design rules",
|
|
27
|
+
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
|
+
};
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
}
|