@neuctra/ui-mcp 0.4.1 → 0.4.3

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/src/server.mjs CHANGED
@@ -3,11 +3,17 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { z } from "zod";
6
+ import {
7
+ FRAMEWORKS,
8
+ PAGE_TYPES,
9
+ generatePageSeo,
10
+ auditPageSeo,
11
+ } from "./seo.mjs";
6
12
 
7
13
  // Reads data/*.json from disk. Only works on runtimes with a filesystem
8
14
  // (Node, Bun, Deno) — callers on edge/serverless runtimes without one
9
15
  // (Cloudflare Workers) must import the JSON files themselves at build time
10
- // and pass them into createServer({ registry, theme, aiDesignRules }) instead.
16
+ // and pass them into createServer({ registry, theme, aiDesignRules, seoGuide }) instead.
11
17
  // Resolving __dirname is deferred inside this function (not module top-level)
12
18
  // because import.meta.url is unavailable in some bundled edge runtimes even
13
19
  // when this function itself is never called there.
@@ -23,7 +29,10 @@ function loadDataFromDisk() {
23
29
  const aiDesignRules = JSON.parse(
24
30
  fs.readFileSync(path.join(DATA_DIR, "aiDesignRules.json"), "utf8"),
25
31
  );
26
- return { registry, theme, aiDesignRules };
32
+ const seoGuide = JSON.parse(
33
+ fs.readFileSync(path.join(DATA_DIR, "seoGuide.json"), "utf8"),
34
+ );
35
+ return { registry, theme, aiDesignRules, seoGuide };
27
36
  }
28
37
 
29
38
  function summarize(component) {
@@ -44,12 +53,12 @@ function matchesQuery(component, query) {
44
53
  }
45
54
 
46
55
  export function createServer(data) {
47
- const { registry, theme, aiDesignRules } = data ?? loadDataFromDisk();
56
+ const { registry, theme, aiDesignRules, seoGuide } = data ?? loadDataFromDisk();
48
57
  const componentsByName = new Map(registry.components.map((c) => [c.name, c]));
49
58
 
50
59
  const server = new McpServer({
51
60
  name: "neuctra-ui",
52
- version: "0.4.0",
61
+ version: "0.5.0",
53
62
  });
54
63
 
55
64
  server.registerTool(
@@ -166,7 +175,7 @@ export function createServer(data) {
166
175
  {
167
176
  title: "Get Neuctra UI design rules",
168
177
  description:
169
- "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 50-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, and the rule against overwriting a component's built-in design. Call this before generating any UI — not just get_theme.",
178
+ "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.",
170
179
  inputSchema: {},
171
180
  },
172
181
  async () => {
@@ -186,5 +195,136 @@ export function createServer(data) {
186
195
  },
187
196
  );
188
197
 
198
+ const seoTopicIds = seoGuide ? seoGuide.topics.map((t) => t.id) : [];
199
+
200
+ server.registerTool(
201
+ "get_seo_guide",
202
+ {
203
+ title: "Get the SEO, AEO and GEO guide",
204
+ description:
205
+ "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.",
206
+ inputSchema: {
207
+ topic: z
208
+ .enum(["all", ...seoTopicIds])
209
+ .optional()
210
+ .describe('One topic id, or "all" (default) for the whole guide.'),
211
+ },
212
+ },
213
+ async ({ topic }) => {
214
+ if (!seoGuide) {
215
+ return {
216
+ isError: true,
217
+ content: [{ type: "text", text: "SEO guide data was not loaded into this server." }],
218
+ };
219
+ }
220
+ const payload =
221
+ !topic || topic === "all"
222
+ ? seoGuide
223
+ : seoGuide.topics.find((t) => t.id === topic);
224
+ return {
225
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
226
+ };
227
+ },
228
+ );
229
+
230
+ server.registerTool(
231
+ "generate_page_seo",
232
+ {
233
+ title: "Generate a page's meta tags and woven JSON-LD",
234
+ description:
235
+ "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.",
236
+ inputSchema: {
237
+ framework: z.enum(FRAMEWORKS).default("html").describe("Target framework for the snippet."),
238
+ pageType: z.enum(PAGE_TYPES).describe("What the page is; picks the schema types and description template."),
239
+ siteName: z.string().describe('Brand name, e.g. "Neuctra UI".'),
240
+ siteUrl: z.string().url().describe('Site origin, e.g. "https://ui.neuctra.com".'),
241
+ path: z.string().describe('Route path, e.g. "/docs/card" ("/" for home).'),
242
+ primaryKeyword: z.string().describe('The one query this page should rank for, e.g. "react card component".'),
243
+ pageName: z.string().optional().describe("Short page name used as the headline/breadcrumb, e.g. \"Card Component\"."),
244
+ title: z.string().optional().describe("Custom title. Omit to build one from the keyword and brand."),
245
+ description: z.string().optional().describe("Custom meta description. Strongly recommended; the fallback is a template."),
246
+ breadcrumbs: z
247
+ .array(z.object({ name: z.string(), path: z.string() }))
248
+ .optional()
249
+ .describe("Breadcrumb trail from Home to this page. Omit to derive it from the path."),
250
+ faqs: z
251
+ .array(z.object({ question: z.string(), answer: z.string() }))
252
+ .optional()
253
+ .describe("Real FAQs that are visible on the page; mirrored word for word into FAQPage schema."),
254
+ ogImage: z.string().url().optional().describe("Absolute URL of a 1200x630 social image."),
255
+ logo: z.string().url().optional().describe("Absolute URL of the organization logo."),
256
+ sameAs: z.array(z.string().url()).optional().describe("Official profiles (GitHub, LinkedIn, X, npm, ...) for the Organization entity."),
257
+ author: z.string().optional().describe("Author name for docs/blog pages."),
258
+ datePublished: z.string().optional().describe("ISO date, e.g. 2026-09-27."),
259
+ dateModified: z.string().optional().describe("ISO date of the last real content change."),
260
+ productType: z.enum(["SoftwareApplication", "Product"]).optional().describe("Main entity type for product/pricing pages (default SoftwareApplication)."),
261
+ applicationCategory: z.string().optional().describe('SoftwareApplication category, e.g. "DeveloperApplication" (default).'),
262
+ offers: z
263
+ .array(z.object({ name: z.string().optional(), price: z.union([z.string(), z.number()]), priceCurrency: z.string() }))
264
+ .optional()
265
+ .describe("Real, visible prices for product/pricing pages."),
266
+ ogTitle: z.string().optional().describe("Shorter social title for og:title/twitter:title (defaults to the meta title)."),
267
+ ogDescription: z.string().optional().describe("Social description (defaults to the meta description)."),
268
+ isFree: z.boolean().optional().describe("Tool/product is free: adds isAccessibleForFree and a price 0 Offer."),
269
+ currency: z.string().optional().describe('Currency for the free Offer (default "USD").'),
270
+ features: z.array(z.string()).optional().describe("3 to 8 short capabilities, used as the SoftwareApplication featureList."),
271
+ 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.'),
272
+ howToName: z.string().optional().describe('HowTo name, e.g. "How to merge PDF files" (default "How to use <page>").'),
273
+ items: z
274
+ .array(z.object({ name: z.string(), path: z.string() }))
275
+ .optional()
276
+ .describe("Pages a category/home page lists, in visible order; builds an ItemList."),
277
+ language: z.string().default("en").describe("Page language (BCP 47)."),
278
+ noindex: z.boolean().optional().describe("Set for utility pages that should not be indexed."),
279
+ },
280
+ },
281
+ async (input) => {
282
+ const result = generatePageSeo(input);
283
+ const { snippet, ...rest } = result;
284
+ return {
285
+ content: [
286
+ { type: "text", text: JSON.stringify({ ...rest, snippetFile: snippet.file }, null, 2) },
287
+ { type: "text", text: `Snippet for ${snippet.framework} (${snippet.file}):\n\n\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`` },
288
+ ],
289
+ };
290
+ },
291
+ );
292
+
293
+ server.registerTool(
294
+ "audit_page_seo",
295
+ {
296
+ title: "Audit a page's SEO, AEO and schema",
297
+ description:
298
+ "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.",
299
+ inputSchema: {
300
+ url: z.string().describe("Absolute URL of the page."),
301
+ html: z
302
+ .string()
303
+ .optional()
304
+ .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."),
305
+ brandName: z.string().optional().describe('Brand expected once at the end of the title, e.g. "Neuctra Tools".'),
306
+ title: z.string().optional(),
307
+ description: z.string().optional(),
308
+ h1s: z.array(z.string()).optional().describe("Text of every H1 on the page."),
309
+ headings: z
310
+ .array(z.object({ level: z.number().int().min(1).max(6), text: z.string() }))
311
+ .optional()
312
+ .describe("All headings in document order."),
313
+ canonical: z.string().optional().describe('Canonical href; pass "" if the page has none.'),
314
+ noindex: z.boolean().optional(),
315
+ hasOgImage: z.boolean().optional(),
316
+ jsonLdTypes: z.array(z.string()).optional().describe('Every @type found in the page\'s JSON-LD, e.g. ["Organization","WebSite","WebPage","BreadcrumbList"].'),
317
+ faqCount: z.number().int().min(0).optional().describe("Number of FAQs visible on the page."),
318
+ wordCount: z.number().int().min(0).optional(),
319
+ imagesMissingAlt: z.number().int().min(0).optional(),
320
+ primaryKeyword: z.string().optional(),
321
+ firstParagraph: z.string().optional().describe("Opening text of the main content."),
322
+ },
323
+ },
324
+ async (input) => ({
325
+ content: [{ type: "text", text: JSON.stringify(auditPageSeo(input), null, 2) }],
326
+ }),
327
+ );
328
+
189
329
  return server;
190
330
  }