Conversation
Adds a new standalone MCP agent package (`packages/web-perf/`) that monitors website performance using Chrome UX Report field data and PageSpeed Insights lab tests. Includes 7 tools (SITE_ADD, SITE_LIST, SITE_GET, SITE_DELETE, PERF_SNAPSHOT, PERF_REPORT, CRUX_HISTORY), 2 prompts (initial-setup, performance-audit), and native UI with CWV gauges, histogram bars, trend sparklines, and opportunity tables. Registers as a well-known agent template with a one-click setup modal in the home page, agents list, and sidebar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsSuggested: Minor ( React with an emoji to override the release type:
Current version:
|
There was a problem hiding this comment.
11 issues found across 26 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/sidebar/agents-section.tsx">
<violation number="1" location="apps/mesh/src/web/components/sidebar/agents-section.tsx:360">
P2: Match Web Performance by title as well as metadata.type so already-installed agents aren’t missed and re-created.</violation>
</file>
<file name="packages/web-perf/server/ui/site-detail.ts">
<violation number="1" location="packages/web-perf/server/ui/site-detail.ts:310">
P1: Unsanitized user/API strings are injected via `innerHTML`, enabling XSS in the site detail view.</violation>
</file>
<file name="packages/web-perf/server/ui/dashboard.ts">
<violation number="1" location="packages/web-perf/server/ui/dashboard.ts:116">
P1: Escape `s.name` and `s.origin` before concatenating into HTML. They are currently injected into `innerHTML` unsafely, which allows script/markup injection.</violation>
</file>
<file name="packages/web-perf/server/tools/site-add.ts">
<violation number="1" location="packages/web-perf/server/tools/site-add.ts:28">
P2: `SITE_ADD` validates a URL but does not normalize it to an actual origin. Paths/query/hash are persisted as `origin`, which breaks downstream CrUX origin requests.</violation>
</file>
<file name="packages/web-perf/server/lib/report.ts">
<violation number="1" location="packages/web-perf/server/lib/report.ts:53">
P2: Do not require LCP to exist before building trend summary; it suppresses valid trends for other available metrics.</violation>
<violation number="2" location="packages/web-perf/server/lib/report.ts:68">
P2: Guard against zero historical averages before calculating percentage change to avoid NaN/Infinity trend output.</violation>
<violation number="3" location="packages/web-perf/server/lib/report.ts:108">
P1: Use an explicit undefined check for `performanceScore`; the current truthy check drops valid score `0`.</violation>
</file>
<file name="packages/web-perf/server/lib/types.ts">
<violation number="1" location="packages/web-perf/server/lib/types.ts:153">
P1: Expose a public site shape here instead of `SiteConfig`; the report currently returns the full `TrackedSite`, which can leak the stored API key and unnecessary snapshot history.</violation>
</file>
<file name="packages/web-perf/server/ui/shared-styles.ts">
<violation number="1" location="packages/web-perf/server/ui/shared-styles.ts:109">
P1: Validate `postMessage` origin/source before processing JSON-RPC messages; currently any origin can inject events into the bridge.</violation>
</file>
<file name="packages/web-perf/server/lib/storage.ts">
<violation number="1" location="packages/web-perf/server/lib/storage.ts:13">
P1: Validate `id` before building the filename; current path construction allows traversal outside the sites directory when `siteId` contains `..` or separators.</violation>
</file>
<file name="packages/web-perf/server/tools/site-get.ts">
<violation number="1" location="packages/web-perf/server/tools/site-get.ts:24">
P0: Strip the persisted API key before returning the site object; otherwise SITE_GET leaks the saved Google API key to callers.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if (!site) { | ||
| throw new Error(`Site not found: ${context.siteId}`); | ||
| } | ||
| return { site }; |
There was a problem hiding this comment.
P0: Strip the persisted API key before returning the site object; otherwise SITE_GET leaks the saved Google API key to callers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/tools/site-get.ts, line 24:
<comment>Strip the persisted API key before returning the site object; otherwise SITE_GET leaks the saved Google API key to callers.</comment>
<file context>
@@ -0,0 +1,26 @@
+ if (!site) {
+ throw new Error(`Site not found: ${context.siteId}`);
+ }
+ return { site };
+ },
+});
</file context>
| } | ||
| html += '</div>'; | ||
|
|
||
| root.innerHTML = html; |
There was a problem hiding this comment.
P1: Unsanitized user/API strings are injected via innerHTML, enabling XSS in the site detail view.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/ui/site-detail.ts, line 310:
<comment>Unsanitized user/API strings are injected via `innerHTML`, enabling XSS in the site detail view.</comment>
<file context>
@@ -0,0 +1,346 @@
+ }
+ html += '</div>';
+
+ root.innerHTML = html;
+}
+
</file context>
| } | ||
|
|
||
| h += '<div style="flex:1;min-width:0">' + | ||
| '<div class="font-semibold truncate">' + s.name + '</div>' + |
There was a problem hiding this comment.
P1: Escape s.name and s.origin before concatenating into HTML. They are currently injected into innerHTML unsafely, which allows script/markup injection.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/ui/dashboard.ts, line 116:
<comment>Escape `s.name` and `s.origin` before concatenating into HTML. They are currently injected into `innerHTML` unsafely, which allows script/markup injection.</comment>
<file context>
@@ -0,0 +1,156 @@
+ }
+
+ h += '<div style="flex:1;min-width:0">' +
+ '<div class="font-semibold truncate">' + s.name + '</div>' +
+ '<div class="site-origin truncate">' + s.origin + '</div>';
+
</file context>
|
|
||
| const performanceScore = latest.pagespeed?.performanceScore; | ||
| const metricRatings = metrics.map((m) => m.rating); | ||
| const scoreRating = performanceScore |
There was a problem hiding this comment.
P1: Use an explicit undefined check for performanceScore; the current truthy check drops valid score 0.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/lib/report.ts, line 108:
<comment>Use an explicit undefined check for `performanceScore`; the current truthy check drops valid score `0`.</comment>
<file context>
@@ -0,0 +1,164 @@
+
+ const performanceScore = latest.pagespeed?.performanceScore;
+ const metricRatings = metrics.map((m) => m.rating);
+ const scoreRating = performanceScore
+ ? ratePerformanceScore(performanceScore)
+ : undefined;
</file context>
| } | ||
|
|
||
| export interface ReportData { | ||
| site: SiteConfig; |
There was a problem hiding this comment.
P1: Expose a public site shape here instead of SiteConfig; the report currently returns the full TrackedSite, which can leak the stored API key and unnecessary snapshot history.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/lib/types.ts, line 153:
<comment>Expose a public site shape here instead of `SiteConfig`; the report currently returns the full `TrackedSite`, which can leak the stored API key and unnecessary snapshot history.</comment>
<file context>
@@ -0,0 +1,168 @@
+}
+
+export interface ReportData {
+ site: SiteConfig;
+ overallRating: Rating;
+ performanceScore?: number;
</file context>
| } | ||
|
|
||
| function sitePath(id: string): string { | ||
| return join(SITES_DIR, `${id}.json`); |
There was a problem hiding this comment.
P1: Validate id before building the filename; current path construction allows traversal outside the sites directory when siteId contains .. or separators.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/lib/storage.ts, line 13:
<comment>Validate `id` before building the filename; current path construction allows traversal outside the sites directory when `siteId` contains `..` or separators.</comment>
<file context>
@@ -0,0 +1,77 @@
+}
+
+function sitePath(id: string): string {
+ return join(SITES_DIR, `${id}.json`);
+}
+
</file context>
| const webPerfTemplate = WELL_KNOWN_AGENT_TEMPLATES.find( | ||
| (t) => t.id === "web-perf", | ||
| ); | ||
| const existingWebPerf = webPerfTemplate |
There was a problem hiding this comment.
P2: Match Web Performance by title as well as metadata.type so already-installed agents aren’t missed and re-created.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sidebar/agents-section.tsx, line 360:
<comment>Match Web Performance by title as well as metadata.type so already-installed agents aren’t missed and re-created.</comment>
<file context>
@@ -350,6 +353,18 @@ function PinAgentPopoverContent({
+ const webPerfTemplate = WELL_KNOWN_AGENT_TEMPLATES.find(
+ (t) => t.id === "web-perf",
+ );
+ const existingWebPerf = webPerfTemplate
+ ? allAgents.find(
+ (a) =>
</file context>
| }), | ||
| execute: async ({ context }) => { | ||
| // Normalize origin: remove trailing slash | ||
| const origin = context.origin.replace(/\/+$/, ""); |
There was a problem hiding this comment.
P2: SITE_ADD validates a URL but does not normalize it to an actual origin. Paths/query/hash are persisted as origin, which breaks downstream CrUX origin requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/tools/site-add.ts, line 28:
<comment>`SITE_ADD` validates a URL but does not normalize it to an actual origin. Paths/query/hash are persisted as `origin`, which breaks downstream CrUX origin requests.</comment>
<file context>
@@ -0,0 +1,59 @@
+ }),
+ execute: async ({ context }) => {
+ // Normalize origin: remove trailing slash
+ const origin = context.origin.replace(/\/+$/, "");
+
+ try {
</file context>
|
|
||
| function describeTrend(site: TrackedSite): string | undefined { | ||
| const history = site.cruxHistory; | ||
| if (!history || !history.record.lcp) return undefined; |
There was a problem hiding this comment.
P2: Do not require LCP to exist before building trend summary; it suppresses valid trends for other available metrics.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/lib/report.ts, line 53:
<comment>Do not require LCP to exist before building trend summary; it suppresses valid trends for other available metrics.</comment>
<file context>
@@ -0,0 +1,164 @@
+
+function describeTrend(site: TrackedSite): string | undefined {
+ const history = site.cruxHistory;
+ if (!history || !history.record.lcp) return undefined;
+
+ const lines: string[] = [];
</file context>
|
|
||
| const recentAvg = recent.reduce((s, v) => s + v, 0) / recent.length; | ||
| const olderAvg = older.reduce((s, v) => s + v, 0) / older.length; | ||
| const change = ((recentAvg - olderAvg) / olderAvg) * 100; |
There was a problem hiding this comment.
P2: Guard against zero historical averages before calculating percentage change to avoid NaN/Infinity trend output.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web-perf/server/lib/report.ts, line 68:
<comment>Guard against zero historical averages before calculating percentage change to avoid NaN/Infinity trend output.</comment>
<file context>
@@ -0,0 +1,164 @@
+
+ const recentAvg = recent.reduce((s, v) => s + v, 0) / recent.length;
+ const olderAvg = older.reduce((s, v) => s + v, 0) / older.length;
+ const change = ((recentAvg - olderAvg) / olderAvg) * 100;
+
+ let direction: string;
</file context>
Summary
packages/web-perf/— standalone MCP agent for website performance monitoring using Chrome UX Report (CrUX) field data and PageSpeed Insights lab datainitial-setup(full onboarding flow) andperformance-audit(deep analysis with prioritized fixes)~/.deco/web-perf/sites/Test plan
bun run --cwd packages/web-perf devto start the MCP serverinitial-setupprompt with a URL and Google API keybun run fmtandbun run check— both pass🤖 Generated with Claude Code
Summary by cubic
Add a new Web Performance agent that monitors sites using CrUX field data and PageSpeed Insights lab tests, with native dashboards and a one‑click setup flow. Registers a new agent template and recruit modal across the home screen, agents list, and sidebar.
New Features
@decocms/web-perf: standalone MCP server athttp://localhost:3002/mcpwith a small REST API at/api/sites.initial-setup,performance-audit.~/.deco/web-perf/sites/for tracked sites and snapshots.WELL_KNOWN_AGENT_TEMPLATES, and default pinned Dashboard view.Migration
bun run --cwd packages/web-perf dev(defaults to port 3002).GOOGLE_API_KEY(enable CrUX API and PageSpeed Insights API).initial-setupprompt to add a site, snapshot data, fetch history, and generate a report.Written for commit eb0a0c6. Summary will update on new commits.