pagesight 0.12.2 → 0.13.1
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/tools/ai.ts +112 -8
- package/src/tools/speed.ts +5 -0
package/package.json
CHANGED
package/src/tools/ai.ts
CHANGED
|
@@ -2,6 +2,85 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { auditAiCrawlers, type CrawlerStatus, fetchRobotsTxt, isAllowed, type RobotsTxt } from "../lib/robots.js";
|
|
4
4
|
|
|
5
|
+
// --- llms.txt detection ---
|
|
6
|
+
|
|
7
|
+
interface LlmsTxtResult {
|
|
8
|
+
exists: boolean;
|
|
9
|
+
size: number | null;
|
|
10
|
+
firstLine: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function checkLlmsTxt(origin: string, path: string): Promise<LlmsTxtResult> {
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${origin}${path}`, {
|
|
16
|
+
headers: { "User-Agent": "Pagesight/1.0" },
|
|
17
|
+
redirect: "follow",
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) return { exists: false, size: null, firstLine: null };
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
const firstLine =
|
|
22
|
+
text
|
|
23
|
+
.split("\n")
|
|
24
|
+
.find((l) => l.trim().length > 0)
|
|
25
|
+
?.trim() ?? null;
|
|
26
|
+
return { exists: true, size: text.length, firstLine };
|
|
27
|
+
} catch {
|
|
28
|
+
return { exists: false, size: null, firstLine: null };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatLlmsTxt(llmsTxt: LlmsTxtResult, llmsFullTxt: LlmsTxtResult): string[] {
|
|
33
|
+
const lines: string[] = ["--- LLM Visibility ---", ""];
|
|
34
|
+
|
|
35
|
+
if (llmsTxt.exists) {
|
|
36
|
+
const sizeKB = llmsTxt.size ? `${(llmsTxt.size / 1024).toFixed(1)} KB` : "unknown size";
|
|
37
|
+
lines.push(`llms.txt: FOUND (${sizeKB})`);
|
|
38
|
+
if (llmsTxt.firstLine) lines.push(` ${llmsTxt.firstLine}`);
|
|
39
|
+
} else {
|
|
40
|
+
lines.push("llms.txt: NOT FOUND — consider adding one for AI-friendly documentation");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (llmsFullTxt.exists) {
|
|
44
|
+
const sizeKB = llmsFullTxt.size ? `${(llmsFullTxt.size / 1024).toFixed(1)} KB` : "unknown size";
|
|
45
|
+
lines.push(`llms-full.txt: FOUND (${sizeKB})`);
|
|
46
|
+
} else if (llmsTxt.exists) {
|
|
47
|
+
lines.push("llms-full.txt: NOT FOUND — consider adding full docs for AI context windows");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Category summary ---
|
|
54
|
+
|
|
55
|
+
function formatCategorySummary(crawlers: CrawlerStatus[]): string[] {
|
|
56
|
+
const categoryOrder = ["Training", "Search", "Assistant", "Agent", "Other"];
|
|
57
|
+
const summary = new Map<string, { blocked: number; allowed: number }>();
|
|
58
|
+
|
|
59
|
+
for (const bot of crawlers) {
|
|
60
|
+
const cat = normalizeCategory(bot.category);
|
|
61
|
+
const existing = summary.get(cat) ?? { blocked: 0, allowed: 0 };
|
|
62
|
+
if (bot.allowed) existing.allowed++;
|
|
63
|
+
else existing.blocked++;
|
|
64
|
+
summary.set(cat, existing);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines: string[] = ["", "--- AI Crawler Summary by Category ---", ""];
|
|
68
|
+
for (const cat of categoryOrder) {
|
|
69
|
+
const s = summary.get(cat);
|
|
70
|
+
if (!s) continue;
|
|
71
|
+
const total = s.blocked + s.allowed;
|
|
72
|
+
if (s.blocked === 0) {
|
|
73
|
+
lines.push(`${cat}: all ${total} allowed`);
|
|
74
|
+
} else if (s.allowed === 0) {
|
|
75
|
+
lines.push(`${cat}: all ${total} BLOCKED`);
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(`${cat}: ${s.blocked} blocked, ${s.allowed} allowed (of ${total})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
|
|
5
84
|
// Normalize the messy registry categories into clean buckets
|
|
6
85
|
function normalizeCategory(raw: string): string {
|
|
7
86
|
const lower = raw.toLowerCase();
|
|
@@ -38,11 +117,23 @@ function formatRobotsAudit(origin: string, robots: RobotsTxt, statusCode: number
|
|
|
38
117
|
|
|
39
118
|
if (robots.groups.length > 0) {
|
|
40
119
|
lines.push("", "--- User-Agent Groups ---", "");
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
120
|
+
if (robots.groups.length > 10) {
|
|
121
|
+
// Condensed: show count and top groups only
|
|
122
|
+
const sorted = [...robots.groups].sort((a, b) => b.rules.length - a.rules.length);
|
|
123
|
+
for (const group of sorted.slice(0, 5)) {
|
|
124
|
+
const agents = group.userAgents.join(", ");
|
|
125
|
+
const allows = group.rules.filter((r) => r.type === "allow").length;
|
|
126
|
+
const disallows = group.rules.filter((r) => r.type === "disallow").length;
|
|
127
|
+
lines.push(` ${agents}: ${disallows} disallow, ${allows} allow`);
|
|
128
|
+
}
|
|
129
|
+
lines.push(` ... and ${robots.groups.length - 5} more groups`);
|
|
130
|
+
} else {
|
|
131
|
+
for (const group of robots.groups) {
|
|
132
|
+
const agents = group.userAgents.join(", ");
|
|
133
|
+
const allows = group.rules.filter((r) => r.type === "allow").length;
|
|
134
|
+
const disallows = group.rules.filter((r) => r.type === "disallow").length;
|
|
135
|
+
lines.push(` ${agents}: ${disallows} disallow, ${allows} allow`);
|
|
136
|
+
}
|
|
46
137
|
}
|
|
47
138
|
}
|
|
48
139
|
|
|
@@ -120,7 +211,7 @@ function formatRobotsAudit(origin: string, robots: RobotsTxt, statusCode: number
|
|
|
120
211
|
export function registerAiTool(server: McpServer): void {
|
|
121
212
|
server.tool(
|
|
122
213
|
"ai",
|
|
123
|
-
"Analyze your site's AI visibility.
|
|
214
|
+
"Analyze your site's AI visibility. Audits AI crawler access (training, search, assistant, agent) via robots.txt, checks for llms.txt and llms-full.txt, validates syntax per RFC 9309, and tests specific path access. Shows how AI systems see your site.",
|
|
124
215
|
{
|
|
125
216
|
url: z
|
|
126
217
|
.string()
|
|
@@ -159,8 +250,21 @@ export function registerAiTool(server: McpServer): void {
|
|
|
159
250
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
160
251
|
}
|
|
161
252
|
|
|
162
|
-
const crawlers = await
|
|
163
|
-
|
|
253
|
+
const [crawlers, llmsTxt, llmsFullTxt] = await Promise.all([
|
|
254
|
+
auditAiCrawlers(robotsTxt),
|
|
255
|
+
checkLlmsTxt(origin, "/llms.txt"),
|
|
256
|
+
checkLlmsTxt(origin, "/llms-full.txt"),
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
const output: string[] = [formatRobotsAudit(origin, robotsTxt, statusCode, crawlers)];
|
|
260
|
+
|
|
261
|
+
if (crawlers.length > 0) {
|
|
262
|
+
output.push(...formatCategorySummary(crawlers));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
output.push("", ...formatLlmsTxt(llmsTxt, llmsFullTxt));
|
|
266
|
+
|
|
267
|
+
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
164
268
|
} catch (err) {
|
|
165
269
|
const msg = err instanceof Error ? err.message : String(err);
|
|
166
270
|
return { content: [{ type: "text", text: `Error analyzing robots.txt: ${msg}` }] };
|
package/src/tools/speed.ts
CHANGED
|
@@ -549,6 +549,11 @@ const METRIC_LABELS: Record<string, string> = {
|
|
|
549
549
|
round_trip_time: "RTT",
|
|
550
550
|
navigation_types: "Navigation Types",
|
|
551
551
|
form_factors: "Form Factors",
|
|
552
|
+
largest_contentful_paint_image_element_render_delay: "LCP Image Render Delay",
|
|
553
|
+
largest_contentful_paint_image_resource_load_delay: "LCP Image Load Delay",
|
|
554
|
+
largest_contentful_paint_image_resource_load_duration: "LCP Image Load Duration",
|
|
555
|
+
largest_contentful_paint_image_time_to_first_byte: "LCP Image TTFB",
|
|
556
|
+
largest_contentful_paint_resource_type: "LCP Resource Type",
|
|
552
557
|
};
|
|
553
558
|
|
|
554
559
|
function formatDate(d: { year: number; month: number; day: number }): string {
|