pagesight 0.10.0 → 0.11.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/README.md +36 -34
- package/package.json +1 -1
- package/src/tools/inspect.ts +5 -0
- package/src/tools/pagespeed.ts +284 -13
package/README.md
CHANGED
|
@@ -8,26 +8,10 @@ See your site the way search engines and AI see it.
|
|
|
8
8
|
npm install pagesight
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Tools
|
|
14
|
-
|
|
15
|
-
| Tool | What it does |
|
|
16
|
-
|------|-------------|
|
|
17
|
-
| `audit` | One-call site audit. Runs pagespeed + metatags + robots + sitemaps + inspect in parallel. Returns prioritized findings. |
|
|
18
|
-
| `pagespeed` | Lighthouse scores, Core Web Vitals, opportunities with resource URLs, failing audits with selectors and fix links. |
|
|
19
|
-
| `metatags` | OG, Twitter Card, canonical, JSON-LD with schema validation, redirect chain, image validation. |
|
|
20
|
-
| `inspect` | Google index status, canonical choice, crawl state, rich results. |
|
|
21
|
-
| `sample_inspect` | Sample URLs from a sitemap and batch-inspect via GSC. Diagnoses indexing patterns. |
|
|
22
|
-
| `performance` | Search analytics — clicks, impressions, CTR, position. `compare: true` for period-over-period deltas. |
|
|
23
|
-
| `crux` | Real-world Core Web Vitals from Chrome users (p75, histograms). |
|
|
24
|
-
| `crux_history` | CWV trends over time — up to 40 weekly data points. |
|
|
25
|
-
| `robots` | robots.txt validation (RFC 9309) + AI crawler audit (139+ bots). |
|
|
26
|
-
| `sitemaps` | GSC properties and sitemaps with submitted/indexed counts. |
|
|
27
|
-
| `setup` | Auth status and OAuth setup. |
|
|
11
|
+
Your AI assistant can write your code. Now it can see your site. Index status, performance, real-user metrics, search traffic, meta tags, structured data, AI crawler access — one package, one call.
|
|
28
12
|
|
|
29
13
|
```
|
|
30
|
-
=== Site Audit: https://
|
|
14
|
+
=== Site Audit: https://example.com ===
|
|
31
15
|
|
|
32
16
|
HIGH Missing canonical URL
|
|
33
17
|
HIGH 7,772 sitemap URLs submitted, 0 indexed
|
|
@@ -37,22 +21,25 @@ LOW Missing Twitter Card tags
|
|
|
37
21
|
LOW No structured data (JSON-LD) found
|
|
38
22
|
```
|
|
39
23
|
|
|
40
|
-
##
|
|
41
|
-
|
|
42
|
-
1. [Google Cloud Console](https://console.cloud.google.com/) — enable Search Console API, PageSpeed Insights API, Chrome UX Report API
|
|
43
|
-
2. Create OAuth client ID (Desktop app) + API key
|
|
44
|
-
3. Configure:
|
|
24
|
+
## Tools
|
|
45
25
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
26
|
+
| Tool | What it does |
|
|
27
|
+
|------|-------------|
|
|
28
|
+
| `audit` | One-call site audit. Runs all checks in parallel. Returns prioritized findings. |
|
|
29
|
+
| `pagespeed` | Lighthouse scores, Core Web Vitals, opportunities, failing audits with fix links. |
|
|
30
|
+
| `metatags` | OG, Twitter Card, canonical, JSON-LD with schema validation, redirect chain, image validation. |
|
|
31
|
+
| `inspect` | Google index status, canonical choice, crawl state, rich results. |
|
|
32
|
+
| `sample_inspect` | Sample URLs from a sitemap and batch-inspect. Diagnoses indexing patterns. |
|
|
33
|
+
| `performance` | Search analytics — clicks, impressions, CTR, position. `compare: true` for period-over-period. |
|
|
34
|
+
| `crux` | Real-user Core Web Vitals (p75, histograms). |
|
|
35
|
+
| `crux_history` | CWV trends over time — up to 40 weekly data points. |
|
|
36
|
+
| `robots` | robots.txt validation (RFC 9309) + AI crawler audit (139+ bots). |
|
|
37
|
+
| `sitemaps` | Search Console properties and sitemaps with submitted/indexed counts. |
|
|
38
|
+
| `setup` | Auth status and OAuth setup. |
|
|
52
39
|
|
|
53
|
-
|
|
40
|
+
## Setup
|
|
54
41
|
|
|
55
|
-
|
|
42
|
+
Add to your MCP config:
|
|
56
43
|
|
|
57
44
|
```json
|
|
58
45
|
{
|
|
@@ -71,15 +58,30 @@ GOOGLE_API_KEY=your-api-key
|
|
|
71
58
|
}
|
|
72
59
|
```
|
|
73
60
|
|
|
74
|
-
|
|
61
|
+
`robots`, `metatags`, and `pagespeed` work without credentials.
|
|
62
|
+
|
|
63
|
+
### Full setup
|
|
64
|
+
|
|
65
|
+
1. [Google Cloud Console](https://console.cloud.google.com/) — enable Search Console API, PageSpeed Insights API, Chrome UX Report API
|
|
66
|
+
2. Create OAuth client ID (Desktop app) + API key
|
|
67
|
+
3. Configure:
|
|
68
|
+
|
|
69
|
+
```env
|
|
70
|
+
GSC_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
|
71
|
+
GSC_CLIENT_SECRET=your-client-secret
|
|
72
|
+
GSC_REFRESH_TOKEN=your-refresh-token
|
|
73
|
+
GOOGLE_API_KEY=your-api-key
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Why Pagesight
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
Every data point comes from a verifiable source. Google's APIs, real Chrome users, RFC 9309, schema.org, a community-maintained bot registry. No invented scores. No rules we can't cite.
|
|
77
79
|
|
|
78
80
|
- **"Title must be under 60 characters"** — Gary Illyes: "an externally made-up metric."
|
|
79
81
|
- **"Only one H1 per page"** — John Mueller: "You can use H1 tags as often as you want."
|
|
80
82
|
- **"Minimum 300 words per page"** — Mueller: "not a quality factor."
|
|
81
83
|
|
|
82
|
-
Pagesight
|
|
84
|
+
Pagesight reports what the sources report. Nothing more.
|
|
83
85
|
|
|
84
86
|
## License
|
|
85
87
|
|
package/package.json
CHANGED
package/src/tools/inspect.ts
CHANGED
|
@@ -34,6 +34,11 @@ function formatInspection(url: string, siteUrl: string, r: InspectionResult): st
|
|
|
34
34
|
lines.push(`\nReferring URLs: ${idx.referringUrls.join(", ")}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
if (idx.verdict !== "PASS") {
|
|
38
|
+
const gscUrl = `https://search.google.com/search-console/inspect?resource_id=${encodeURIComponent(siteUrl)}&id=${encodeURIComponent(url)}`;
|
|
39
|
+
lines.push("", `→ This page is not indexed. Request indexing manually in Google Search Console:`, ` ${gscUrl}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
// Rich Results
|
|
38
43
|
if (r.richResultsResult) {
|
|
39
44
|
lines.push("", "--- Rich Results ---", "");
|
package/src/tools/pagespeed.ts
CHANGED
|
@@ -16,6 +16,10 @@ function scoreLabel(score: number | null): string {
|
|
|
16
16
|
return `${pct} (poor)`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function scorePct(score: number | null): number | null {
|
|
20
|
+
return score === null ? null : Math.round(score * 100);
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
function cwvRating(category: string): string {
|
|
20
24
|
if (category === "FAST") return "good";
|
|
21
25
|
if (category === "AVERAGE") return "needs improvement";
|
|
@@ -190,6 +194,8 @@ function formatFailingAudits(audits: Record<string, PsiAudit>, categoryRefs: str
|
|
|
190
194
|
return lines;
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
// --- Single URL formatting (existing) ---
|
|
198
|
+
|
|
193
199
|
function formatPagespeed(url: string, result: PsiResult): string {
|
|
194
200
|
const lhr = result.lighthouseResult;
|
|
195
201
|
const lines: string[] = [
|
|
@@ -274,12 +280,234 @@ function formatPagespeed(url: string, result: PsiResult): string {
|
|
|
274
280
|
return lines.join("\n");
|
|
275
281
|
}
|
|
276
282
|
|
|
283
|
+
// --- Batch formatting ---
|
|
284
|
+
|
|
285
|
+
function shortUrl(url: string, allUrls: string[]): string {
|
|
286
|
+
try {
|
|
287
|
+
const u = new URL(url);
|
|
288
|
+
const path = u.pathname + u.search;
|
|
289
|
+
const hasDuplicate = allUrls.some(
|
|
290
|
+
(other) => other !== url && new URL(other).pathname + new URL(other).search === path,
|
|
291
|
+
);
|
|
292
|
+
return hasDuplicate ? u.hostname + path : path;
|
|
293
|
+
} catch {
|
|
294
|
+
return url;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatDelta(a: number | null, b: number | null): string {
|
|
299
|
+
if (a === null || b === null) return "";
|
|
300
|
+
const diff = b - a;
|
|
301
|
+
if (diff === 0) return " (=)";
|
|
302
|
+
return diff > 0 ? ` (+${diff})` : ` (${diff})`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function formatBatchCompare(results: Array<{ url: string; result: PsiResult }>, strategy: string): string {
|
|
306
|
+
const [a, b] = results;
|
|
307
|
+
const lhrA = a.result.lighthouseResult;
|
|
308
|
+
const lhrB = b.result.lighthouseResult;
|
|
309
|
+
|
|
310
|
+
const lines: string[] = [
|
|
311
|
+
`=== PageSpeed Compare (${strategy}) ===`,
|
|
312
|
+
``,
|
|
313
|
+
`A: ${a.url}`,
|
|
314
|
+
`B: ${b.url}`,
|
|
315
|
+
`Lighthouse: ${lhrA.lighthouseVersion}`,
|
|
316
|
+
"",
|
|
317
|
+
"--- Scores ---",
|
|
318
|
+
"",
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
// Score comparison
|
|
322
|
+
const catIds = Object.keys(lhrA.categories);
|
|
323
|
+
for (const id of catIds) {
|
|
324
|
+
const catA = lhrA.categories[id];
|
|
325
|
+
const catB = lhrB.categories[id];
|
|
326
|
+
if (!catA || !catB) continue;
|
|
327
|
+
const pA = scorePct(catA.score);
|
|
328
|
+
const pB = scorePct(catB.score);
|
|
329
|
+
const delta = formatDelta(pA, pB);
|
|
330
|
+
lines.push(`${catA.title}: ${pA ?? "N/A"} → ${pB ?? "N/A"}${delta}`);
|
|
331
|
+
}
|
|
332
|
+
lines.push("");
|
|
333
|
+
|
|
334
|
+
// CWV comparison
|
|
335
|
+
const cwvIds = [
|
|
336
|
+
"first-contentful-paint",
|
|
337
|
+
"largest-contentful-paint",
|
|
338
|
+
"total-blocking-time",
|
|
339
|
+
"cumulative-layout-shift",
|
|
340
|
+
"speed-index",
|
|
341
|
+
"interactive",
|
|
342
|
+
];
|
|
343
|
+
const cwvLines: string[] = [];
|
|
344
|
+
for (const id of cwvIds) {
|
|
345
|
+
const auditA = lhrA.audits[id];
|
|
346
|
+
const auditB = lhrB.audits[id];
|
|
347
|
+
if (!auditA?.displayValue || !auditB?.displayValue) continue;
|
|
348
|
+
cwvLines.push(`${auditA.title}: ${auditA.displayValue} → ${auditB.displayValue}`);
|
|
349
|
+
}
|
|
350
|
+
if (cwvLines.length > 0) {
|
|
351
|
+
lines.push("--- Core Web Vitals (Lab) ---", "", ...cwvLines, "");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Opportunities unique to each / shared
|
|
355
|
+
const oppsA = collectOpportunityIds(lhrA.audits);
|
|
356
|
+
const oppsB = collectOpportunityIds(lhrB.audits);
|
|
357
|
+
const onlyA = [...oppsA].filter((id) => !oppsB.has(id));
|
|
358
|
+
const onlyB = [...oppsB].filter((id) => !oppsA.has(id));
|
|
359
|
+
const shared = [...oppsA].filter((id) => oppsB.has(id));
|
|
360
|
+
|
|
361
|
+
if (onlyA.length > 0) {
|
|
362
|
+
lines.push(`--- Opportunities (A only) ---`, "");
|
|
363
|
+
for (const id of onlyA) lines.push(` ${lhrA.audits[id].title}: ${lhrA.audits[id].displayValue ?? ""}`);
|
|
364
|
+
lines.push("");
|
|
365
|
+
}
|
|
366
|
+
if (onlyB.length > 0) {
|
|
367
|
+
lines.push(`--- Opportunities (B only) ---`, "");
|
|
368
|
+
for (const id of onlyB) lines.push(` ${lhrB.audits[id].title}: ${lhrB.audits[id].displayValue ?? ""}`);
|
|
369
|
+
lines.push("");
|
|
370
|
+
}
|
|
371
|
+
if (shared.length > 0) {
|
|
372
|
+
lines.push(`--- Shared Opportunities ---`, "");
|
|
373
|
+
for (const id of shared) {
|
|
374
|
+
lines.push(
|
|
375
|
+
` ${lhrA.audits[id].title}: ${lhrA.audits[id].displayValue ?? ""} → ${lhrB.audits[id].displayValue ?? ""}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
lines.push("");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const timeA = (lhrA.timing.total / 1000).toFixed(1);
|
|
382
|
+
const timeB = (lhrB.timing.total / 1000).toFixed(1);
|
|
383
|
+
lines.push(`Analysis took ${timeA}s + ${timeB}s`);
|
|
384
|
+
|
|
385
|
+
return lines.join("\n");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, strategy: string): string {
|
|
389
|
+
const lines: string[] = [`=== Batch PageSpeed (${results.length} URLs, ${strategy}) ===`, ""];
|
|
390
|
+
|
|
391
|
+
// Collect all category IDs from first result
|
|
392
|
+
const catIds = Object.keys(results[0].result.lighthouseResult.categories);
|
|
393
|
+
const catNames = catIds.map((id) => results[0].result.lighthouseResult.categories[id].title);
|
|
394
|
+
|
|
395
|
+
// Score table
|
|
396
|
+
lines.push("--- Scores ---", "");
|
|
397
|
+
|
|
398
|
+
// Header
|
|
399
|
+
const urlCol = "URL";
|
|
400
|
+
const allUrls = results.map((r) => r.url);
|
|
401
|
+
const urlWidth = Math.max(urlCol.length, ...results.map((r) => shortUrl(r.url, allUrls).length));
|
|
402
|
+
const colWidth = Math.max(...catNames.map((n) => n.length), 4);
|
|
403
|
+
lines.push(`${urlCol.padEnd(urlWidth)} ${catNames.map((n) => n.padEnd(colWidth)).join(" ")}`);
|
|
404
|
+
|
|
405
|
+
// Rows
|
|
406
|
+
let bestPerf: { url: string; score: number } | null = null;
|
|
407
|
+
let worstPerf: { url: string; score: number } | null = null;
|
|
408
|
+
|
|
409
|
+
for (const { url, result } of results) {
|
|
410
|
+
const lhr = result.lighthouseResult;
|
|
411
|
+
const scores = catIds.map((id) => {
|
|
412
|
+
const s = scorePct(lhr.categories[id]?.score);
|
|
413
|
+
return s !== null ? String(s) : "N/A";
|
|
414
|
+
});
|
|
415
|
+
lines.push(`${shortUrl(url, allUrls).padEnd(urlWidth)} ${scores.map((s) => s.padEnd(colWidth)).join(" ")}`);
|
|
416
|
+
|
|
417
|
+
const perf = scorePct(lhr.categories.performance?.score);
|
|
418
|
+
if (perf !== null) {
|
|
419
|
+
if (!bestPerf || perf > bestPerf.score) bestPerf = { url: shortUrl(url, allUrls), score: perf };
|
|
420
|
+
if (!worstPerf || perf < worstPerf.score) worstPerf = { url: shortUrl(url, allUrls), score: perf };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
lines.push("");
|
|
425
|
+
if (bestPerf) lines.push(`Best: ${bestPerf.url} (${bestPerf.score})`);
|
|
426
|
+
if (worstPerf && worstPerf.url !== bestPerf?.url) lines.push(`Worst: ${worstPerf.url} (${worstPerf.score})`);
|
|
427
|
+
lines.push("");
|
|
428
|
+
|
|
429
|
+
// Shared opportunities across pages
|
|
430
|
+
const oppCounts = new Map<string, { title: string; count: number }>();
|
|
431
|
+
for (const { result } of results) {
|
|
432
|
+
for (const id of collectOpportunityIds(result.lighthouseResult.audits)) {
|
|
433
|
+
const existing = oppCounts.get(id);
|
|
434
|
+
if (existing) {
|
|
435
|
+
existing.count++;
|
|
436
|
+
} else {
|
|
437
|
+
oppCounts.set(id, { title: result.lighthouseResult.audits[id].title, count: 1 });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const sharedOpps = [...oppCounts.entries()].filter(([, v]) => v.count >= 2).sort((a, b) => b[1].count - a[1].count);
|
|
443
|
+
if (sharedOpps.length > 0) {
|
|
444
|
+
lines.push("--- Shared Opportunities ---", "");
|
|
445
|
+
for (const [, { title, count }] of sharedOpps.slice(0, 10)) {
|
|
446
|
+
lines.push(` ${title} (${count}/${results.length} pages)`);
|
|
447
|
+
}
|
|
448
|
+
lines.push("");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const totalTime = results.reduce((sum, r) => sum + r.result.lighthouseResult.timing.total, 0);
|
|
452
|
+
lines.push(`Total analysis time: ${(totalTime / 1000).toFixed(1)}s`);
|
|
453
|
+
|
|
454
|
+
return lines.join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function collectOpportunityIds(audits: Record<string, PsiAudit>): Set<string> {
|
|
458
|
+
const ids = new Set<string>();
|
|
459
|
+
for (const [id, audit] of Object.entries(audits)) {
|
|
460
|
+
if (audit.score === null || audit.score >= 1) continue;
|
|
461
|
+
const mode = audit.scoreDisplayMode;
|
|
462
|
+
const hasNumeric = audit.numericValue && audit.numericValue > 0;
|
|
463
|
+
if (mode === "metricSavings" || ((mode === "numeric" || mode === "binary") && hasNumeric)) {
|
|
464
|
+
if (hasNumeric || (audit.details?.items?.length ?? 0) > 0) {
|
|
465
|
+
ids.add(id);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return ids;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function runBatch(
|
|
473
|
+
urls: string[],
|
|
474
|
+
options: { strategy?: "mobile" | "desktop"; categories?: PsiCategoryType[]; locale?: string },
|
|
475
|
+
): Promise<Array<{ url: string; result?: PsiResult; error?: string }>> {
|
|
476
|
+
const concurrency = 2;
|
|
477
|
+
const results: Array<{ url: string; result?: PsiResult; error?: string }> = [];
|
|
478
|
+
let i = 0;
|
|
479
|
+
|
|
480
|
+
while (i < urls.length) {
|
|
481
|
+
const batch = urls.slice(i, i + concurrency);
|
|
482
|
+
const settled = await Promise.all(
|
|
483
|
+
batch.map(async (url) => {
|
|
484
|
+
try {
|
|
485
|
+
const result = await runPagespeed(url, options);
|
|
486
|
+
return { url, result };
|
|
487
|
+
} catch (err) {
|
|
488
|
+
return { url, error: err instanceof Error ? err.message : String(err) };
|
|
489
|
+
}
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
results.push(...settled);
|
|
493
|
+
i += concurrency;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return results;
|
|
497
|
+
}
|
|
498
|
+
|
|
277
499
|
export function registerPagespeedTool(server: McpServer): void {
|
|
278
500
|
server.tool(
|
|
279
501
|
"pagespeed",
|
|
280
|
-
"Analyze
|
|
502
|
+
"Analyze page performance using Google PageSpeed Insights. Accepts a single URL or multiple URLs (batch mode). With 2 URLs, returns a side-by-side comparison with deltas. With 3-10 URLs, returns a summary table with shared opportunities.",
|
|
281
503
|
{
|
|
282
|
-
url: z.string().url().describe("
|
|
504
|
+
url: z.string().url().optional().describe("Single URL to analyze. Use this OR urls, not both."),
|
|
505
|
+
urls: z
|
|
506
|
+
.array(z.string().url())
|
|
507
|
+
.min(2)
|
|
508
|
+
.max(10)
|
|
509
|
+
.optional()
|
|
510
|
+
.describe("Multiple URLs (2-10) for batch analysis. 2 URLs = compare mode, 3+ = summary table."),
|
|
283
511
|
strategy: z.enum(["mobile", "desktop"]).optional().describe("Device strategy. Default: 'mobile'."),
|
|
284
512
|
categories: z
|
|
285
513
|
.array(z.enum(["performance", "accessibility", "best-practices", "seo"]))
|
|
@@ -287,18 +515,61 @@ export function registerPagespeedTool(server: McpServer): void {
|
|
|
287
515
|
.describe("Lighthouse categories to run. Default: all four."),
|
|
288
516
|
locale: z.string().optional().describe("Locale for localized results (e.g., 'pt-BR', 'en')."),
|
|
289
517
|
},
|
|
290
|
-
async ({ url, strategy, categories, locale }) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
518
|
+
async ({ url, urls, strategy, categories, locale }) => {
|
|
519
|
+
const strat = (strategy as "mobile" | "desktop") ?? "mobile";
|
|
520
|
+
const cats = categories as PsiCategoryType[] | undefined;
|
|
521
|
+
const opts = { strategy: strat, categories: cats, locale };
|
|
522
|
+
|
|
523
|
+
// Validate: must provide url or urls, not both
|
|
524
|
+
if (url && urls) {
|
|
525
|
+
return {
|
|
526
|
+
content: [{ type: "text", text: "Error: provide either 'url' (single) or 'urls' (batch), not both." }],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (!url && !urls) {
|
|
530
|
+
return {
|
|
531
|
+
content: [{ type: "text", text: "Error: provide 'url' for single analysis or 'urls' for batch analysis." }],
|
|
532
|
+
};
|
|
301
533
|
}
|
|
534
|
+
|
|
535
|
+
// Single URL — existing behavior
|
|
536
|
+
if (url) {
|
|
537
|
+
try {
|
|
538
|
+
const result = await runPagespeed(url, opts);
|
|
539
|
+
return { content: [{ type: "text", text: formatPagespeed(url, result) }] };
|
|
540
|
+
} catch (err) {
|
|
541
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
542
|
+
return { content: [{ type: "text", text: `Error running PageSpeed analysis: ${msg}` }] };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Batch mode
|
|
547
|
+
const batchUrls = urls as string[];
|
|
548
|
+
const results = await runBatch(batchUrls, opts);
|
|
549
|
+
|
|
550
|
+
// Separate successes and failures
|
|
551
|
+
const successes = results.filter((r): r is { url: string; result: PsiResult } => !!r.result);
|
|
552
|
+
const failures = results.filter((r): r is { url: string; error: string } => !!r.error);
|
|
553
|
+
|
|
554
|
+
if (successes.length === 0) {
|
|
555
|
+
const errorLines = failures.map((f) => `${f.url}: ${f.error}`);
|
|
556
|
+
return { content: [{ type: "text", text: `All URLs failed:\n${errorLines.join("\n")}` }] };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let output: string;
|
|
560
|
+
if (successes.length === 2) {
|
|
561
|
+
output = formatBatchCompare(successes, strat);
|
|
562
|
+
} else {
|
|
563
|
+
output = formatBatchTable(successes, strat);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Append any failures
|
|
567
|
+
if (failures.length > 0) {
|
|
568
|
+
const errorLines = failures.map((f) => `${f.url}: ${f.error}`);
|
|
569
|
+
output += `\n\n--- Errors ---\n${errorLines.join("\n")}`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { content: [{ type: "text", text: output }] };
|
|
302
573
|
},
|
|
303
574
|
);
|
|
304
575
|
}
|