pagesight 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +8 -18
- package/src/lib/psi.ts +4 -0
- package/src/lib/sitemap.ts +106 -0
- package/src/tools/{robots.ts → ai.ts} +3 -3
- package/src/tools/audit.ts +13 -11
- package/src/tools/{metatags.ts → page.ts} +480 -131
- package/src/tools/search.ts +685 -0
- package/src/tools/{pagespeed.ts → speed.ts} +382 -54
- package/src/tools/crux.ts +0 -296
- package/src/tools/inspect.ts +0 -121
- package/src/tools/links.ts +0 -183
- package/src/tools/performance.ts +0 -325
- package/src/tools/sample-inspect.ts +0 -284
- package/src/tools/sitemaps.ts +0 -112
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerAiTool } from "./tools/ai.js";
|
|
4
5
|
import { registerAuditTool } from "./tools/audit.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { registerLinksTool } from "./tools/links.js";
|
|
8
|
-
import { registerMetatagsTool } from "./tools/metatags.js";
|
|
9
|
-
import { registerPagespeedTool } from "./tools/pagespeed.js";
|
|
10
|
-
import { registerPerformanceTool } from "./tools/performance.js";
|
|
11
|
-
import { registerRobotsTool } from "./tools/robots.js";
|
|
12
|
-
import { registerSampleInspectTool } from "./tools/sample-inspect.js";
|
|
6
|
+
import { registerPageTool } from "./tools/page.js";
|
|
7
|
+
import { registerSearchTool } from "./tools/search.js";
|
|
13
8
|
import { registerSetupTool } from "./tools/setup.js";
|
|
14
|
-
import {
|
|
9
|
+
import { registerSpeedTool } from "./tools/speed.js";
|
|
15
10
|
|
|
16
11
|
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
17
12
|
|
|
@@ -20,17 +15,12 @@ const server = new McpServer({
|
|
|
20
15
|
version: pkg.version,
|
|
21
16
|
});
|
|
22
17
|
|
|
18
|
+
registerAiTool(server);
|
|
23
19
|
registerAuditTool(server);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
registerLinksTool(server);
|
|
27
|
-
registerMetatagsTool(server);
|
|
28
|
-
registerPagespeedTool(server);
|
|
29
|
-
registerPerformanceTool(server);
|
|
30
|
-
registerRobotsTool(server);
|
|
31
|
-
registerSampleInspectTool(server);
|
|
32
|
-
registerSitemapsTool(server);
|
|
20
|
+
registerPageTool(server);
|
|
21
|
+
registerSearchTool(server);
|
|
33
22
|
registerSetupTool(server);
|
|
23
|
+
registerSpeedTool(server);
|
|
34
24
|
|
|
35
25
|
const transport = new StdioServerTransport();
|
|
36
26
|
await server.connect(transport);
|
package/src/lib/psi.ts
CHANGED
|
@@ -79,6 +79,10 @@ export interface PsiResult {
|
|
|
79
79
|
export type PsiStrategy = "mobile" | "desktop";
|
|
80
80
|
export type PsiCategoryType = "performance" | "accessibility" | "best-practices" | "seo";
|
|
81
81
|
|
|
82
|
+
export function hasApiKey(): boolean {
|
|
83
|
+
return !!process.env.GOOGLE_API_KEY;
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
export async function runPagespeed(
|
|
83
87
|
url: string,
|
|
84
88
|
options?: {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { inspectUrl } from "./gsc.js";
|
|
2
|
+
|
|
3
|
+
export interface SitemapParseResult {
|
|
4
|
+
urls: string[];
|
|
5
|
+
isSitemapIndex: boolean;
|
|
6
|
+
childSitemaps: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseSitemapXml(xml: string): SitemapParseResult {
|
|
10
|
+
const urls: string[] = [];
|
|
11
|
+
const childSitemaps: string[] = [];
|
|
12
|
+
|
|
13
|
+
const isSitemapIndex = /<sitemapindex/i.test(xml);
|
|
14
|
+
|
|
15
|
+
if (isSitemapIndex) {
|
|
16
|
+
for (const m of xml.matchAll(/<sitemap[^>]*>[\s\S]*?<loc>\s*(.*?)\s*<\/loc>[\s\S]*?<\/sitemap>/gi)) {
|
|
17
|
+
childSitemaps.push(m[1].trim());
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
for (const m of xml.matchAll(/<url[^>]*>[\s\S]*?<loc>\s*(.*?)\s*<\/loc>[\s\S]*?<\/url>/gi)) {
|
|
21
|
+
urls.push(m[1].trim());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { urls, isSitemapIndex, childSitemaps };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchSitemap(sitemapUrl: string): Promise<SitemapParseResult> {
|
|
29
|
+
const res = await fetch(sitemapUrl, {
|
|
30
|
+
headers: { "User-Agent": "Pagesight/1.0" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`Failed to fetch sitemap ${sitemapUrl}: HTTP ${res.status}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const xml = await res.text();
|
|
38
|
+
return parseSitemapXml(xml);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sampleUrls(urls: string[], count: number, strategy: string): string[] {
|
|
42
|
+
if (urls.length <= count) return [...urls];
|
|
43
|
+
|
|
44
|
+
if (strategy === "first") {
|
|
45
|
+
return urls.slice(0, count);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (strategy === "spread") {
|
|
49
|
+
const step = Math.floor(urls.length / count);
|
|
50
|
+
const sampled: string[] = [];
|
|
51
|
+
for (let i = 0; i < count; i++) {
|
|
52
|
+
sampled.push(urls[i * step]);
|
|
53
|
+
}
|
|
54
|
+
return sampled;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// random (default)
|
|
58
|
+
const shuffled = [...urls];
|
|
59
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
60
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
61
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
62
|
+
}
|
|
63
|
+
return shuffled.slice(0, count);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface InspectionSummary {
|
|
67
|
+
url: string;
|
|
68
|
+
verdict: string;
|
|
69
|
+
coverageState: string;
|
|
70
|
+
pageFetchState: string;
|
|
71
|
+
robotsTxtState: string;
|
|
72
|
+
indexingState: string;
|
|
73
|
+
lastCrawlTime: string | null;
|
|
74
|
+
googleCanonical: string | null;
|
|
75
|
+
error: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function inspectSingle(url: string, siteUrl: string): Promise<InspectionSummary> {
|
|
79
|
+
try {
|
|
80
|
+
const r = await inspectUrl(url, siteUrl);
|
|
81
|
+
const idx = r.indexStatusResult;
|
|
82
|
+
return {
|
|
83
|
+
url,
|
|
84
|
+
verdict: idx.verdict,
|
|
85
|
+
coverageState: idx.coverageState,
|
|
86
|
+
pageFetchState: idx.pageFetchState,
|
|
87
|
+
robotsTxtState: idx.robotsTxtState,
|
|
88
|
+
indexingState: idx.indexingState,
|
|
89
|
+
lastCrawlTime: idx.lastCrawlTime ?? null,
|
|
90
|
+
googleCanonical: idx.googleCanonical ?? null,
|
|
91
|
+
error: null,
|
|
92
|
+
};
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return {
|
|
95
|
+
url,
|
|
96
|
+
verdict: "ERROR",
|
|
97
|
+
coverageState: "ERROR",
|
|
98
|
+
pageFetchState: "ERROR",
|
|
99
|
+
robotsTxtState: "ERROR",
|
|
100
|
+
indexingState: "ERROR",
|
|
101
|
+
lastCrawlTime: null,
|
|
102
|
+
googleCanonical: null,
|
|
103
|
+
error: err instanceof Error ? err.message : String(err),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -117,10 +117,10 @@ function formatRobotsAudit(origin: string, robots: RobotsTxt, statusCode: number
|
|
|
117
117
|
return lines.join("\n");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export function
|
|
120
|
+
export function registerAiTool(server: McpServer): void {
|
|
121
121
|
server.tool(
|
|
122
|
-
"
|
|
123
|
-
"
|
|
122
|
+
"ai",
|
|
123
|
+
"Analyze your site's AI visibility. Fetches robots.txt, audits which AI crawlers (training, search, assistant, agent) can access your content, validates syntax per RFC 9309, and checks specific paths. Shows how AI systems see your site.",
|
|
124
124
|
{
|
|
125
125
|
url: z
|
|
126
126
|
.string()
|
package/src/tools/audit.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { inspectUrl, listSitemaps } from "../lib/gsc.js";
|
|
4
4
|
import { type PsiCategoryType, type PsiResult, runPagespeed } from "../lib/psi.js";
|
|
5
5
|
import { auditAiCrawlers, fetchRobotsTxt, isAllowed } from "../lib/robots.js";
|
|
6
|
-
import { fetchSitemap, inspectSingle, sampleUrls } from "
|
|
6
|
+
import { fetchSitemap, inspectSingle, sampleUrls } from "../lib/sitemap.js";
|
|
7
7
|
|
|
8
8
|
type Severity = "HIGH" | "MEDIUM" | "LOW";
|
|
9
9
|
|
|
@@ -252,26 +252,28 @@ function addInspectFindings(verdict: string, coverageState: string, findings: Fi
|
|
|
252
252
|
function formatAudit(url: string, findings: Finding[], errors: string[]): string {
|
|
253
253
|
const lines: string[] = [`=== Site Audit: ${url} ===`, ""];
|
|
254
254
|
|
|
255
|
+
// Lead with errors so partial results are obvious
|
|
256
|
+
if (errors.length > 0) {
|
|
257
|
+
lines.push(`${errors.length} check${errors.length > 1 ? "s" : ""} failed — results below are partial:`, "");
|
|
258
|
+
for (const e of errors) {
|
|
259
|
+
lines.push(` FAIL ${e}`);
|
|
260
|
+
}
|
|
261
|
+
lines.push("");
|
|
262
|
+
}
|
|
263
|
+
|
|
255
264
|
// Sort: HIGH first, then MEDIUM, then LOW
|
|
256
265
|
const order: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
257
266
|
findings.sort((a, b) => order[a.severity] - order[b.severity]);
|
|
258
267
|
|
|
259
|
-
if (findings.length === 0) {
|
|
268
|
+
if (findings.length === 0 && errors.length === 0) {
|
|
260
269
|
lines.push("No issues found.");
|
|
261
|
-
} else {
|
|
262
|
-
lines.push(`${findings.length} findings:`, "");
|
|
270
|
+
} else if (findings.length > 0) {
|
|
271
|
+
lines.push(`${findings.length} finding${findings.length > 1 ? "s" : ""}:`, "");
|
|
263
272
|
for (const f of findings) {
|
|
264
273
|
lines.push(`${f.severity.padEnd(6)} ${f.message}`);
|
|
265
274
|
}
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
if (errors.length > 0) {
|
|
269
|
-
lines.push("", "--- Errors (partial audit) ---", "");
|
|
270
|
-
for (const e of errors) {
|
|
271
|
-
lines.push(`- ${e}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
277
|
return lines.join("\n");
|
|
276
278
|
}
|
|
277
279
|
|