@rog0x/mcp-seo-tools 1.0.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.
Files changed (38) hide show
  1. package/README.md +105 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +175 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/tools/heading-checker.d.ts +15 -0
  7. package/dist/tools/heading-checker.d.ts.map +1 -0
  8. package/dist/tools/heading-checker.js +123 -0
  9. package/dist/tools/heading-checker.js.map +1 -0
  10. package/dist/tools/keyword-density.d.ts +22 -0
  11. package/dist/tools/keyword-density.d.ts.map +1 -0
  12. package/dist/tools/keyword-density.js +176 -0
  13. package/dist/tools/keyword-density.js.map +1 -0
  14. package/dist/tools/link-checker.d.ts +22 -0
  15. package/dist/tools/link-checker.d.ts.map +1 -0
  16. package/dist/tools/link-checker.js +171 -0
  17. package/dist/tools/link-checker.js.map +1 -0
  18. package/dist/tools/meta-analyzer.d.ts +27 -0
  19. package/dist/tools/meta-analyzer.d.ts.map +1 -0
  20. package/dist/tools/meta-analyzer.js +161 -0
  21. package/dist/tools/meta-analyzer.js.map +1 -0
  22. package/dist/tools/page-speed.d.ts +31 -0
  23. package/dist/tools/page-speed.d.ts.map +1 -0
  24. package/dist/tools/page-speed.js +180 -0
  25. package/dist/tools/page-speed.js.map +1 -0
  26. package/dist/tools/sitemap-parser.d.ts +29 -0
  27. package/dist/tools/sitemap-parser.d.ts.map +1 -0
  28. package/dist/tools/sitemap-parser.js +224 -0
  29. package/dist/tools/sitemap-parser.js.map +1 -0
  30. package/package.json +24 -0
  31. package/src/index.ts +199 -0
  32. package/src/tools/heading-checker.ts +109 -0
  33. package/src/tools/keyword-density.ts +180 -0
  34. package/src/tools/link-checker.ts +163 -0
  35. package/src/tools/meta-analyzer.ts +148 -0
  36. package/src/tools/page-speed.ts +190 -0
  37. package/src/tools/sitemap-parser.ts +230 -0
  38. package/tsconfig.json +19 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-speed.js","sourceRoot":"","sources":["../../src/tools/page-speed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface SitemapUrl {
2
+ loc: string;
3
+ lastmod: string | null;
4
+ changefreq: string | null;
5
+ priority: string | null;
6
+ }
7
+ export interface SitemapIndex {
8
+ loc: string;
9
+ lastmod: string | null;
10
+ }
11
+ export interface SitemapAnalysis {
12
+ url: string;
13
+ type: "urlset" | "sitemapindex" | "not_found";
14
+ urls: SitemapUrl[];
15
+ sitemapIndexEntries: SitemapIndex[];
16
+ totalUrls: number;
17
+ stats: {
18
+ withLastmod: number;
19
+ withChangefreq: number;
20
+ withPriority: number;
21
+ uniqueHosts: string[];
22
+ oldestLastmod: string | null;
23
+ newestLastmod: string | null;
24
+ };
25
+ issues: string[];
26
+ recommendations: string[];
27
+ }
28
+ export declare function parseSitemap(url: string): Promise<SitemapAnalysis>;
29
+ //# sourceMappingURL=sitemap-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-parser.d.ts","sourceRoot":"","sources":["../../src/tools/sitemap-parser.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,GAAG,cAAc,GAAG,WAAW,CAAC;IAC9C,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QACL,WAAW,EAAE,MAAM,CAAC;QACpB,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,EAAE,CAAC;QACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9B,CAAC;IACF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AA0BD,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CA6KxE"}
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseSitemap = parseSitemap;
37
+ const cheerio = __importStar(require("cheerio"));
38
+ async function fetchSitemap(url) {
39
+ const response = await fetch(url, {
40
+ headers: { "User-Agent": "MCPSEOTools/1.0 (Sitemap Parser)" },
41
+ redirect: "follow",
42
+ signal: AbortSignal.timeout(20000),
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`HTTP ${response.status} fetching sitemap at ${url}`);
46
+ }
47
+ return response.text();
48
+ }
49
+ function resolveSitemapUrl(pageUrl) {
50
+ try {
51
+ const parsed = new URL(pageUrl);
52
+ // If the URL already ends with sitemap.xml or similar, use it directly
53
+ if (parsed.pathname.includes("sitemap"))
54
+ return pageUrl;
55
+ // Otherwise, try the standard location
56
+ return `${parsed.protocol}//${parsed.host}/sitemap.xml`;
57
+ }
58
+ catch {
59
+ return pageUrl;
60
+ }
61
+ }
62
+ async function parseSitemap(url) {
63
+ const sitemapUrl = resolveSitemapUrl(url);
64
+ let xml;
65
+ try {
66
+ xml = await fetchSitemap(sitemapUrl);
67
+ }
68
+ catch (err) {
69
+ // Try robots.txt for sitemap location
70
+ try {
71
+ const robotsUrl = `${new URL(sitemapUrl).protocol}//${new URL(sitemapUrl).host}/robots.txt`;
72
+ const robotsResp = await fetch(robotsUrl, {
73
+ headers: { "User-Agent": "MCPSEOTools/1.0 (Sitemap Parser)" },
74
+ signal: AbortSignal.timeout(10000),
75
+ });
76
+ const robotsTxt = await robotsResp.text();
77
+ const sitemapMatch = robotsTxt.match(/Sitemap:\s*(.+)/i);
78
+ if (sitemapMatch) {
79
+ xml = await fetchSitemap(sitemapMatch[1].trim());
80
+ }
81
+ else {
82
+ return {
83
+ url: sitemapUrl,
84
+ type: "not_found",
85
+ urls: [],
86
+ sitemapIndexEntries: [],
87
+ totalUrls: 0,
88
+ stats: { withLastmod: 0, withChangefreq: 0, withPriority: 0, uniqueHosts: [], oldestLastmod: null, newestLastmod: null },
89
+ issues: [`No sitemap found at ${sitemapUrl} and no Sitemap directive in robots.txt.`],
90
+ recommendations: ["Create a sitemap.xml and submit it to search engines via Google Search Console."],
91
+ };
92
+ }
93
+ }
94
+ catch {
95
+ return {
96
+ url: sitemapUrl,
97
+ type: "not_found",
98
+ urls: [],
99
+ sitemapIndexEntries: [],
100
+ totalUrls: 0,
101
+ stats: { withLastmod: 0, withChangefreq: 0, withPriority: 0, uniqueHosts: [], oldestLastmod: null, newestLastmod: null },
102
+ issues: [`Failed to fetch sitemap: ${err.message}`],
103
+ recommendations: ["Ensure a sitemap.xml exists at the root of your domain."],
104
+ };
105
+ }
106
+ }
107
+ const $ = cheerio.load(xml, { xml: true });
108
+ const issues = [];
109
+ const recommendations = [];
110
+ // Check if it's a sitemap index
111
+ const sitemapIndexEntries = [];
112
+ $("sitemapindex > sitemap").each((_, el) => {
113
+ sitemapIndexEntries.push({
114
+ loc: $(el).find("loc").text().trim(),
115
+ lastmod: $(el).find("lastmod").text().trim() || null,
116
+ });
117
+ });
118
+ if (sitemapIndexEntries.length > 0) {
119
+ if (sitemapIndexEntries.length > 500) {
120
+ issues.push(`Sitemap index has ${sitemapIndexEntries.length} entries. Google supports up to 500 sitemaps per index.`);
121
+ }
122
+ const withoutLastmod = sitemapIndexEntries.filter((s) => !s.lastmod).length;
123
+ if (withoutLastmod > 0) {
124
+ recommendations.push(`${withoutLastmod} sitemap index entries lack lastmod dates. Adding them helps search engines prioritize crawling.`);
125
+ }
126
+ return {
127
+ url: sitemapUrl,
128
+ type: "sitemapindex",
129
+ urls: [],
130
+ sitemapIndexEntries,
131
+ totalUrls: sitemapIndexEntries.length,
132
+ stats: { withLastmod: sitemapIndexEntries.length - (sitemapIndexEntries.filter((s) => !s.lastmod).length), withChangefreq: 0, withPriority: 0, uniqueHosts: [], oldestLastmod: null, newestLastmod: null },
133
+ issues,
134
+ recommendations,
135
+ };
136
+ }
137
+ // Parse URL set
138
+ const urls = [];
139
+ $("urlset > url").each((_, el) => {
140
+ urls.push({
141
+ loc: $(el).find("loc").text().trim(),
142
+ lastmod: $(el).find("lastmod").text().trim() || null,
143
+ changefreq: $(el).find("changefreq").text().trim() || null,
144
+ priority: $(el).find("priority").text().trim() || null,
145
+ });
146
+ });
147
+ // Stats
148
+ const withLastmod = urls.filter((u) => u.lastmod).length;
149
+ const withChangefreq = urls.filter((u) => u.changefreq).length;
150
+ const withPriority = urls.filter((u) => u.priority).length;
151
+ const hosts = new Set();
152
+ for (const u of urls) {
153
+ try {
154
+ hosts.add(new URL(u.loc).hostname);
155
+ }
156
+ catch { /* skip */ }
157
+ }
158
+ const lastmods = urls
159
+ .map((u) => u.lastmod)
160
+ .filter((d) => d !== null)
161
+ .sort();
162
+ const oldestLastmod = lastmods.length > 0 ? lastmods[0] : null;
163
+ const newestLastmod = lastmods.length > 0 ? lastmods[lastmods.length - 1] : null;
164
+ // Analysis
165
+ if (urls.length === 0) {
166
+ issues.push("Sitemap contains no URLs.");
167
+ }
168
+ if (urls.length > 50000) {
169
+ issues.push(`Sitemap has ${urls.length} URLs. Maximum allowed per sitemap file is 50,000. Split into multiple sitemaps.`);
170
+ }
171
+ if (withLastmod === 0 && urls.length > 0) {
172
+ recommendations.push("No lastmod dates found. Adding lastmod helps search engines identify updated content.");
173
+ }
174
+ else if (withLastmod < urls.length * 0.5) {
175
+ recommendations.push(`Only ${withLastmod} of ${urls.length} URLs have lastmod dates. Add dates to all entries.`);
176
+ }
177
+ // Check for stale lastmod
178
+ if (newestLastmod) {
179
+ const newestDate = new Date(newestLastmod);
180
+ const sixMonthsAgo = new Date();
181
+ sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
182
+ if (newestDate < sixMonthsAgo) {
183
+ issues.push(`Most recent lastmod is ${newestLastmod}. The sitemap appears outdated. Update it regularly.`);
184
+ }
185
+ }
186
+ // Check for duplicate URLs
187
+ const locSet = new Set();
188
+ let duplicates = 0;
189
+ for (const u of urls) {
190
+ if (locSet.has(u.loc))
191
+ duplicates++;
192
+ locSet.add(u.loc);
193
+ }
194
+ if (duplicates > 0) {
195
+ issues.push(`${duplicates} duplicate URL(s) found. Remove duplicates to avoid crawl budget waste.`);
196
+ }
197
+ // Check for non-canonical patterns
198
+ const mixedProtocol = urls.some((u) => u.loc.startsWith("http://")) && urls.some((u) => u.loc.startsWith("https://"));
199
+ if (mixedProtocol) {
200
+ issues.push("Sitemap contains both HTTP and HTTPS URLs. Use only HTTPS URLs.");
201
+ }
202
+ const mixedTrailingSlash = urls.some((u) => u.loc.endsWith("/")) && urls.some((u) => !u.loc.endsWith("/") && !u.loc.match(/\.\w{2,5}$/));
203
+ if (mixedTrailingSlash) {
204
+ recommendations.push("Inconsistent trailing slashes in URLs. Standardize to one pattern for cleaner crawling.");
205
+ }
206
+ return {
207
+ url: sitemapUrl,
208
+ type: "urlset",
209
+ urls: urls.slice(0, 200), // Cap output to avoid overwhelming responses
210
+ sitemapIndexEntries: [],
211
+ totalUrls: urls.length,
212
+ stats: {
213
+ withLastmod,
214
+ withChangefreq,
215
+ withPriority,
216
+ uniqueHosts: [...hosts],
217
+ oldestLastmod,
218
+ newestLastmod,
219
+ },
220
+ issues,
221
+ recommendations,
222
+ };
223
+ }
224
+ //# sourceMappingURL=sitemap-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-parser.js","sourceRoot":"","sources":["../../src/tools/sitemap-parser.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@rog0x/mcp-seo-tools",
3
+ "version": "1.0.0",
4
+ "description": "MCP server providing SEO analysis tools for AI agents",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "mcp-seo-tools": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.12.1",
17
+ "cheerio": "^1.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.15.0",
21
+ "typescript": "^5.8.3"
22
+ },
23
+ "license": "MIT"
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { analyzeMeta } from "./tools/meta-analyzer.js";
10
+ import { checkHeadings } from "./tools/heading-checker.js";
11
+ import { checkLinks } from "./tools/link-checker.js";
12
+ import { analyzeKeywordDensity } from "./tools/keyword-density.js";
13
+ import { analyzePageSpeed } from "./tools/page-speed.js";
14
+ import { parseSitemap } from "./tools/sitemap-parser.js";
15
+
16
+ const server = new Server(
17
+ {
18
+ name: "mcp-seo-tools",
19
+ version: "1.0.0",
20
+ },
21
+ {
22
+ capabilities: {
23
+ tools: {},
24
+ },
25
+ }
26
+ );
27
+
28
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29
+ tools: [
30
+ {
31
+ name: "seo_meta_analyze",
32
+ description:
33
+ "Analyze a page's meta tags, Open Graph tags, and Twitter Card tags. Returns an SEO score with actionable recommendations for title, description, social sharing tags, and other meta elements.",
34
+ inputSchema: {
35
+ type: "object" as const,
36
+ properties: {
37
+ url: {
38
+ type: "string",
39
+ description: "Full URL of the page to analyze (e.g. https://example.com)",
40
+ },
41
+ },
42
+ required: ["url"],
43
+ },
44
+ },
45
+ {
46
+ name: "seo_heading_check",
47
+ description:
48
+ "Audit the heading hierarchy (H1-H6) of a page. Detects missing H1, skipped heading levels, duplicate headings, empty headings, and provides a visual hierarchy tree.",
49
+ inputSchema: {
50
+ type: "object" as const,
51
+ properties: {
52
+ url: {
53
+ type: "string",
54
+ description: "Full URL of the page to audit",
55
+ },
56
+ },
57
+ required: ["url"],
58
+ },
59
+ },
60
+ {
61
+ name: "seo_link_check",
62
+ description:
63
+ "Crawl a page and check all links for broken URLs, redirects, and missing anchor text. Tests up to 50 links by default with concurrent requests.",
64
+ inputSchema: {
65
+ type: "object" as const,
66
+ properties: {
67
+ url: {
68
+ type: "string",
69
+ description: "Full URL of the page to scan for links",
70
+ },
71
+ max_links: {
72
+ type: "number",
73
+ description: "Maximum number of links to check (default: 50, max: 200)",
74
+ },
75
+ },
76
+ required: ["url"],
77
+ },
78
+ },
79
+ {
80
+ name: "seo_keyword_density",
81
+ description:
82
+ "Analyze keyword density and distribution across a page. Returns top single words, two-word and three-word phrases with density percentages. Optionally checks placement of a target keyword in title, H1, meta description, and first paragraph.",
83
+ inputSchema: {
84
+ type: "object" as const,
85
+ properties: {
86
+ url: {
87
+ type: "string",
88
+ description: "Full URL of the page to analyze",
89
+ },
90
+ target_keyword: {
91
+ type: "string",
92
+ description: "Optional target keyword to check placement and density for",
93
+ },
94
+ },
95
+ required: ["url"],
96
+ },
97
+ },
98
+ {
99
+ name: "seo_page_speed",
100
+ description:
101
+ "Measure basic page speed metrics including Time to First Byte, total load time, HTML size, and resource counts (scripts, stylesheets, images). Identifies render-blocking resources and missing optimizations.",
102
+ inputSchema: {
103
+ type: "object" as const,
104
+ properties: {
105
+ url: {
106
+ type: "string",
107
+ description: "Full URL of the page to measure",
108
+ },
109
+ },
110
+ required: ["url"],
111
+ },
112
+ },
113
+ {
114
+ name: "seo_sitemap_parse",
115
+ description:
116
+ "Parse and analyze a website's sitemap.xml. Handles both sitemap indexes and URL sets. Reports URL counts, lastmod freshness, duplicate URLs, protocol consistency, and sitemap compliance issues. Automatically tries /sitemap.xml and falls back to robots.txt.",
117
+ inputSchema: {
118
+ type: "object" as const,
119
+ properties: {
120
+ url: {
121
+ type: "string",
122
+ description: "URL of the sitemap or any page on the site (will try /sitemap.xml automatically)",
123
+ },
124
+ },
125
+ required: ["url"],
126
+ },
127
+ },
128
+ ],
129
+ }));
130
+
131
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
132
+ const { name, arguments: args } = request.params;
133
+
134
+ try {
135
+ switch (name) {
136
+ case "seo_meta_analyze": {
137
+ const result = await analyzeMeta(args?.url as string);
138
+ return {
139
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
140
+ };
141
+ }
142
+
143
+ case "seo_heading_check": {
144
+ const result = await checkHeadings(args?.url as string);
145
+ return {
146
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
147
+ };
148
+ }
149
+
150
+ case "seo_link_check": {
151
+ const maxLinks = Math.min((args?.max_links as number) || 50, 200);
152
+ const result = await checkLinks(args?.url as string, maxLinks);
153
+ return {
154
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
155
+ };
156
+ }
157
+
158
+ case "seo_keyword_density": {
159
+ const result = await analyzeKeywordDensity(
160
+ args?.url as string,
161
+ args?.target_keyword as string | undefined
162
+ );
163
+ return {
164
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
165
+ };
166
+ }
167
+
168
+ case "seo_page_speed": {
169
+ const result = await analyzePageSpeed(args?.url as string);
170
+ return {
171
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
172
+ };
173
+ }
174
+
175
+ case "seo_sitemap_parse": {
176
+ const result = await parseSitemap(args?.url as string);
177
+ return {
178
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
179
+ };
180
+ }
181
+
182
+ default:
183
+ throw new Error(`Unknown tool: ${name}`);
184
+ }
185
+ } catch (error: any) {
186
+ return {
187
+ content: [{ type: "text", text: `Error: ${error.message}` }],
188
+ isError: true,
189
+ };
190
+ }
191
+ });
192
+
193
+ async function main() {
194
+ const transport = new StdioServerTransport();
195
+ await server.connect(transport);
196
+ console.error("MCP SEO Tools server running on stdio");
197
+ }
198
+
199
+ main().catch(console.error);
@@ -0,0 +1,109 @@
1
+ import * as cheerio from "cheerio";
2
+
3
+ export interface HeadingInfo {
4
+ level: number;
5
+ text: string;
6
+ id: string | null;
7
+ }
8
+
9
+ export interface HeadingAnalysis {
10
+ url: string;
11
+ headings: HeadingInfo[];
12
+ hierarchy: string;
13
+ counts: Record<string, number>;
14
+ issues: string[];
15
+ score: number;
16
+ }
17
+
18
+ export async function checkHeadings(url: string): Promise<HeadingAnalysis> {
19
+ const response = await fetch(url, {
20
+ headers: { "User-Agent": "MCPSEOTools/1.0 (SEO Analyzer)" },
21
+ redirect: "follow",
22
+ signal: AbortSignal.timeout(15000),
23
+ });
24
+ const html = await response.text();
25
+ const $ = cheerio.load(html);
26
+
27
+ const headings: HeadingInfo[] = [];
28
+ const counts: Record<string, number> = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
29
+
30
+ $("h1, h2, h3, h4, h5, h6").each((_, el) => {
31
+ const tag = $(el).prop("tagName")?.toLowerCase() || "";
32
+ const level = parseInt(tag.replace("h", ""), 10);
33
+ const text = $(el).text().trim().replace(/\s+/g, " ");
34
+ const id = $(el).attr("id") || null;
35
+ headings.push({ level, text, id });
36
+ if (counts[tag] !== undefined) counts[tag]++;
37
+ });
38
+
39
+ const issues: string[] = [];
40
+
41
+ // Check for missing H1
42
+ if (counts.h1 === 0) {
43
+ issues.push("No H1 tag found. Every page should have exactly one H1 as the main heading.");
44
+ } else if (counts.h1 > 1) {
45
+ issues.push(`Found ${counts.h1} H1 tags. Use only one H1 per page for optimal SEO.`);
46
+ }
47
+
48
+ // Check if first heading is H1
49
+ if (headings.length > 0 && headings[0].level !== 1) {
50
+ issues.push(`First heading is H${headings[0].level} instead of H1. The first heading should be H1.`);
51
+ }
52
+
53
+ // Check for skipped levels
54
+ let previousLevel = 0;
55
+ for (const heading of headings) {
56
+ if (heading.level > previousLevel + 1 && previousLevel > 0) {
57
+ issues.push(`Heading level skipped: H${previousLevel} to H${heading.level} ("${heading.text.substring(0, 50)}"). Avoid skipping heading levels.`);
58
+ }
59
+ previousLevel = heading.level;
60
+ }
61
+
62
+ // Check for empty headings
63
+ const emptyHeadings = headings.filter((h) => h.text.length === 0);
64
+ if (emptyHeadings.length > 0) {
65
+ issues.push(`Found ${emptyHeadings.length} empty heading(s). All headings should contain descriptive text.`);
66
+ }
67
+
68
+ // Check for very long headings
69
+ const longHeadings = headings.filter((h) => h.text.length > 70);
70
+ for (const h of longHeadings) {
71
+ issues.push(`H${h.level} exceeds 70 characters (${h.text.length} chars): "${h.text.substring(0, 60)}..."`);
72
+ }
73
+
74
+ // Check for duplicate headings
75
+ const seen = new Map<string, number>();
76
+ for (const h of headings) {
77
+ const key = `h${h.level}:${h.text.toLowerCase()}`;
78
+ seen.set(key, (seen.get(key) || 0) + 1);
79
+ }
80
+ for (const [key, count] of seen) {
81
+ if (count > 1) {
82
+ const [tag, ...textParts] = key.split(":");
83
+ issues.push(`Duplicate ${tag.toUpperCase()}: "${textParts.join(":")}" appears ${count} times.`);
84
+ }
85
+ }
86
+
87
+ // Build visual hierarchy
88
+ const hierarchy = headings
89
+ .map((h) => `${" ".repeat(h.level - 1)}H${h.level}: ${h.text.substring(0, 80)}`)
90
+ .join("\n");
91
+
92
+ // Score calculation
93
+ let score = 100;
94
+ if (counts.h1 === 0) score -= 25;
95
+ else if (counts.h1 > 1) score -= 15;
96
+ if (headings.length > 0 && headings[0].level !== 1) score -= 10;
97
+ score -= Math.min(issues.filter((i) => i.includes("skipped")).length * 10, 20);
98
+ score -= emptyHeadings.length * 5;
99
+ score -= longHeadings.length * 3;
100
+
101
+ return {
102
+ url,
103
+ headings,
104
+ hierarchy,
105
+ counts,
106
+ issues,
107
+ score: Math.max(0, score),
108
+ };
109
+ }