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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagesight",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "See your site the way search engines and AI see it.",
5
5
  "keywords": [
6
6
  "seo",
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 { registerCruxTool } from "./tools/crux.js";
6
- import { registerInspectTool } from "./tools/inspect.js";
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 { registerSitemapsTool } from "./tools/sitemaps.js";
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
- registerCruxTool(server);
25
- registerInspectTool(server);
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 registerRobotsTool(server: McpServer): void {
120
+ export function registerAiTool(server: McpServer): void {
121
121
  server.tool(
122
- "robots",
123
- "Fetch and analyze a site's robots.txt. Validates syntax per RFC 9309, audits AI crawler access (139+ bots), lists sitemaps. Shows blocked bots in detail, summarizes allowed.",
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()
@@ -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 "./sample-inspect.js";
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