@rivi-software/agentic-seo-check 0.1.0 → 0.2.0
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/LICENSE +1 -1
- package/README.md +124 -18
- package/dist/brand.d.ts +11 -0
- package/dist/brand.js +22 -0
- package/dist/brand.js.map +1 -0
- package/dist/cli.js +174 -86
- package/dist/cli.js.map +1 -1
- package/dist/core/aeo/audit.d.ts +44 -0
- package/dist/core/aeo/audit.js +310 -0
- package/dist/core/aeo/audit.js.map +1 -0
- package/dist/core/aeo/bots.d.ts +30 -0
- package/dist/core/aeo/bots.js +299 -0
- package/dist/core/aeo/bots.js.map +1 -0
- package/dist/core/aeo/llms-txt.d.ts +8 -0
- package/dist/core/aeo/llms-txt.js +88 -0
- package/dist/core/aeo/llms-txt.js.map +1 -0
- package/dist/core/aeo/page.d.ts +53 -0
- package/dist/core/aeo/page.js +312 -0
- package/dist/core/aeo/page.js.map +1 -0
- package/dist/core/aeo/probe.d.ts +10 -0
- package/dist/core/aeo/probe.js +53 -0
- package/dist/core/aeo/probe.js.map +1 -0
- package/dist/core/aeo/site-rules.d.ts +13 -0
- package/dist/core/aeo/site-rules.js +178 -0
- package/dist/core/aeo/site-rules.js.map +1 -0
- package/dist/core/audit.d.ts +37 -16
- package/dist/core/audit.js +389 -249
- package/dist/core/audit.js.map +1 -1
- package/dist/core/concurrency.d.ts +4 -4
- package/dist/core/concurrency.js +33 -23
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/crawl.d.ts +9 -5
- package/dist/core/crawl.js +271 -123
- package/dist/core/crawl.js.map +1 -1
- package/dist/core/fetch.d.ts +5 -5
- package/dist/core/fetch.js +151 -100
- package/dist/core/fetch.js.map +1 -1
- package/dist/core/html/answer.d.ts +4 -0
- package/dist/core/html/answer.js +198 -0
- package/dist/core/html/answer.js.map +1 -0
- package/dist/core/html/dom.d.ts +17 -0
- package/dist/core/html/dom.js +83 -0
- package/dist/core/html/dom.js.map +1 -0
- package/dist/core/html/extract.d.ts +2 -2
- package/dist/core/html/extract.js +102 -111
- package/dist/core/html/extract.js.map +1 -1
- package/dist/core/html/structured-data.d.ts +16 -6
- package/dist/core/html/structured-data.js +180 -145
- package/dist/core/html/structured-data.js.map +1 -1
- package/dist/core/html/text.d.ts +14 -14
- package/dist/core/html/text.js +110 -107
- package/dist/core/html/text.js.map +1 -1
- package/dist/core/lighthouse.d.ts +26 -25
- package/dist/core/lighthouse.js +181 -105
- package/dist/core/lighthouse.js.map +1 -1
- package/dist/core/performance-rules.d.ts +2 -2
- package/dist/core/performance-rules.js +105 -33
- package/dist/core/performance-rules.js.map +1 -1
- package/dist/core/render-rules.d.ts +5 -4
- package/dist/core/render-rules.js +129 -62
- package/dist/core/render-rules.js.map +1 -1
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +29 -23
- package/dist/core/render.js.map +1 -1
- package/dist/core/report/markdown.d.ts +4 -2
- package/dist/core/report/markdown.js +210 -129
- package/dist/core/report/markdown.js.map +1 -1
- package/dist/core/robots.d.ts +12 -5
- package/dist/core/robots.js +43 -17
- package/dist/core/robots.js.map +1 -1
- package/dist/core/rules/aeo.d.ts +11 -0
- package/dist/core/rules/aeo.js +423 -0
- package/dist/core/rules/aeo.js.map +1 -0
- package/dist/core/rules/canonical.d.ts +1 -1
- package/dist/core/rules/canonical.js +93 -57
- package/dist/core/rules/canonical.js.map +1 -1
- package/dist/core/rules/content.js +40 -26
- package/dist/core/rules/content.js.map +1 -1
- package/dist/core/rules/document.js +64 -41
- package/dist/core/rules/document.js.map +1 -1
- package/dist/core/rules/headings.js +58 -38
- package/dist/core/rules/headings.js.map +1 -1
- package/dist/core/rules/hreflang.js +70 -38
- package/dist/core/rules/hreflang.js.map +1 -1
- package/dist/core/rules/images.js +44 -26
- package/dist/core/rules/images.js.map +1 -1
- package/dist/core/rules/index.d.ts +10 -6
- package/dist/core/rules/index.js +43 -36
- package/dist/core/rules/index.js.map +1 -1
- package/dist/core/rules/indexing.d.ts +2 -2
- package/dist/core/rules/indexing.js +48 -26
- package/dist/core/rules/indexing.js.map +1 -1
- package/dist/core/rules/keyword.js +84 -47
- package/dist/core/rules/keyword.js.map +1 -1
- package/dist/core/rules/links.js +72 -37
- package/dist/core/rules/links.js.map +1 -1
- package/dist/core/rules/meta.js +55 -34
- package/dist/core/rules/meta.js.map +1 -1
- package/dist/core/rules/response.js +75 -41
- package/dist/core/rules/response.js.map +1 -1
- package/dist/core/rules/rule.d.ts +6 -6
- package/dist/core/rules/rule.js +36 -36
- package/dist/core/rules/rule.js.map +1 -1
- package/dist/core/rules/social.js +86 -45
- package/dist/core/rules/social.js.map +1 -1
- package/dist/core/rules/structured-data.d.ts +6 -4
- package/dist/core/rules/structured-data.js +314 -102
- package/dist/core/rules/structured-data.js.map +1 -1
- package/dist/core/rules/title.js +48 -33
- package/dist/core/rules/title.js.map +1 -1
- package/dist/core/score.d.ts +5 -3
- package/dist/core/score.js +11 -9
- package/dist/core/score.js.map +1 -1
- package/dist/core/site-rules.d.ts +3 -3
- package/dist/core/site-rules.js +409 -198
- package/dist/core/site-rules.js.map +1 -1
- package/dist/core/sitemap.d.ts +5 -5
- package/dist/core/sitemap.js +109 -64
- package/dist/core/sitemap.js.map +1 -1
- package/dist/core/static.d.ts +2 -2
- package/dist/core/static.js +210 -112
- package/dist/core/static.js.map +1 -1
- package/dist/core/summary.d.ts +2 -2
- package/dist/core/summary.js +11 -6
- package/dist/core/summary.js.map +1 -1
- package/dist/core/types.d.ts +189 -14
- package/dist/core/types.js +15 -8
- package/dist/core/types.js.map +1 -1
- package/dist/core/url.d.ts +11 -11
- package/dist/core/url.js +122 -70
- package/dist/core/url.js.map +1 -1
- package/dist/index.d.ts +15 -11
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +7 -4
- package/dist/mcp-bin.js.map +1 -1
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +437 -121
- package/dist/mcp.js.map +1 -1
- package/dist/version.d.ts +0 -1
- package/dist/version.js +4 -2
- package/dist/version.js.map +1 -1
- package/package.json +32 -10
- package/skills/seo-audit/SKILL.md +8 -2
package/dist/mcp.js
CHANGED
|
@@ -1,92 +1,255 @@
|
|
|
1
|
-
import { McpServer } from
|
|
2
|
-
import { StdioServerTransport } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { MAKER, MCP_ICONS, REPOSITORY_URL } from "./brand.js";
|
|
5
|
+
import { auditAeoDirectory, auditAeoHtml, auditAeoSite, auditAeoUrl, compactAeoAudit, } from "./core/aeo/audit.js";
|
|
6
|
+
import { auditDirectory, auditHtml, auditSite, auditUrl, compactAudit, } from "./core/audit.js";
|
|
7
|
+
import { runLighthouse } from "./core/lighthouse.js";
|
|
8
|
+
import { performanceFindings } from "./core/performance-rules.js";
|
|
9
|
+
import { renderAeoMarkdown, renderMarkdown } from "./core/report/markdown.js";
|
|
10
|
+
import { ALL_RULES } from "./core/rules/index.js";
|
|
11
|
+
import { VERSION } from "./version.js";
|
|
12
|
+
export const MCP_SERVER_NAME = "agentic-seo-check";
|
|
11
13
|
export const MCP_SERVER_VERSION = VERSION;
|
|
12
|
-
const format = z
|
|
13
|
-
|
|
14
|
+
const format = z
|
|
15
|
+
.enum(["json", "markdown"])
|
|
16
|
+
.default("json")
|
|
17
|
+
.describe("json (default) returns the structured audit; markdown returns a human-readable report.");
|
|
18
|
+
const detail = z
|
|
19
|
+
.enum(["full", "compact"])
|
|
20
|
+
.default("compact")
|
|
21
|
+
.describe("compact trims evidence lists and bulky fields to save tokens (default); full returns everything.");
|
|
14
22
|
const commonOptions = {
|
|
15
|
-
keyword: z
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
keyword: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Target keyword/phrase. Enables keyword.* checks (title, H1, body, description, URL, density)."),
|
|
27
|
+
includeInfo: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.default(true)
|
|
30
|
+
.describe("Include info-level findings (best practices). Set false to see only errors and warnings."),
|
|
31
|
+
disableRules: z
|
|
32
|
+
.array(z.string())
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Rule ids to skip, e.g. ["twitter.card-missing"]. Use seo_list_rules to see ids.'),
|
|
35
|
+
timeoutMs: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.min(1000)
|
|
39
|
+
.max(120_000)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Per-request timeout in milliseconds (default 15000)."),
|
|
42
|
+
userAgent: z.string().optional().describe("Override the User-Agent header."),
|
|
43
|
+
headers: z
|
|
44
|
+
.record(z.string(), z.string())
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Extra request headers, e.g. {"Authorization": "Basic ..."} for staging sites.'),
|
|
21
47
|
format,
|
|
22
48
|
detail,
|
|
23
49
|
};
|
|
24
|
-
|
|
50
|
+
const toOptions = (input) => ({
|
|
51
|
+
keyword: input.keyword,
|
|
52
|
+
includeInfo: input.includeInfo,
|
|
53
|
+
disableRules: input.disableRules,
|
|
54
|
+
fetch: {
|
|
55
|
+
timeoutMs: input.timeoutMs,
|
|
56
|
+
userAgent: input.userAgent,
|
|
57
|
+
headers: input.headers,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const respond = (audit, fmt, det) => {
|
|
61
|
+
const report = det === "compact" ? compactAudit(audit) : audit;
|
|
62
|
+
const text = fmt === "markdown"
|
|
63
|
+
? renderMarkdown(report)
|
|
64
|
+
: JSON.stringify(report, null, 2);
|
|
25
65
|
return {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
disableRules: input.disableRules,
|
|
29
|
-
fetch: { timeoutMs: input.timeoutMs, userAgent: input.userAgent, headers: input.headers },
|
|
66
|
+
content: [{ type: "text", text }],
|
|
67
|
+
structuredContent: { ...report },
|
|
30
68
|
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const text = fmt ===
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
};
|
|
70
|
+
const respondAeo = (audit, fmt, det) => {
|
|
71
|
+
const report = det === "compact" ? compactAeoAudit(audit) : audit;
|
|
72
|
+
const text = fmt === "markdown"
|
|
73
|
+
? renderAeoMarkdown(report)
|
|
74
|
+
: JSON.stringify(report, null, 2);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text }],
|
|
77
|
+
structuredContent: { ...report },
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- a thrown value is unknown until this reads it
|
|
81
|
+
const fail = (error) => {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
return {
|
|
84
|
+
isError: true,
|
|
85
|
+
content: [
|
|
86
|
+
{ type: "text", text: `agentic-seo-check error: ${message}` },
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const metricCell = (name, value) => {
|
|
91
|
+
if (value === null) {
|
|
92
|
+
return "—";
|
|
93
|
+
}
|
|
94
|
+
return name.endsWith("Ms") ? `${Math.round(value)} ms` : String(value);
|
|
95
|
+
};
|
|
96
|
+
const lighthouseMarkdown = (payload) => {
|
|
97
|
+
const lines = [
|
|
98
|
+
`# Lighthouse (${payload.formFactor}): ${payload.finalUrl}`,
|
|
99
|
+
"",
|
|
100
|
+
"| Metric | Value |",
|
|
101
|
+
"| --- | --- |",
|
|
102
|
+
];
|
|
103
|
+
for (const [name, value] of Object.entries(payload.metrics)) {
|
|
104
|
+
lines.push(`| ${name} | ${metricCell(name, value)} |`);
|
|
105
|
+
}
|
|
106
|
+
lines.push("", "## Opportunities", "");
|
|
107
|
+
for (const o of payload.opportunities.slice(0, 10)) {
|
|
108
|
+
lines.push(`- ${o.title}${o.savingsMs ? ` (~${Math.round(o.savingsMs)} ms)` : ""}${o.savingsBytes ? ` (~${Math.round(o.savingsBytes / 1024)} KiB)` : ""}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("", "## Findings", "");
|
|
111
|
+
for (const f of payload.findings) {
|
|
112
|
+
lines.push(`- **\`${f.rule}\`** ${f.message}`, ` - Fix: ${f.recommendation}`);
|
|
113
|
+
}
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
};
|
|
116
|
+
export const createMcpServer = () => {
|
|
117
|
+
const server = new McpServer({
|
|
118
|
+
name: MCP_SERVER_NAME,
|
|
119
|
+
title: `agentic-seo-check by ${MAKER}`,
|
|
120
|
+
version: MCP_SERVER_VERSION,
|
|
121
|
+
description: "SEO and AEO audits for coding agents: pages, sites, build output and raw HTML, checked for search engines and AI answer engines, with findings an agent can act on.",
|
|
122
|
+
websiteUrl: REPOSITORY_URL,
|
|
123
|
+
icons: MCP_ICONS,
|
|
124
|
+
});
|
|
125
|
+
server.registerTool("seo_audit_page", {
|
|
126
|
+
title: "Audit one page for SEO",
|
|
127
|
+
description: "Audit a single page and return prioritised, actionable findings (rule id, severity, message, recommendation, CSS selector, evidence). " +
|
|
128
|
+
"Checks title, meta description, headings, canonical, robots/noindex, viewport/lang, Open Graph/Twitter cards, images, links, hreflang, structured data (JSON-LD/microdata), content quality and the HTTP response. " +
|
|
129
|
+
"Provide either `url` (live site, staging or http://localhost dev server) or `html` (raw HTML string, e.g. from a template or test render). " +
|
|
130
|
+
"Set `render: true` to also load the page in headless Chromium and report content that only exists after JavaScript runs (SPA/CSR problems). " +
|
|
131
|
+
"Set `lighthouse: true` for Core Web Vitals (LCP, CLS, TBT) and Lighthouse SEO audits; this takes 20-60 seconds. " +
|
|
132
|
+
"Typical workflow: audit, fix the errors and warnings in the source code, re-audit to confirm.",
|
|
51
133
|
inputSchema: {
|
|
52
|
-
url: z
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
134
|
+
url: z
|
|
135
|
+
.string()
|
|
136
|
+
.url()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("Absolute http(s) URL to fetch and audit."),
|
|
139
|
+
html: z
|
|
140
|
+
.string()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe("Raw HTML to audit instead of fetching a URL."),
|
|
143
|
+
baseUrl: z
|
|
144
|
+
.string()
|
|
145
|
+
.url()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe("With `html`: the URL the HTML would be served at (used to resolve links and classify them as internal/external)."),
|
|
148
|
+
render: z
|
|
149
|
+
.boolean()
|
|
150
|
+
.default(false)
|
|
151
|
+
.describe("Also render with headless Chromium (Playwright) and compare against the server HTML."),
|
|
152
|
+
lighthouse: z
|
|
153
|
+
.boolean()
|
|
154
|
+
.default(false)
|
|
155
|
+
.describe("Also run Lighthouse (performance, SEO, agentic-browsing categories). Slow."),
|
|
156
|
+
formFactor: z
|
|
157
|
+
.enum(["mobile", "desktop"])
|
|
158
|
+
.default("mobile")
|
|
159
|
+
.describe("Lighthouse device emulation."),
|
|
58
160
|
...commonOptions,
|
|
59
161
|
},
|
|
60
|
-
annotations: {
|
|
162
|
+
annotations: {
|
|
163
|
+
readOnlyHint: true,
|
|
164
|
+
openWorldHint: true,
|
|
165
|
+
idempotentHint: true,
|
|
166
|
+
},
|
|
61
167
|
}, async (input) => {
|
|
62
168
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
169
|
+
const options = {
|
|
170
|
+
...toOptions(input),
|
|
171
|
+
render: input.render,
|
|
172
|
+
lighthouse: input.lighthouse,
|
|
173
|
+
formFactor: input.formFactor,
|
|
174
|
+
baseUrl: input.baseUrl,
|
|
175
|
+
};
|
|
176
|
+
const { html, url } = input;
|
|
177
|
+
let audit;
|
|
178
|
+
// `html` wins over `url`; an empty `html` counts only when a `url` came with it.
|
|
179
|
+
if (html !== undefined && (html || url)) {
|
|
180
|
+
audit = await auditHtml(html, { url: input.baseUrl, source: "html", target: "inline-html" }, options);
|
|
181
|
+
}
|
|
182
|
+
else if (url) {
|
|
183
|
+
audit = await auditUrl(url, options);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
throw new Error("Provide `url` or `html`.");
|
|
187
|
+
}
|
|
67
188
|
return respond(audit, input.format, input.detail);
|
|
68
189
|
}
|
|
69
|
-
catch (
|
|
70
|
-
return fail(
|
|
190
|
+
catch (error) {
|
|
191
|
+
return fail(error);
|
|
71
192
|
}
|
|
72
193
|
});
|
|
73
|
-
server.registerTool(
|
|
74
|
-
title:
|
|
75
|
-
description:
|
|
76
|
-
|
|
77
|
-
|
|
194
|
+
server.registerTool("seo_crawl_site", {
|
|
195
|
+
title: "Crawl a site and audit every page",
|
|
196
|
+
description: "Crawl internal links from a start URL (default 25 pages, depth 3), audit every HTML page, and run site-wide checks: robots.txt, XML sitemap coverage (missing/noindex/non-canonical/broken URLs), broken internal links with their sources, redirect chains, duplicate titles/descriptions/H1s/content, orphan and deep pages, soft-404 behaviour, http→https and www redirects, hreflang return links. " +
|
|
197
|
+
"Returns site findings plus `issues` (page findings rolled up by rule with example URLs) and per-page scores. Use seo_audit_page on a specific URL for full per-page detail. " +
|
|
198
|
+
"Works on http://localhost dev servers; for those set `respectRobots: false` if the dev server serves a blocking robots.txt.",
|
|
78
199
|
inputSchema: {
|
|
79
|
-
url: z.string().url().describe(
|
|
80
|
-
maxPages: z
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
200
|
+
url: z.string().url().describe("Start URL (usually the homepage)."),
|
|
201
|
+
maxPages: z
|
|
202
|
+
.number()
|
|
203
|
+
.int()
|
|
204
|
+
.min(1)
|
|
205
|
+
.max(500)
|
|
206
|
+
.default(25)
|
|
207
|
+
.describe("Maximum pages to fetch."),
|
|
208
|
+
maxDepth: z
|
|
209
|
+
.number()
|
|
210
|
+
.int()
|
|
211
|
+
.min(0)
|
|
212
|
+
.max(10)
|
|
213
|
+
.default(3)
|
|
214
|
+
.describe("Maximum link depth from the start URL."),
|
|
215
|
+
concurrency: z
|
|
216
|
+
.number()
|
|
217
|
+
.int()
|
|
218
|
+
.min(1)
|
|
219
|
+
.max(10)
|
|
220
|
+
.default(4)
|
|
221
|
+
.describe("Parallel requests."),
|
|
222
|
+
delayMs: z
|
|
223
|
+
.number()
|
|
224
|
+
.int()
|
|
225
|
+
.min(0)
|
|
226
|
+
.max(5000)
|
|
227
|
+
.default(0)
|
|
228
|
+
.describe("Delay between requests per worker (politeness)."),
|
|
229
|
+
respectRobots: z
|
|
230
|
+
.boolean()
|
|
231
|
+
.default(true)
|
|
232
|
+
.describe("Obey robots.txt disallow rules while crawling."),
|
|
233
|
+
includeSubdomains: z
|
|
234
|
+
.boolean()
|
|
235
|
+
.default(false)
|
|
236
|
+
.describe("Treat subdomains of the start host as internal."),
|
|
237
|
+
checkExternalLinks: z
|
|
238
|
+
.boolean()
|
|
239
|
+
.default(false)
|
|
240
|
+
.describe("Also probe external link targets for 4xx/5xx (slower)."),
|
|
241
|
+
includePatterns: z
|
|
242
|
+
.array(z.string())
|
|
243
|
+
.optional()
|
|
244
|
+
.describe("Only crawl URLs matching one of these regular expressions."),
|
|
245
|
+
excludePatterns: z
|
|
246
|
+
.array(z.string())
|
|
247
|
+
.optional()
|
|
248
|
+
.describe("Skip URLs matching any of these regular expressions."),
|
|
249
|
+
pageDetail: z
|
|
250
|
+
.enum(["summary", "full"])
|
|
251
|
+
.default("summary")
|
|
252
|
+
.describe("summary: per-page scores/counts only (findings are rolled up in `issues`); full: every finding on every page."),
|
|
90
253
|
...commonOptions,
|
|
91
254
|
},
|
|
92
255
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
@@ -107,84 +270,237 @@ export function createMcpServer() {
|
|
|
107
270
|
});
|
|
108
271
|
return respond(audit, input.format, input.detail);
|
|
109
272
|
}
|
|
110
|
-
catch (
|
|
111
|
-
return fail(
|
|
273
|
+
catch (error) {
|
|
274
|
+
return fail(error);
|
|
112
275
|
}
|
|
113
276
|
});
|
|
114
|
-
server.registerTool(
|
|
115
|
-
title:
|
|
116
|
-
description:
|
|
117
|
-
|
|
277
|
+
server.registerTool("seo_audit_directory", {
|
|
278
|
+
title: "Audit a static build output directory",
|
|
279
|
+
description: "Audit every .html file in a build output directory (dist/, out/, build/, .output/public) without running a server. Runs all page checks plus directory-level checks: internal links that resolve to no file, sitemap.xml/robots.txt in the output, duplicate titles/descriptions/content, unlinked files. " +
|
|
280
|
+
"Pass `baseUrl` as the production origin (e.g. https://www.example.com) so absolute links, canonicals and sitemap URLs are classified correctly.",
|
|
118
281
|
inputSchema: {
|
|
119
|
-
dir: z
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
282
|
+
dir: z
|
|
283
|
+
.string()
|
|
284
|
+
.describe("Absolute or relative path to the directory containing built HTML files."),
|
|
285
|
+
baseUrl: z
|
|
286
|
+
.string()
|
|
287
|
+
.url()
|
|
288
|
+
.default("http://localhost/")
|
|
289
|
+
.describe("Origin the files will be served from."),
|
|
290
|
+
maxPages: z
|
|
291
|
+
.number()
|
|
292
|
+
.int()
|
|
293
|
+
.min(1)
|
|
294
|
+
.max(5000)
|
|
295
|
+
.default(500)
|
|
296
|
+
.describe("Maximum HTML files to audit."),
|
|
297
|
+
pageDetail: z
|
|
298
|
+
.enum(["summary", "full"])
|
|
299
|
+
.default("summary")
|
|
300
|
+
.describe("summary: per-page scores/counts only; full: every finding on every page."),
|
|
123
301
|
...commonOptions,
|
|
124
302
|
},
|
|
125
|
-
annotations: {
|
|
303
|
+
annotations: {
|
|
304
|
+
readOnlyHint: true,
|
|
305
|
+
openWorldHint: false,
|
|
306
|
+
idempotentHint: true,
|
|
307
|
+
},
|
|
126
308
|
}, async (input) => {
|
|
127
309
|
try {
|
|
128
|
-
const audit = await auditDirectory(input.dir, {
|
|
310
|
+
const audit = await auditDirectory(input.dir, {
|
|
311
|
+
...toOptions(input),
|
|
312
|
+
baseUrl: input.baseUrl,
|
|
313
|
+
maxPages: input.maxPages,
|
|
314
|
+
pageDetail: input.pageDetail,
|
|
315
|
+
});
|
|
129
316
|
return respond(audit, input.format, input.detail);
|
|
130
317
|
}
|
|
131
|
-
catch (
|
|
132
|
-
return fail(
|
|
318
|
+
catch (error) {
|
|
319
|
+
return fail(error);
|
|
133
320
|
}
|
|
134
321
|
});
|
|
135
|
-
server.registerTool(
|
|
136
|
-
title:
|
|
137
|
-
description:
|
|
322
|
+
server.registerTool("seo_lighthouse", {
|
|
323
|
+
title: "Run Lighthouse on a URL",
|
|
324
|
+
description: "Run Google Lighthouse (performance, SEO and agentic-browsing categories) against one URL using a local headless Chrome. Returns Core Web Vitals lab metrics (LCP, CLS, TBT, FCP, Speed Index, TTFB), the top opportunities with estimated savings, failed Lighthouse SEO audits, and findings with thresholds applied. Takes 20-60 seconds. Requires Chrome or Playwright Chromium on this machine.",
|
|
138
325
|
inputSchema: {
|
|
139
|
-
url: z.string().url().describe(
|
|
140
|
-
formFactor: z
|
|
141
|
-
|
|
326
|
+
url: z.string().url().describe("Absolute http(s) URL."),
|
|
327
|
+
formFactor: z
|
|
328
|
+
.enum(["mobile", "desktop"])
|
|
329
|
+
.default("mobile")
|
|
330
|
+
.describe("Device emulation. Mobile is what Google uses for ranking."),
|
|
331
|
+
headers: z
|
|
332
|
+
.record(z.string(), z.string())
|
|
333
|
+
.optional()
|
|
334
|
+
.describe("Extra request headers (e.g. auth for staging)."),
|
|
142
335
|
format,
|
|
143
336
|
},
|
|
144
337
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
145
338
|
}, async (input) => {
|
|
146
339
|
try {
|
|
147
|
-
const result = await runLighthouse(input.url, {
|
|
340
|
+
const result = await runLighthouse(input.url, {
|
|
341
|
+
formFactor: input.formFactor,
|
|
342
|
+
headers: input.headers,
|
|
343
|
+
});
|
|
148
344
|
const findings = performanceFindings(result);
|
|
149
345
|
const payload = { ...result, findings };
|
|
150
|
-
const text = input.format ===
|
|
151
|
-
|
|
346
|
+
const text = input.format === "markdown"
|
|
347
|
+
? lighthouseMarkdown(payload)
|
|
348
|
+
: JSON.stringify(payload, null, 2);
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text", text }],
|
|
351
|
+
structuredContent: payload,
|
|
352
|
+
};
|
|
152
353
|
}
|
|
153
|
-
catch (
|
|
154
|
-
return fail(
|
|
354
|
+
catch (error) {
|
|
355
|
+
return fail(error);
|
|
155
356
|
}
|
|
156
357
|
});
|
|
157
|
-
server.registerTool(
|
|
158
|
-
title:
|
|
159
|
-
description:
|
|
358
|
+
server.registerTool("seo_aeo_audit", {
|
|
359
|
+
title: "Audit a page or site for answer engines (AEO)",
|
|
360
|
+
description: "Answer engine optimization audit: can AI answer engines (ChatGPT search, Claude, Perplexity, Google AI Overviews and AI Mode, Copilot, Gemini) reach, quote and trust the content? " +
|
|
361
|
+
"Checks robots.txt for each AI crawler (search crawlers, answer bots, training bots), Content-Signal lines and the shared `__usage_ai-training` opt-out, /llms.txt, nosnippet/max-snippet/data-nosnippet, Bing's noarchive/nocache for Copilot, noai, content that needs JavaScript, question headings without a direct answer, FAQPage/HowTo markup that the page does not show, article author and dates, and Organization/Person entities with sameAs. " +
|
|
362
|
+
"Returns its own AEO score, a per-bot `crawlers` table and findings with rule ids (aeo.*). " +
|
|
363
|
+
"Provide one of `url` (one page, or a site with `crawl: true`), `html` or `dir` (build output). " +
|
|
364
|
+
"Set `aiBots: true` to also request the page with each AI bot's User-Agent and catch CDN or firewall blocks that robots.txt does not show.",
|
|
160
365
|
inputSchema: {
|
|
161
|
-
|
|
162
|
-
|
|
366
|
+
url: z
|
|
367
|
+
.string()
|
|
368
|
+
.url()
|
|
369
|
+
.optional()
|
|
370
|
+
.describe("Absolute http(s) URL to audit (the start URL with `crawl`)."),
|
|
371
|
+
html: z
|
|
372
|
+
.string()
|
|
373
|
+
.optional()
|
|
374
|
+
.describe("Raw HTML to audit instead of fetching a URL. Page checks only: there is no site to read robots.txt or llms.txt from."),
|
|
375
|
+
dir: z
|
|
376
|
+
.string()
|
|
377
|
+
.optional()
|
|
378
|
+
.describe("Build output directory to audit, with the robots.txt and llms.txt in it."),
|
|
379
|
+
baseUrl: z
|
|
380
|
+
.string()
|
|
381
|
+
.url()
|
|
382
|
+
.optional()
|
|
383
|
+
.describe("With `html` or `dir`: the origin the HTML is served from (default http://localhost/)."),
|
|
384
|
+
crawl: z
|
|
385
|
+
.boolean()
|
|
386
|
+
.default(false)
|
|
387
|
+
.describe("With `url`: crawl internal links and audit every page."),
|
|
388
|
+
aiBots: z
|
|
389
|
+
.boolean()
|
|
390
|
+
.default(false)
|
|
391
|
+
.describe("With `url`: request the page with each AI bot's User-Agent (from this machine, not the bots' IPs) to find CDN or firewall blocks."),
|
|
392
|
+
maxPages: z
|
|
393
|
+
.number()
|
|
394
|
+
.int()
|
|
395
|
+
.min(1)
|
|
396
|
+
.max(5000)
|
|
397
|
+
.optional()
|
|
398
|
+
.describe("Crawl or directory: maximum pages to audit (default 25 for a crawl, 500 for a directory)."),
|
|
399
|
+
maxDepth: z
|
|
400
|
+
.number()
|
|
401
|
+
.int()
|
|
402
|
+
.min(0)
|
|
403
|
+
.max(10)
|
|
404
|
+
.default(3)
|
|
405
|
+
.describe("Crawl: maximum link depth from the start URL."),
|
|
406
|
+
concurrency: z
|
|
407
|
+
.number()
|
|
408
|
+
.int()
|
|
409
|
+
.min(1)
|
|
410
|
+
.max(10)
|
|
411
|
+
.default(4)
|
|
412
|
+
.describe("Crawl: parallel requests."),
|
|
413
|
+
respectRobots: z
|
|
414
|
+
.boolean()
|
|
415
|
+
.default(true)
|
|
416
|
+
.describe("Crawl: obey robots.txt disallow rules."),
|
|
417
|
+
includeSubdomains: z
|
|
418
|
+
.boolean()
|
|
419
|
+
.default(false)
|
|
420
|
+
.describe("Crawl: treat subdomains of the start host as internal."),
|
|
421
|
+
pageDetail: z
|
|
422
|
+
.enum(["summary", "full"])
|
|
423
|
+
.default("summary")
|
|
424
|
+
.describe("Crawl or directory: summary keeps page findings rolled up in `issues`; full lists every page finding in `findings`."),
|
|
425
|
+
includeInfo: commonOptions.includeInfo,
|
|
426
|
+
disableRules: commonOptions.disableRules,
|
|
427
|
+
timeoutMs: commonOptions.timeoutMs,
|
|
428
|
+
userAgent: commonOptions.userAgent,
|
|
429
|
+
headers: commonOptions.headers,
|
|
430
|
+
format,
|
|
431
|
+
detail,
|
|
163
432
|
},
|
|
164
|
-
annotations: { readOnlyHint: true, openWorldHint:
|
|
433
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
165
434
|
}, async (input) => {
|
|
166
|
-
|
|
435
|
+
try {
|
|
436
|
+
const options = {
|
|
437
|
+
...toOptions(input),
|
|
438
|
+
baseUrl: input.baseUrl,
|
|
439
|
+
aiBots: input.aiBots,
|
|
440
|
+
maxPages: input.maxPages ?? (input.dir === undefined ? 25 : 500),
|
|
441
|
+
maxDepth: input.maxDepth,
|
|
442
|
+
concurrency: input.concurrency,
|
|
443
|
+
respectRobots: input.respectRobots,
|
|
444
|
+
includeSubdomains: input.includeSubdomains,
|
|
445
|
+
pageDetail: input.pageDetail,
|
|
446
|
+
};
|
|
447
|
+
const given = [input.url, input.html, input.dir].filter((v) => v !== undefined);
|
|
448
|
+
if (given.length !== 1) {
|
|
449
|
+
throw new Error("Provide exactly one of `url`, `html` or `dir`.");
|
|
450
|
+
}
|
|
451
|
+
let audit;
|
|
452
|
+
if (input.html !== undefined) {
|
|
453
|
+
audit = await auditAeoHtml(input.html, { url: input.baseUrl, target: "inline-html" }, options);
|
|
454
|
+
}
|
|
455
|
+
else if (input.dir !== undefined) {
|
|
456
|
+
audit = await auditAeoDirectory(input.dir, options);
|
|
457
|
+
}
|
|
458
|
+
else if (input.url && input.crawl) {
|
|
459
|
+
audit = await auditAeoSite(input.url, options);
|
|
460
|
+
}
|
|
461
|
+
else if (input.url) {
|
|
462
|
+
audit = await auditAeoUrl(input.url, options);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
throw new Error("Provide `url`, `html` or `dir`.");
|
|
466
|
+
}
|
|
467
|
+
return respondAeo(audit, input.format, input.detail);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
return fail(error);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
server.registerTool("seo_list_rules", {
|
|
474
|
+
title: "List SEO rules",
|
|
475
|
+
description: "List every rule this tool can report, with id, category, typical severity, what it checks and the generic fix. Filter by category or look up one rule id to understand a finding.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
category: z
|
|
478
|
+
.string()
|
|
479
|
+
.optional()
|
|
480
|
+
.describe("Filter by category (title, meta, headings, canonical, indexing, document, social, images, links, international, structured-data, content, keyword, response, rendering, performance, site, aeo)."),
|
|
481
|
+
rule: z.string().optional().describe("Return only this rule id."),
|
|
482
|
+
},
|
|
483
|
+
annotations: {
|
|
484
|
+
readOnlyHint: true,
|
|
485
|
+
openWorldHint: false,
|
|
486
|
+
idempotentHint: true,
|
|
487
|
+
},
|
|
488
|
+
}, (input) => {
|
|
489
|
+
const rules = ALL_RULES.filter((r) => (!input.category || r.category === input.category) &&
|
|
490
|
+
(!input.rule || r.id === input.rule));
|
|
167
491
|
const payload = { count: rules.length, rules };
|
|
168
|
-
return {
|
|
492
|
+
return {
|
|
493
|
+
content: [
|
|
494
|
+
{ type: "text", text: JSON.stringify(payload, null, 2) },
|
|
495
|
+
],
|
|
496
|
+
structuredContent: payload,
|
|
497
|
+
};
|
|
169
498
|
});
|
|
170
499
|
return server;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const lines = [`# Lighthouse (${payload.formFactor}): ${payload.finalUrl}`, '', '| Metric | Value |', '| --- | --- |'];
|
|
174
|
-
for (const [k, v] of Object.entries(payload.metrics)) {
|
|
175
|
-
lines.push(`| ${k} | ${v === null ? '—' : k.endsWith('Ms') ? `${Math.round(v)} ms` : v} |`);
|
|
176
|
-
}
|
|
177
|
-
lines.push('', '## Opportunities', '');
|
|
178
|
-
for (const o of payload.opportunities.slice(0, 10))
|
|
179
|
-
lines.push(`- ${o.title}${o.savingsMs ? ` (~${Math.round(o.savingsMs)} ms)` : ''}${o.savingsBytes ? ` (~${Math.round(o.savingsBytes / 1024)} KiB)` : ''}`);
|
|
180
|
-
lines.push('', '## Findings', '');
|
|
181
|
-
for (const f of payload.findings)
|
|
182
|
-
lines.push(`- **\`${f.rule}\`** ${f.message}`, ` - Fix: ${f.recommendation}`);
|
|
183
|
-
return lines.join('\n');
|
|
184
|
-
}
|
|
185
|
-
export async function startMcpServer() {
|
|
500
|
+
};
|
|
501
|
+
export const startMcpServer = async () => {
|
|
186
502
|
const server = createMcpServer();
|
|
187
503
|
const transport = new StdioServerTransport();
|
|
188
504
|
await server.connect(transport);
|
|
189
|
-
}
|
|
505
|
+
};
|
|
190
506
|
//# sourceMappingURL=mcp.js.map
|