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
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
|
+
type CruxFormFactor,
|
|
5
|
+
type CruxHistoryResponse,
|
|
6
|
+
type CruxResponse,
|
|
7
|
+
queryCrux,
|
|
8
|
+
queryCruxHistory,
|
|
9
|
+
} from "../lib/crux.js";
|
|
10
|
+
import {
|
|
11
|
+
hasApiKey,
|
|
4
12
|
type PsiAudit,
|
|
5
13
|
type PsiAuditDetailItem,
|
|
6
14
|
type PsiCategoryType,
|
|
@@ -8,6 +16,11 @@ import {
|
|
|
8
16
|
runPagespeed,
|
|
9
17
|
} from "../lib/psi.js";
|
|
10
18
|
|
|
19
|
+
// --- PageSpeed helpers ---
|
|
20
|
+
|
|
21
|
+
const QUOTA_NOTE =
|
|
22
|
+
"\n\nNote: No GOOGLE_API_KEY configured — using shared quota (400 req/day). Set your own key to avoid rate limits.";
|
|
23
|
+
|
|
11
24
|
function scoreLabel(score: number | null): string {
|
|
12
25
|
if (score === null) return "N/A";
|
|
13
26
|
const pct = Math.round(score * 100);
|
|
@@ -194,8 +207,6 @@ function formatFailingAudits(audits: Record<string, PsiAudit>, categoryRefs: str
|
|
|
194
207
|
return lines;
|
|
195
208
|
}
|
|
196
209
|
|
|
197
|
-
// --- Single URL formatting (existing) ---
|
|
198
|
-
|
|
199
210
|
function formatPagespeed(url: string, result: PsiResult): string {
|
|
200
211
|
const lhr = result.lighthouseResult;
|
|
201
212
|
const lines: string[] = [
|
|
@@ -280,8 +291,6 @@ function formatPagespeed(url: string, result: PsiResult): string {
|
|
|
280
291
|
return lines.join("\n");
|
|
281
292
|
}
|
|
282
293
|
|
|
283
|
-
// --- Batch formatting ---
|
|
284
|
-
|
|
285
294
|
function shortUrl(url: string, allUrls: string[]): string {
|
|
286
295
|
try {
|
|
287
296
|
const u = new URL(url);
|
|
@@ -448,6 +457,39 @@ function formatBatchTable(results: Array<{ url: string; result: PsiResult }>, st
|
|
|
448
457
|
lines.push("");
|
|
449
458
|
}
|
|
450
459
|
|
|
460
|
+
// A11y failures deduplicated across pages
|
|
461
|
+
const a11yFailures = new Map<string, { title: string; count: number; pages: string[] }>();
|
|
462
|
+
for (const { url, result } of results) {
|
|
463
|
+
const lhr = result.lighthouseResult;
|
|
464
|
+
const a11yCat = lhr.categories.accessibility;
|
|
465
|
+
if (!a11yCat?.auditRefs) continue;
|
|
466
|
+
|
|
467
|
+
for (const ref of a11yCat.auditRefs) {
|
|
468
|
+
const audit = lhr.audits[ref.id];
|
|
469
|
+
if (audit && audit.score !== null && audit.score < 1) {
|
|
470
|
+
const existing = a11yFailures.get(ref.id);
|
|
471
|
+
const page = shortUrl(url, allUrls);
|
|
472
|
+
if (existing) {
|
|
473
|
+
existing.count++;
|
|
474
|
+
existing.pages.push(page);
|
|
475
|
+
} else {
|
|
476
|
+
a11yFailures.set(ref.id, { title: audit.title, count: 1, pages: [page] });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const sharedA11y = [...a11yFailures.entries()]
|
|
483
|
+
.filter(([, v]) => v.count >= 2)
|
|
484
|
+
.sort((a, b) => b[1].count - a[1].count);
|
|
485
|
+
if (sharedA11y.length > 0) {
|
|
486
|
+
lines.push("--- Accessibility Issues (shared) ---", "");
|
|
487
|
+
for (const [, { title, count, pages }] of sharedA11y.slice(0, 10)) {
|
|
488
|
+
lines.push(` ${title} (${count}/${results.length} pages: ${pages.join(", ")})`);
|
|
489
|
+
}
|
|
490
|
+
lines.push("");
|
|
491
|
+
}
|
|
492
|
+
|
|
451
493
|
const totalTime = results.reduce((sum, r) => sum + r.result.lighthouseResult.timing.total, 0);
|
|
452
494
|
lines.push(`Total analysis time: ${(totalTime / 1000).toFixed(1)}s`);
|
|
453
495
|
|
|
@@ -496,80 +538,366 @@ async function runBatch(
|
|
|
496
538
|
return results;
|
|
497
539
|
}
|
|
498
540
|
|
|
499
|
-
|
|
541
|
+
// --- CrUX helpers ---
|
|
542
|
+
|
|
543
|
+
const METRIC_LABELS: Record<string, string> = {
|
|
544
|
+
cumulative_layout_shift: "CLS",
|
|
545
|
+
first_contentful_paint: "FCP",
|
|
546
|
+
interaction_to_next_paint: "INP",
|
|
547
|
+
largest_contentful_paint: "LCP",
|
|
548
|
+
experimental_time_to_first_byte: "TTFB",
|
|
549
|
+
round_trip_time: "RTT",
|
|
550
|
+
navigation_types: "Navigation Types",
|
|
551
|
+
form_factors: "Form Factors",
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
function formatDate(d: { year: number; month: number; day: number }): string {
|
|
555
|
+
return `${d.year}-${String(d.month).padStart(2, "0")}-${String(d.day).padStart(2, "0")}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function formatCrux(target: string, result: CruxResponse): string {
|
|
559
|
+
const r = result.record;
|
|
560
|
+
const period = r.collectionPeriod;
|
|
561
|
+
const lines: string[] = [
|
|
562
|
+
`=== CrUX: ${target} ===`,
|
|
563
|
+
`Form factor: ${r.key.formFactor ?? "all"}`,
|
|
564
|
+
`Period: ${formatDate(period.firstDate)} to ${formatDate(period.lastDate)}`,
|
|
565
|
+
"",
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
if (result.urlNormalizationDetails) {
|
|
569
|
+
const norm = result.urlNormalizationDetails;
|
|
570
|
+
if (norm.originalUrl !== norm.normalizedUrl) {
|
|
571
|
+
lines.push(`Normalized: ${norm.originalUrl} → ${norm.normalizedUrl}`, "");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
lines.push("--- Metrics (p75) ---", "");
|
|
576
|
+
|
|
577
|
+
for (const [key, metric] of Object.entries(r.metrics)) {
|
|
578
|
+
const label = METRIC_LABELS[key] ?? key;
|
|
579
|
+
|
|
580
|
+
if (metric.percentiles) {
|
|
581
|
+
const val = metric.percentiles.p75;
|
|
582
|
+
const unit = key === "cumulative_layout_shift" ? "" : "ms";
|
|
583
|
+
lines.push(`${label}: ${val}${unit}`);
|
|
584
|
+
|
|
585
|
+
if (metric.histogram) {
|
|
586
|
+
const buckets = metric.histogram.map((b) => `${Math.round(b.density * 100)}%`).join(" / ");
|
|
587
|
+
lines.push(` Distribution (good/needs improvement/poor): ${buckets}`);
|
|
588
|
+
}
|
|
589
|
+
} else if (metric.fractions) {
|
|
590
|
+
lines.push(`${label}:`);
|
|
591
|
+
for (const [fKey, fVal] of Object.entries(metric.fractions)) {
|
|
592
|
+
lines.push(` ${fKey}: ${(fVal * 100).toFixed(1)}%`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return lines.join("\n");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function formatCruxHistory(target: string, result: CruxHistoryResponse): string {
|
|
601
|
+
const r = result.record;
|
|
602
|
+
const periods = r.collectionPeriods;
|
|
603
|
+
const lines: string[] = [
|
|
604
|
+
`=== CrUX History: ${target} ===`,
|
|
605
|
+
`Form factor: ${r.key.formFactor ?? "all"}`,
|
|
606
|
+
`Periods: ${periods.length} (${formatDate(periods[0].firstDate)} to ${formatDate(periods[periods.length - 1].lastDate)})`,
|
|
607
|
+
"",
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
if (result.urlNormalizationDetails) {
|
|
611
|
+
const norm = result.urlNormalizationDetails;
|
|
612
|
+
if (norm.originalUrl !== norm.normalizedUrl) {
|
|
613
|
+
lines.push(`Normalized: ${norm.originalUrl} → ${norm.normalizedUrl}`, "");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
lines.push("--- p75 Trend ---", "");
|
|
618
|
+
|
|
619
|
+
for (const [key, metric] of Object.entries(r.metrics)) {
|
|
620
|
+
const label = METRIC_LABELS[key] ?? key;
|
|
621
|
+
|
|
622
|
+
if (metric.percentilesTimeseries) {
|
|
623
|
+
const values = metric.percentilesTimeseries.p75s;
|
|
624
|
+
const first = values[0];
|
|
625
|
+
const last = values[values.length - 1];
|
|
626
|
+
const unit = key === "cumulative_layout_shift" ? "" : "ms";
|
|
627
|
+
|
|
628
|
+
if (first === null && last === null) {
|
|
629
|
+
lines.push(`${label}: insufficient data`);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
lines.push(
|
|
634
|
+
`${label}: ${first ?? "N/A"}${first !== null ? unit : ""} → ${last ?? "N/A"}${last !== null ? unit : ""} (${values.length} points)`,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// Show trend direction
|
|
638
|
+
if (first !== null && last !== null) {
|
|
639
|
+
const f = Number(first);
|
|
640
|
+
const l = Number(last);
|
|
641
|
+
if (!Number.isNaN(f) && !Number.isNaN(l)) {
|
|
642
|
+
const change = ((l - f) / f) * 100;
|
|
643
|
+
const dir = change > 5 ? "worse" : change < -5 ? "improved" : "stable";
|
|
644
|
+
lines.push(` Trend: ${change > 0 ? "+" : ""}${change.toFixed(1)}% (${dir})`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} else if (metric.fractionTimeseries) {
|
|
648
|
+
lines.push(`${label}: (fraction timeseries, ${periods.length} points)`);
|
|
649
|
+
for (const [fKey, fData] of Object.entries(metric.fractionTimeseries)) {
|
|
650
|
+
const fracs = fData.fractions;
|
|
651
|
+
const first = fracs[0];
|
|
652
|
+
const last = fracs[fracs.length - 1];
|
|
653
|
+
if (first !== null && last !== null && !Number.isNaN(first) && !Number.isNaN(last)) {
|
|
654
|
+
lines.push(` ${fKey}: ${(first * 100).toFixed(1)}% → ${(last * 100).toFixed(1)}%`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Show last 5 data points as table for core metrics
|
|
661
|
+
const coreMetrics = ["largest_contentful_paint", "interaction_to_next_paint", "cumulative_layout_shift"];
|
|
662
|
+
const available = coreMetrics.filter((m) => r.metrics[m]?.percentilesTimeseries);
|
|
663
|
+
|
|
664
|
+
if (available.length > 0 && periods.length >= 5) {
|
|
665
|
+
lines.push("", "--- Recent Data Points ---", "");
|
|
666
|
+
const lastN = 5;
|
|
667
|
+
const startIdx = periods.length - lastN;
|
|
668
|
+
|
|
669
|
+
lines.push(`${"Date".padEnd(12)} ${available.map((m) => (METRIC_LABELS[m] ?? m).padEnd(10)).join(" ")}`);
|
|
670
|
+
for (let i = startIdx; i < periods.length; i++) {
|
|
671
|
+
const date = formatDate(periods[i].lastDate);
|
|
672
|
+
const vals = available.map((m) => {
|
|
673
|
+
const v = r.metrics[m].percentilesTimeseries?.p75s[i];
|
|
674
|
+
return String(v ?? "N/A").padEnd(10);
|
|
675
|
+
});
|
|
676
|
+
lines.push(`${date.padEnd(12)} ${vals.join(" ")}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return lines.join("\n");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// --- Unified speed tool ---
|
|
684
|
+
|
|
685
|
+
export function registerSpeedTool(server: McpServer): void {
|
|
500
686
|
server.tool(
|
|
501
|
-
"
|
|
502
|
-
"Analyze
|
|
687
|
+
"speed",
|
|
688
|
+
"Analyze site performance. Run PageSpeed Insights (lab metrics, Lighthouse scores, opportunities) for single or multiple URLs, or query Chrome UX Report for real-world field data and historical trends.",
|
|
503
689
|
{
|
|
504
|
-
|
|
690
|
+
action: z
|
|
691
|
+
.enum(["pagespeed", "crux", "crux_history"])
|
|
692
|
+
.optional()
|
|
693
|
+
.describe(
|
|
694
|
+
"Which analysis to run. Auto-detected: 'pagespeed' when url/urls provided, 'crux' when origin provided.",
|
|
695
|
+
),
|
|
696
|
+
url: z.string().url().optional().describe("URL to analyze (PageSpeed or CrUX)."),
|
|
505
697
|
urls: z
|
|
506
698
|
.array(z.string().url())
|
|
507
699
|
.min(2)
|
|
508
700
|
.max(10)
|
|
509
701
|
.optional()
|
|
510
|
-
.describe("Multiple URLs (2-10) for batch
|
|
511
|
-
strategy: z.enum(["mobile", "desktop"]).optional().describe("Device strategy. Default: 'mobile'."),
|
|
702
|
+
.describe("Multiple URLs (2-10) for batch PageSpeed. 2 = compare, 3+ = summary table."),
|
|
703
|
+
strategy: z.enum(["mobile", "desktop"]).optional().describe("Device strategy for PageSpeed. Default: 'mobile'."),
|
|
512
704
|
categories: z
|
|
513
705
|
.array(z.enum(["performance", "accessibility", "best-practices", "seo"]))
|
|
514
706
|
.optional()
|
|
515
|
-
.describe("Lighthouse categories
|
|
516
|
-
locale: z.string().optional().describe("Locale for
|
|
707
|
+
.describe("Lighthouse categories. Default: all four."),
|
|
708
|
+
locale: z.string().optional().describe("Locale for PageSpeed results."),
|
|
709
|
+
origin: z.string().optional().describe("Origin for CrUX data (e.g., 'https://example.com'). Triggers CrUX mode."),
|
|
710
|
+
form_factor: z.enum(["DESKTOP", "PHONE", "TABLET"]).optional().describe("CrUX device filter."),
|
|
711
|
+
metrics: z
|
|
712
|
+
.array(
|
|
713
|
+
z.enum([
|
|
714
|
+
"cumulative_layout_shift",
|
|
715
|
+
"first_contentful_paint",
|
|
716
|
+
"interaction_to_next_paint",
|
|
717
|
+
"largest_contentful_paint",
|
|
718
|
+
"experimental_time_to_first_byte",
|
|
719
|
+
"round_trip_time",
|
|
720
|
+
"navigation_types",
|
|
721
|
+
"form_factors",
|
|
722
|
+
]),
|
|
723
|
+
)
|
|
724
|
+
.optional()
|
|
725
|
+
.describe("CrUX metrics to query."),
|
|
726
|
+
periods: z.number().min(1).max(40).optional().describe("CrUX history periods (1-40). Default: 25."),
|
|
517
727
|
},
|
|
518
|
-
async ({ url, urls, strategy, categories, locale }) => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
728
|
+
async ({ action, url, urls, strategy, categories, locale, origin, form_factor, metrics, periods }) => {
|
|
729
|
+
// Determine which action to run
|
|
730
|
+
let resolvedAction = action;
|
|
731
|
+
if (!resolvedAction) {
|
|
732
|
+
if (urls) {
|
|
733
|
+
resolvedAction = "pagespeed";
|
|
734
|
+
} else if (origin && !url) {
|
|
735
|
+
resolvedAction = "crux";
|
|
736
|
+
} else if (periods) {
|
|
737
|
+
resolvedAction = "crux_history";
|
|
738
|
+
} else {
|
|
739
|
+
resolvedAction = "pagespeed";
|
|
740
|
+
}
|
|
528
741
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
742
|
+
|
|
743
|
+
// --- PageSpeed action ---
|
|
744
|
+
if (resolvedAction === "pagespeed") {
|
|
745
|
+
const strat = (strategy as "mobile" | "desktop") ?? "mobile";
|
|
746
|
+
const cats = categories as PsiCategoryType[] | undefined;
|
|
747
|
+
const opts = { strategy: strat, categories: cats, locale };
|
|
748
|
+
|
|
749
|
+
if (url && urls) {
|
|
750
|
+
return {
|
|
751
|
+
content: [
|
|
752
|
+
{ type: "text" as const, text: "Error: provide either 'url' (single) or 'urls' (batch), not both." },
|
|
753
|
+
],
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (!url && !urls) {
|
|
757
|
+
return {
|
|
758
|
+
content: [
|
|
759
|
+
{ type: "text" as const, text: "Error: provide 'url' for single analysis or 'urls' for batch analysis." },
|
|
760
|
+
],
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Single URL
|
|
765
|
+
if (url) {
|
|
766
|
+
try {
|
|
767
|
+
const result = await runPagespeed(url, opts);
|
|
768
|
+
const text = formatPagespeed(url, result) + (hasApiKey() ? "" : QUOTA_NOTE);
|
|
769
|
+
return { content: [{ type: "text" as const, text }] };
|
|
770
|
+
} catch (err) {
|
|
771
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
772
|
+
return { content: [{ type: "text" as const, text: `Error running PageSpeed analysis: ${msg}` }] };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Batch mode
|
|
777
|
+
const batchUrls = urls as string[];
|
|
778
|
+
const results = await runBatch(batchUrls, opts);
|
|
779
|
+
|
|
780
|
+
const successes = results.filter((r): r is { url: string; result: PsiResult } => !!r.result);
|
|
781
|
+
const failures = results.filter((r): r is { url: string; error: string } => !!r.error);
|
|
782
|
+
|
|
783
|
+
if (successes.length === 0) {
|
|
784
|
+
const errorLines = failures.map((f) => `${f.url}: ${f.error}`);
|
|
785
|
+
return { content: [{ type: "text" as const, text: `All URLs failed:\n${errorLines.join("\n")}` }] };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
let output: string;
|
|
789
|
+
if (successes.length === 2) {
|
|
790
|
+
output = formatBatchCompare(successes, strat);
|
|
791
|
+
} else {
|
|
792
|
+
output = formatBatchTable(successes, strat);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (failures.length > 0) {
|
|
796
|
+
const errorLines = failures.map((f) => `${f.url}: ${f.error}`);
|
|
797
|
+
output += `\n\n--- Errors ---\n${errorLines.join("\n")}`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!hasApiKey()) output += QUOTA_NOTE;
|
|
801
|
+
|
|
802
|
+
return { content: [{ type: "text" as const, text: output }] };
|
|
533
803
|
}
|
|
534
804
|
|
|
535
|
-
//
|
|
536
|
-
if (
|
|
805
|
+
// --- CrUX action ---
|
|
806
|
+
if (resolvedAction === "crux") {
|
|
807
|
+
const cruxUrl = url;
|
|
808
|
+
const cruxOrigin = origin;
|
|
809
|
+
|
|
810
|
+
if (!cruxUrl && !cruxOrigin) {
|
|
811
|
+
return { content: [{ type: "text" as const, text: "Error: provide either url or origin, not both." }] };
|
|
812
|
+
}
|
|
813
|
+
if (cruxUrl && cruxOrigin) {
|
|
814
|
+
return { content: [{ type: "text" as const, text: "Error: provide either url or origin, not both." }] };
|
|
815
|
+
}
|
|
816
|
+
|
|
537
817
|
try {
|
|
538
|
-
const result = await
|
|
539
|
-
|
|
818
|
+
const result = await queryCrux({
|
|
819
|
+
url: cruxUrl,
|
|
820
|
+
origin: cruxOrigin,
|
|
821
|
+
formFactor: form_factor as CruxFormFactor | undefined,
|
|
822
|
+
metrics,
|
|
823
|
+
});
|
|
824
|
+
return { content: [{ type: "text" as const, text: formatCrux(cruxUrl ?? cruxOrigin ?? "", result) }] };
|
|
540
825
|
} catch (err) {
|
|
541
826
|
const msg = err instanceof Error ? err.message : String(err);
|
|
542
|
-
|
|
827
|
+
if (msg.includes("404")) {
|
|
828
|
+
const target = cruxUrl ?? cruxOrigin ?? "";
|
|
829
|
+
const lines = [`No CrUX data for ${target}.`, ""];
|
|
830
|
+
lines.push("CrUX requires sufficient Chrome user traffic (roughly 1,000+ monthly visits).");
|
|
831
|
+
if (cruxUrl) {
|
|
832
|
+
const originUrl = new URL(cruxUrl).origin;
|
|
833
|
+
lines.push(`Try origin-level data instead: origin "${originUrl}"`);
|
|
834
|
+
}
|
|
835
|
+
lines.push("For lab metrics without traffic requirements, use speed with a url instead.");
|
|
836
|
+
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
837
|
+
}
|
|
838
|
+
if (msg.includes("SERVICE_DISABLED") || msg.includes("API_KEY_SERVICE_BLOCKED")) {
|
|
839
|
+
return {
|
|
840
|
+
content: [
|
|
841
|
+
{
|
|
842
|
+
type: "text" as const,
|
|
843
|
+
text: "Chrome UX Report API is not enabled or the API key doesn't have access. Enable the API at: https://console.cloud.google.com/apis/library/chromeuxreport.googleapis.com — and ensure your API key allows it (Credentials > API key > API restrictions).",
|
|
844
|
+
},
|
|
845
|
+
],
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return { content: [{ type: "text" as const, text: `Error querying CrUX: ${msg}` }] };
|
|
543
849
|
}
|
|
544
850
|
}
|
|
545
851
|
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
}
|
|
852
|
+
// --- CrUX History action ---
|
|
853
|
+
if (resolvedAction === "crux_history") {
|
|
854
|
+
const histUrl = url;
|
|
855
|
+
const histOrigin = origin;
|
|
558
856
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
857
|
+
if (!histUrl && !histOrigin) {
|
|
858
|
+
return { content: [{ type: "text" as const, text: "Error: provide either url or origin, not both." }] };
|
|
859
|
+
}
|
|
860
|
+
if (histUrl && histOrigin) {
|
|
861
|
+
return { content: [{ type: "text" as const, text: "Error: provide either url or origin, not both." }] };
|
|
862
|
+
}
|
|
565
863
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
864
|
+
try {
|
|
865
|
+
const result = await queryCruxHistory({
|
|
866
|
+
url: histUrl,
|
|
867
|
+
origin: histOrigin,
|
|
868
|
+
formFactor: form_factor as CruxFormFactor | undefined,
|
|
869
|
+
metrics,
|
|
870
|
+
collectionPeriodCount: periods,
|
|
871
|
+
});
|
|
872
|
+
return { content: [{ type: "text" as const, text: formatCruxHistory(histUrl ?? histOrigin ?? "", result) }] };
|
|
873
|
+
} catch (err) {
|
|
874
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
875
|
+
if (msg.includes("404")) {
|
|
876
|
+
const target = histUrl ?? histOrigin ?? "";
|
|
877
|
+
const lines = [`No CrUX history data for ${target}.`, ""];
|
|
878
|
+
lines.push("CrUX requires sufficient Chrome user traffic (roughly 1,000+ monthly visits).");
|
|
879
|
+
if (histUrl) {
|
|
880
|
+
const originUrl = new URL(histUrl).origin;
|
|
881
|
+
lines.push(`Try origin-level data instead: origin "${originUrl}"`);
|
|
882
|
+
}
|
|
883
|
+
lines.push("For lab metrics without traffic requirements, use speed with a url instead.");
|
|
884
|
+
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
885
|
+
}
|
|
886
|
+
if (msg.includes("SERVICE_DISABLED")) {
|
|
887
|
+
return {
|
|
888
|
+
content: [
|
|
889
|
+
{
|
|
890
|
+
type: "text" as const,
|
|
891
|
+
text: "Chrome UX Report API is not enabled. Enable it at: https://console.cloud.google.com/apis/library/chromeuxreport.googleapis.com",
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
return { content: [{ type: "text" as const, text: `Error querying CrUX History: ${msg}` }] };
|
|
897
|
+
}
|
|
570
898
|
}
|
|
571
899
|
|
|
572
|
-
return { content: [{ type: "text", text:
|
|
900
|
+
return { content: [{ type: "text" as const, text: `Unknown action: ${resolvedAction}` }] };
|
|
573
901
|
},
|
|
574
902
|
);
|
|
575
903
|
}
|