nfunc-mcp 0.3.0 → 0.4.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 +84 -376
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mappers/labFieldComparator.d.ts +62 -0
- package/dist/mappers/labFieldComparator.js +134 -0
- package/dist/mappers/labFieldComparator.js.map +1 -0
- package/dist/mappers/psiAggregator.d.ts +130 -0
- package/dist/mappers/psiAggregator.js +293 -0
- package/dist/mappers/psiAggregator.js.map +1 -0
- package/dist/mappers/webVitalsMapper.d.ts +52 -0
- package/dist/mappers/webVitalsMapper.js +131 -0
- package/dist/mappers/webVitalsMapper.js.map +1 -0
- package/dist/tools/performanceAudit.d.ts +2 -0
- package/dist/tools/performanceAudit.js +446 -0
- package/dist/tools/performanceAudit.js.map +1 -0
- package/dist/tools/performanceAuditPlan.d.ts +2 -0
- package/dist/tools/performanceAuditPlan.js +438 -0
- package/dist/tools/performanceAuditPlan.js.map +1 -0
- package/dist/utils/csvReader.d.ts +20 -0
- package/dist/utils/csvReader.js +172 -0
- package/dist/utils/csvReader.js.map +1 -0
- package/dist/utils/httpClient.d.ts +84 -0
- package/dist/utils/httpClient.js +171 -0
- package/dist/utils/httpClient.js.map +1 -0
- package/dist/utils/psiAuth.d.ts +26 -0
- package/dist/utils/psiAuth.js +36 -0
- package/dist/utils/psiAuth.js.map +1 -0
- package/dist/utils/psiParser.d.ts +124 -0
- package/dist/utils/psiParser.js +200 -0
- package/dist/utils/psiParser.js.map +1 -0
- package/dist/utils/publicUrl.d.ts +17 -0
- package/dist/utils/publicUrl.js +115 -0
- package/dist/utils/publicUrl.js.map +1 -0
- package/dist/utils/sitemapReader.d.ts +27 -0
- package/dist/utils/sitemapReader.js +272 -0
- package/dist/utils/sitemapReader.js.map +1 -0
- package/dist/utils/urlClassifier.d.ts +45 -0
- package/dist/utils/urlClassifier.js +267 -0
- package/dist/utils/urlClassifier.js.map +1 -0
- package/docs/manual.md +558 -0
- package/docs/psi-report-spec.md +174 -0
- package/package.json +13 -3
package/docs/manual.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# nfunc-mcp — operating manual
|
|
2
|
+
|
|
3
|
+
Everything past the quick start: installation options, per-tool reference,
|
|
4
|
+
output shapes, and troubleshooting. The [README](../README.md) covers what the
|
|
5
|
+
tools are and how to ask for them; this covers how to run and interpret them.
|
|
6
|
+
|
|
7
|
+
**Contents**
|
|
8
|
+
|
|
9
|
+
1. [Prerequisites](#prerequisites)
|
|
10
|
+
2. [Install and register](#install-and-register)
|
|
11
|
+
3. [`run_qa_gate` reference](#run_qa_gate-reference)
|
|
12
|
+
4. [Individual tool reference](#individual-tool-reference)
|
|
13
|
+
5. [PSI performance audit](#psi-performance-audit)
|
|
14
|
+
6. [Priority system](#priority-system)
|
|
15
|
+
7. [Project layout](#project-layout)
|
|
16
|
+
8. [Troubleshooting](#troubleshooting)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
Four of the tools wrap CLIs. Install the ones you need:
|
|
23
|
+
|
|
24
|
+
| Tool | Install | Used by |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Lighthouse | `npm install -g lighthouse` | `run_lighthouse`, `run_qa_gate` (URL) |
|
|
27
|
+
| pa11y | `npm install -g pa11y` | `run_accessibility_check`, `run_qa_gate` (URL) |
|
|
28
|
+
| ESLint | `npm install -g eslint` | `run_static_analysis`, `run_qa_gate` (path) |
|
|
29
|
+
| Semgrep | `brew install semgrep` or `pip install semgrep` | `run_static_analysis`, `run_qa_gate` (path) |
|
|
30
|
+
|
|
31
|
+
Verify:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
lighthouse --version && pa11y --version && eslint --version && semgrep --version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**You don't need all four.** A missing tool shows `UNAVAILABLE` in the
|
|
38
|
+
scorecard and its findings are skipped; the gate still runs. URL-only runs need
|
|
39
|
+
Lighthouse and pa11y; path-only runs need ESLint and Semgrep.
|
|
40
|
+
|
|
41
|
+
### PageSpeed Insights API key
|
|
42
|
+
|
|
43
|
+
The PSI tools call an HTTP API rather than a CLI, but they want a key:
|
|
44
|
+
|
|
45
|
+
1. Enable the **PageSpeed Insights API** in the Google Cloud console.
|
|
46
|
+
2. Create an API key, restricted to that API.
|
|
47
|
+
3. Put it in the `env` block of your MCP client config — the client launches
|
|
48
|
+
this server, so it owns the environment:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
claude mcp remove nfunc-mcp -s local
|
|
52
|
+
claude mcp add nfunc-mcp -s local -e PAGESPEED_API_KEY=your_key -- npx -y nfunc-mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Confirm with any `plan_performance_audit` call: the response carries
|
|
56
|
+
`api_key: { present: true, source: "env" }` and never the key itself.
|
|
57
|
+
|
|
58
|
+
**Without a key the tools still load but are capped at 4 runs.** The shared
|
|
59
|
+
anonymous quota is exhausted in practice — expect `429 Quota exceeded` on the
|
|
60
|
+
first real request. A key gives 25,000 requests/day.
|
|
61
|
+
|
|
62
|
+
An `api_key` tool input also works but is discouraged: it lands in the
|
|
63
|
+
conversation transcript and in client logs. It is redacted from every error
|
|
64
|
+
message either way.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Install and register
|
|
69
|
+
|
|
70
|
+
### Option A — npm (recommended)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
claude mcp add nfunc-mcp -- npx -y nfunc-mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`npx` fetches and runs the server. No clone, no build.
|
|
77
|
+
|
|
78
|
+
### Option B — manual config
|
|
79
|
+
|
|
80
|
+
In `~/.claude.json` under `mcpServers`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"nfunc-mcp": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "nfunc-mcp"],
|
|
88
|
+
"env": { "PAGESPEED_API_KEY": "your_key" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Option C — from source (contributors)
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/Hiddensound/NFunc_MCP.git
|
|
98
|
+
cd NFunc_MCP
|
|
99
|
+
npm install
|
|
100
|
+
npm run build
|
|
101
|
+
claude mcp add nfunc-mcp -- node /absolute/path/to/NFunc_MCP/dist/index.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Script | Purpose |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `npm run build` | Compile TypeScript → `dist/` |
|
|
107
|
+
| `npm start` | Run the compiled server |
|
|
108
|
+
| `npm run dev` | Run from source with hot reload (`tsx watch`) |
|
|
109
|
+
|
|
110
|
+
For local development, registering `tsx src/index.ts` instead of
|
|
111
|
+
`dist/index.js` means edits need only an MCP reconnect, not a rebuild.
|
|
112
|
+
|
|
113
|
+
### Verify
|
|
114
|
+
|
|
115
|
+
1. `/mcp` in Claude Code — `nfunc-mcp` shows as connected.
|
|
116
|
+
2. Ask: *"Call the nfunc-mcp ping tool."*
|
|
117
|
+
3. Expect `{ "status": "ok", "timestamp": "..." }`
|
|
118
|
+
|
|
119
|
+
After changing the server's code or environment, reconnect via `/mcp` — the
|
|
120
|
+
running process does not pick up changes on its own.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## `run_qa_gate` reference
|
|
125
|
+
|
|
126
|
+
### Inputs
|
|
127
|
+
|
|
128
|
+
Both `url` and `path` are optional; at least one is required.
|
|
129
|
+
|
|
130
|
+
| Input | Type | When to provide |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `url` | string (URL) | A running page — production, staging, preview, or localhost. Enables Lighthouse and pa11y. |
|
|
133
|
+
| `path` | string (path) | A local codebase. Enables ESLint and Semgrep. |
|
|
134
|
+
| `context` | string | Optional free-text description (e.g. `"React e-commerce checkout"`). Helps interpretation. |
|
|
135
|
+
| `form_factor` | `mobile` \| `desktop` \| `both` | Optional, default `mobile`. See [mobile vs desktop](#mobile-vs-desktop). |
|
|
136
|
+
| `a11y_runner` | `htmlcs` \| `axe` \| `both` | Optional, default `htmlcs`. See [choosing an engine](#choosing-an-accessibility-engine). |
|
|
137
|
+
|
|
138
|
+
### Release readiness tiers
|
|
139
|
+
|
|
140
|
+
| Value | Meaning | Condition |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `BLOCKED` | Cannot ship | Any P1 finding |
|
|
143
|
+
| `CONDITIONAL` | Shippable with caveats | P2 findings, no P1s |
|
|
144
|
+
| `ADVISORY` | Safe to ship; P3s are tech debt | Only P3 findings |
|
|
145
|
+
| `CLEAR` | No issues detected | Zero findings |
|
|
146
|
+
|
|
147
|
+
### Composite score
|
|
148
|
+
|
|
149
|
+
A single 0–100 health measure. Start at 100 and deduct: **P1 −15, P2 −7,
|
|
150
|
+
P3 −3**, floored at 0. Most useful as a trend line across sprints rather than
|
|
151
|
+
as an absolute grade.
|
|
152
|
+
|
|
153
|
+
### Scorecard
|
|
154
|
+
|
|
155
|
+
One line per tool:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
[
|
|
159
|
+
{ "tool": "Lighthouse", "gate": "WARN", "score": 75,
|
|
160
|
+
"breakdown": { "performance": 52, "accessibility": 98, "seo": 100, "best-practices": 58 } },
|
|
161
|
+
{ "tool": "pa11y", "gate": "PASS", "issues": 0 },
|
|
162
|
+
{ "tool": "ESLint / Semgrep", "gate": "SKIPPED" }
|
|
163
|
+
]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Gate | Meaning |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `PASS` | No issues at this tool's threshold |
|
|
169
|
+
| `WARN` | Issues below the FAIL threshold |
|
|
170
|
+
| `FAIL` | P1-level issues (or Lighthouse average < 50) |
|
|
171
|
+
| `SKIPPED` | Input not provided |
|
|
172
|
+
| `UNAVAILABLE` | Tool invoked but not installed, or errored |
|
|
173
|
+
|
|
174
|
+
### Cross-tool corroboration
|
|
175
|
+
|
|
176
|
+
When Lighthouse and pa11y independently flag the same accessibility gap, the
|
|
177
|
+
findings are **merged** into `corroborated_findings`, **promoted one tier**
|
|
178
|
+
(P3→P2, P2→P1), and annotated `confidence: "high"` with
|
|
179
|
+
`confirmed_by: ["lighthouse", "pa11y"]`.
|
|
180
|
+
|
|
181
|
+
Two independent tools agreeing is stronger evidence than either alone, so these
|
|
182
|
+
appear above all other findings and bubble to the top of `top_issues`.
|
|
183
|
+
|
|
184
|
+
**Rule 1 — accessibility mapping:**
|
|
185
|
+
|
|
186
|
+
| Lighthouse audit | pa11y technique |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `color-contrast` | `.G18`, `.G145`, `.G174` |
|
|
189
|
+
| `image-alt` | `.H37`, `.H67`, `.F65` |
|
|
190
|
+
| `label` | `.H44`, `.F68`, `.H91.Input` |
|
|
191
|
+
| `link-name` | `.H30`, `.H91.A.` |
|
|
192
|
+
| `html-has-lang` | `.H57` |
|
|
193
|
+
| `button-name` | `.H91.Button` |
|
|
194
|
+
| *(and more)* | |
|
|
195
|
+
|
|
196
|
+
**Rule 2 — performance ↔ code:** when a Lighthouse performance finding's
|
|
197
|
+
display value contains a filename that also appears in a static analysis
|
|
198
|
+
finding, the static finding is attached as `related_findings`. They are linked
|
|
199
|
+
by reference, not merged.
|
|
200
|
+
|
|
201
|
+
### HTML report
|
|
202
|
+
|
|
203
|
+
Every call writes a self-contained HTML file to `/tmp` and returns its path as
|
|
204
|
+
`report_file`. It contains the readiness banner, a composite-score gauge,
|
|
205
|
+
the scorecard, cross-confirmed findings, and collapsible finding cards grouped
|
|
206
|
+
by priority. Inline CSS, no server needed.
|
|
207
|
+
|
|
208
|
+
### Output shape
|
|
209
|
+
|
|
210
|
+
```jsonc
|
|
211
|
+
{
|
|
212
|
+
"release_readiness": "BLOCKED", // BLOCKED | CONDITIONAL | ADVISORY | CLEAR
|
|
213
|
+
"composite_score": 22, // 0–100
|
|
214
|
+
"report_file": "file:///tmp/qa-report-xxx.html",
|
|
215
|
+
"scorecard": [ ... ],
|
|
216
|
+
"eslint_config_used": "project", // only when path was supplied
|
|
217
|
+
"summary": "110 findings (101 P1, 9 P2) across 2 tools. ...",
|
|
218
|
+
"corroborated_findings": [ ... ], // cross-confirmed, confidence: "high"
|
|
219
|
+
"top_issues": [ ... ], // top 3 (corroborated first)
|
|
220
|
+
"all_findings": [ ... ], // all, sorted by priority
|
|
221
|
+
"correlations_found": 1,
|
|
222
|
+
"errors": [ ... ] // only if a tool errored
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Each finding:
|
|
227
|
+
|
|
228
|
+
```jsonc
|
|
229
|
+
{
|
|
230
|
+
"priority": "P1",
|
|
231
|
+
"title": "Largest Contentful Paint",
|
|
232
|
+
"description": "Users see main content 34s after navigation...",
|
|
233
|
+
"evidence": { "audit_id": "largest-contentful-paint", "value": "34.3 s" },
|
|
234
|
+
"source_tool": "lighthouse",
|
|
235
|
+
// corroborated findings also carry:
|
|
236
|
+
"confirmed_by": ["lighthouse", "pa11y"],
|
|
237
|
+
"confidence": "high"
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Individual tool reference
|
|
244
|
+
|
|
245
|
+
### `run_lighthouse`
|
|
246
|
+
|
|
247
|
+
Returns `url`, `form_factor`, `scores` per category, `ttfb_ms`, and
|
|
248
|
+
priority-ordered `findings`.
|
|
249
|
+
|
|
250
|
+
#### Mobile vs desktop
|
|
251
|
+
|
|
252
|
+
`form_factor` accepts `desktop` (default), `mobile`, or `both`.
|
|
253
|
+
|
|
254
|
+
**This default deliberately differs from the Lighthouse CLI's**, which is
|
|
255
|
+
mobile: a 412×823 screen, mid-range Android user agent, simulated slow 4G, and
|
|
256
|
+
a **4× CPU slowdown**. That profile reports substantially lower performance
|
|
257
|
+
scores for the same page, so `mobile` here is not a like-for-like comparison
|
|
258
|
+
with a default CLI run — check `form_factor` in the response before comparing
|
|
259
|
+
two reports.
|
|
260
|
+
|
|
261
|
+
**The two are not interchangeable.** They render different DOM, so they find
|
|
262
|
+
different defects, not just different numbers. On one commerce category page:
|
|
263
|
+
|
|
264
|
+
| | Mobile | Desktop |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| performance | 54 | 62 |
|
|
267
|
+
| accessibility | **87** | **73** |
|
|
268
|
+
| seo | 77 | 69 |
|
|
269
|
+
|
|
270
|
+
Five accessibility audits failed on desktop that mobile never reported —
|
|
271
|
+
`image-alt`, `aria-required-children`, `aria-required-parent`,
|
|
272
|
+
`aria-allowed-attr`, `aria-valid-attr-value` — while three others failed only
|
|
273
|
+
on mobile. Neither profile is a superset of the other.
|
|
274
|
+
|
|
275
|
+
With `both`, the two run concurrently (little more wall time than one), `scores`
|
|
276
|
+
is keyed by form factor, and each finding carries `affects_form_factors` and
|
|
277
|
+
`form_factor_specific`.
|
|
278
|
+
|
|
279
|
+
### `run_accessibility_check`
|
|
280
|
+
|
|
281
|
+
pa11y at WCAG 2 AA by default, violations only. Returns `url`, `standard`,
|
|
282
|
+
`runners`, `violation_count`, `raw_violation_count`, `findings`.
|
|
283
|
+
|
|
284
|
+
`raw_violation_count` versus `violation_count` shows the dedup at work: a rule
|
|
285
|
+
failing across many elements collapses into one systemic finding rather than
|
|
286
|
+
one per element.
|
|
287
|
+
|
|
288
|
+
#### Choosing an accessibility engine
|
|
289
|
+
|
|
290
|
+
| Engine | Strongest at | Severity source |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `htmlcs` (default) | WCAG techniques, document structure, form labelling, duplicate ids | WCAG technique class |
|
|
293
|
+
| `axe` | **ARIA** — invalid roles, missing required parent/child relationships, prohibited and unsupported attributes — and computed colour contrast | axe's own `impact` |
|
|
294
|
+
|
|
295
|
+
**Reach for `axe` whenever the work involves ARIA, a component library, or a
|
|
296
|
+
design system.** The overlap is smaller than expected. On the same page:
|
|
297
|
+
|
|
298
|
+
- htmlcs found unlabelled inputs, forms with no submit mechanism, and ten
|
|
299
|
+
duplicate ids that axe did not report.
|
|
300
|
+
- axe found `aria-allowed-attr`, `aria-prohibited-attr`,
|
|
301
|
+
`aria-required-parent`, `aria-required-children` and `image-alt` failures
|
|
302
|
+
htmlcs missed entirely.
|
|
303
|
+
|
|
304
|
+
`both` runs them concurrently and merges. An element flagged by both appears
|
|
305
|
+
twice, because they emit different rule codes — deliberate, since two engines
|
|
306
|
+
agreeing is corroboration worth seeing.
|
|
307
|
+
|
|
308
|
+
axe findings carry `axe_impact`, and `needs_manual_review: true` where axe
|
|
309
|
+
wants human confirmation. Those are demoted one tier: a maybe should not gate a
|
|
310
|
+
release as hard as a certainty.
|
|
311
|
+
|
|
312
|
+
### `run_static_analysis`
|
|
313
|
+
|
|
314
|
+
ESLint and Semgrep in parallel against a local directory. Uses the project's own
|
|
315
|
+
ESLint config when it finds one, otherwise a QA-focused baseline. Returns
|
|
316
|
+
`path`, `tools_run`, `eslint_config_used`, `issue_count`, `findings`,
|
|
317
|
+
`warnings`.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## PSI performance audit
|
|
322
|
+
|
|
323
|
+
`plan_performance_audit` and `run_performance_audit` wrap the Google PageSpeed
|
|
324
|
+
Insights API. They are **exclusive and opt-in** — `run_qa_gate` never calls
|
|
325
|
+
them. Use them when someone asks for a PSI audit, a Core Web Vitals report, or
|
|
326
|
+
real-user field data.
|
|
327
|
+
|
|
328
|
+
### Why two tools
|
|
329
|
+
|
|
330
|
+
An MCP tool cannot ask a question mid-call, and a useful audit needs decisions
|
|
331
|
+
first — which URLs, how many per template, what to do about pages PSI cannot
|
|
332
|
+
reach. So:
|
|
333
|
+
|
|
334
|
+
1. `plan_performance_audit` discovers, classifies and costs the run, then
|
|
335
|
+
returns a `questions` array. **Spends no quota.**
|
|
336
|
+
2. Those questions go to the user.
|
|
337
|
+
3. `run_performance_audit` executes the approved page list.
|
|
338
|
+
|
|
339
|
+
A misclassified template should cost a conversation turn, not forty API calls.
|
|
340
|
+
|
|
341
|
+
### What PSI adds over `run_lighthouse`
|
|
342
|
+
|
|
343
|
+
One call returns two independent datasets: a Lighthouse run on Google's
|
|
344
|
+
infrastructure (**lab**) and Chrome UX Report data for the URL (**field** — real
|
|
345
|
+
users, 28-day 75th percentile). `run_lighthouse` gives you the first. Only PSI
|
|
346
|
+
gives the second, and the disagreement is the point:
|
|
347
|
+
|
|
348
|
+
| Lab | Field | Meaning |
|
|
349
|
+
|---|---|---|
|
|
350
|
+
| Pass | Pass | Genuinely fine |
|
|
351
|
+
| Fail | Pass | Lab profile harsher than the real audience — deprioritise |
|
|
352
|
+
| **Pass** | **Fail** | **The test environment is lying to you.** Real users hit something the simulation does not |
|
|
353
|
+
| Fail | Fail | Confirmed by two independent measurements |
|
|
354
|
+
|
|
355
|
+
Row three is invisible to every other tool here. On one commerce homepage the
|
|
356
|
+
lab reported a perfect CLS of 0 while real users were at 0.55 — 5.5× the "poor"
|
|
357
|
+
threshold, affecting 70% of them. On another site the lab reported TTFB of 2 ms
|
|
358
|
+
(Google's network sits next to the origin) against 1.5 s in the field.
|
|
359
|
+
|
|
360
|
+
CrUX is **not real-time** — it is a 28-day trailing aggregate. It is valuable
|
|
361
|
+
because it is real users, not because it is current.
|
|
362
|
+
|
|
363
|
+
### Where PSI does not work
|
|
364
|
+
|
|
365
|
+
- **Localhost and private hosts** — PSI fetches from Google's infrastructure.
|
|
366
|
+
Rejected at preflight; use `run_lighthouse`.
|
|
367
|
+
- **Cart, checkout, account pages** — PSI fetches anonymously, so it would
|
|
368
|
+
measure an empty cart or a login redirect. The plan tool flags these and
|
|
369
|
+
routes them to `run_lighthouse`, which can carry session cookies.
|
|
370
|
+
- **Low-traffic URLs** — reachable, but with little or no CrUX data. You get a
|
|
371
|
+
lab-only audit, labelled as such. Staging and preview deployments are always
|
|
372
|
+
in this category, which is why PSI is optional for non-production and
|
|
373
|
+
authoritative for hosted sites.
|
|
374
|
+
|
|
375
|
+
### URL discovery
|
|
376
|
+
|
|
377
|
+
`discovery` accepts:
|
|
378
|
+
|
|
379
|
+
| Mode | Behaviour |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `sitemap` (default) | robots.txt, then `/sitemap.xml` and `/sitemap_index.xml`, then `<link rel="sitemap">`, then seven common CMS locations. Tiers only advance when earlier ones find nothing. |
|
|
382
|
+
| `list` | Explicit `urls` array. |
|
|
383
|
+
| `csv` | `csv_path` — column detected by name or by content. An analytics top-pages export is the best input for a performance audit, being traffic-weighted. |
|
|
384
|
+
| `crawl` | Not implemented, and [declined deliberately](../performance-audit-plan.md) — static crawling finds 7–11 internal links on modern commerce homepages. |
|
|
385
|
+
|
|
386
|
+
Discovered URLs are clustered into **templates** by path shape, and the plan
|
|
387
|
+
tool proposes representative samples per template. Passing a URL with a path
|
|
388
|
+
(e.g. `https://site.com/shop/`) scopes discovery to that subtree.
|
|
389
|
+
|
|
390
|
+
### Chunking and time
|
|
391
|
+
|
|
392
|
+
PSI is slow and erratic: measured latency on live runs ranged from **10 s to
|
|
393
|
+
57 s for the same URL**, with occasional hangs and intermittent 500s. Roughly
|
|
394
|
+
one run in three failed on one origin.
|
|
395
|
+
|
|
396
|
+
So `run_performance_audit` runs in chunks. Each call is bounded by
|
|
397
|
+
`max_seconds_per_call` (default 150) and returns a `cursor`; keep calling until
|
|
398
|
+
`complete` is true. Raw reports are written to `output_dir` as they land and
|
|
399
|
+
merged into `_index.json`, so nothing completed is ever lost.
|
|
400
|
+
|
|
401
|
+
**Re-run to fill gaps.** Call again with the same pages and *no cursor* —
|
|
402
|
+
completed page/strategy pairs are skipped automatically, so only failures are
|
|
403
|
+
retried. Pass `skip_completed: false` to force fresh measurements.
|
|
404
|
+
|
|
405
|
+
`runs_per_url` defaults to 1. Raising it to 3 takes the median run and removes
|
|
406
|
+
single-run outlier risk, at three times the wall clock.
|
|
407
|
+
|
|
408
|
+
### Reading the output
|
|
409
|
+
|
|
410
|
+
The final call returns an `aggregate` block with every cross-page number:
|
|
411
|
+
per-strategy means, `lab_metric_failures`, CWV verdict tallies,
|
|
412
|
+
`lab_vs_field_summary`, and outliers. **Quote those rather than recomputing
|
|
413
|
+
them.**
|
|
414
|
+
|
|
415
|
+
Two redundancy rules apply there:
|
|
416
|
+
|
|
417
|
+
- **Systemic collapse** — a vital failing on 80%+ of runs *with little
|
|
418
|
+
variation between pages* collapses into one site-wide finding. Computed only
|
|
419
|
+
from URL-level field data, since origin-level CrUX is one number repeated and
|
|
420
|
+
cannot evidence a claim about variation.
|
|
421
|
+
- **Component suppression** — FCP is folded into LCP when both fail on a page,
|
|
422
|
+
since FCP is a component of LCP rather than an independent defect.
|
|
423
|
+
|
|
424
|
+
`field_source` on every field metric says whether the number describes the URL
|
|
425
|
+
or the whole origin. **PSI substitutes origin data silently**, so trust that
|
|
426
|
+
field rather than the presence of a populated field block.
|
|
427
|
+
|
|
428
|
+
[`psi-report-spec.md`](psi-report-spec.md) is the full guide to turning this
|
|
429
|
+
output into a written report.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Priority system
|
|
434
|
+
|
|
435
|
+
| Priority | Meaning | Lighthouse | WCAG | ESLint / Semgrep | CrUX field |
|
|
436
|
+
|---|---|---|---|---|---|
|
|
437
|
+
| P1 | Blocker — fix before shipping | Score < 50 | Level A | Semgrep security, ESLint error | Core vital rated poor |
|
|
438
|
+
| P2 | Warning — track before merging | 50–79 | Level AA | ESLint warning | Needs improvement, or any diagnostic |
|
|
439
|
+
| P3 | Advisory — log as tech debt | 80–89 | Level AAA | — | — |
|
|
440
|
+
| *(suppressed)* | Passing — never reported | ≥ 90 | — | — | Good |
|
|
441
|
+
|
|
442
|
+
Lighthouse findings are actually ranked by `weight × (1 − score)` — the category
|
|
443
|
+
points an audit really costs — rather than by score alone, so a weight-30 metric
|
|
444
|
+
failing outright outranks a weight-1 SEO check that also scores 0.
|
|
445
|
+
|
|
446
|
+
Adjustments:
|
|
447
|
+
|
|
448
|
+
- **Corroborated** findings (two tools agreeing) are promoted one tier.
|
|
449
|
+
- **Field-confirmed** findings are promoted one tier; **lab-only** findings
|
|
450
|
+
contradicted by healthy field data are demoted and tagged `lab_only`.
|
|
451
|
+
- **Non-core vitals** (FCP, TTFB) never exceed P2 — they explain a Core Web
|
|
452
|
+
Vital rather than being one.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Project layout
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
├── src/
|
|
460
|
+
│ ├── index.ts # MCP server bootstrap + tool registration
|
|
461
|
+
│ ├── types.ts # Shared types (Finding, Priority)
|
|
462
|
+
│ ├── config/
|
|
463
|
+
│ │ └── qa-mcp-baseline.eslint.config.js # Fallback ESLint config
|
|
464
|
+
│ ├── tools/ # One file per MCP tool
|
|
465
|
+
│ │ ├── qaGate.ts # Orchestrator
|
|
466
|
+
│ │ ├── lighthouse.ts
|
|
467
|
+
│ │ ├── accessibility.ts
|
|
468
|
+
│ │ ├── staticAnalysis.ts
|
|
469
|
+
│ │ ├── performanceAuditPlan.ts # plan_performance_audit
|
|
470
|
+
│ │ └── performanceAudit.ts # run_performance_audit
|
|
471
|
+
│ ├── mappers/ # Raw output → QA report shape
|
|
472
|
+
│ │ ├── correlator.ts # Cross-tool correlation (Rule 1 + 2)
|
|
473
|
+
│ │ ├── defectFormatter.ts # Findings and defect prose
|
|
474
|
+
│ │ ├── priorityMapper.ts # Score/severity → P1/P2/P3
|
|
475
|
+
│ │ ├── a11yDedupe.ts # Systemic a11y collapse
|
|
476
|
+
│ │ ├── compositeScore.ts # Per-tool sub-scores
|
|
477
|
+
│ │ ├── webVitalsMapper.ts # CrUX thresholds → priorities → prose
|
|
478
|
+
│ │ ├── labFieldComparator.ts # Lab vs field verdicts
|
|
479
|
+
│ │ └── psiAggregator.ts # Cross-run arithmetic + redundancy rules
|
|
480
|
+
│ └── utils/ # Cross-tool helpers
|
|
481
|
+
│ ├── shellRunner.ts # Subprocess choke point
|
|
482
|
+
│ ├── httpClient.ts # HTTP choke point (retry, deadline, redaction)
|
|
483
|
+
│ ├── reportGenerator.ts # HTML report builder
|
|
484
|
+
│ ├── outputParsers.ts # Per-tool JSON parsers
|
|
485
|
+
│ ├── psiParser.ts # PSI response → lab + field
|
|
486
|
+
│ ├── psiAuth.ts # API key resolution
|
|
487
|
+
│ ├── sitemapReader.ts # Tiered sitemap discovery
|
|
488
|
+
│ ├── urlClassifier.ts # URL list → page templates
|
|
489
|
+
│ ├── csvReader.ts # URL extraction from CSV
|
|
490
|
+
│ ├── publicUrl.ts # Reachability + session-gate checks
|
|
491
|
+
│ ├── eslintConfigDetector.ts
|
|
492
|
+
│ └── toolResponse.ts
|
|
493
|
+
├── docs/
|
|
494
|
+
│ ├── manual.md # This file
|
|
495
|
+
│ └── psi-report-spec.md # How to write the PSI audit report
|
|
496
|
+
└── dist/ # Compiled output (gitignored)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
External calls go through exactly two choke points: `shellRunner` for
|
|
500
|
+
subprocesses, `httpClient` for HTTP. Anything holding an API key must use
|
|
501
|
+
`httpClient`, which redacts secrets from every error.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Troubleshooting
|
|
506
|
+
|
|
507
|
+
### The new tools don't appear after an update
|
|
508
|
+
|
|
509
|
+
Reconnect: `/mcp` → the server → **Reconnect**. The running process does not
|
|
510
|
+
reload code or environment changes on its own.
|
|
511
|
+
|
|
512
|
+
### `429 Quota exceeded` on the first PSI call
|
|
513
|
+
|
|
514
|
+
No API key. The shared anonymous quota is exhausted in practice, so keyless PSI
|
|
515
|
+
fails immediately rather than working slowly. See
|
|
516
|
+
[the key setup](#pagespeed-insights-api-key).
|
|
517
|
+
|
|
518
|
+
### A PSI call gets "moved to the background"
|
|
519
|
+
|
|
520
|
+
Claude Code backgrounds an MCP call at 120 s, and the default
|
|
521
|
+
`max_seconds_per_call` is 150. Results still arrive by notification. To keep
|
|
522
|
+
runs in the foreground, pass `max_seconds_per_call: 100` or lower.
|
|
523
|
+
|
|
524
|
+
### PSI runs fail intermittently
|
|
525
|
+
|
|
526
|
+
Expected — roughly one in three on some origins, as PSI 500s or hangs. Call the
|
|
527
|
+
tool again with the same pages and no cursor; completed pairs are skipped and
|
|
528
|
+
only the gaps are retried.
|
|
529
|
+
|
|
530
|
+
### The plan tool warns about HTTP 403
|
|
531
|
+
|
|
532
|
+
A bot wall (Cloudflare and similar) blocking the preflight. It says nothing
|
|
533
|
+
about PSI, which fetches from Google's address space and is usually
|
|
534
|
+
allowlisted. Not a reason to stop — but if the audit returns nothing, the
|
|
535
|
+
challenge page is the first suspect.
|
|
536
|
+
|
|
537
|
+
### "No sitemap found"
|
|
538
|
+
|
|
539
|
+
Nine locations were tried. Supply URLs with `discovery: "list"`, or point at a
|
|
540
|
+
CSV export — an analytics top-pages export is a better input for a performance
|
|
541
|
+
audit anyway, being weighted by real traffic.
|
|
542
|
+
|
|
543
|
+
### Field data says "site-wide data"
|
|
544
|
+
|
|
545
|
+
That URL has too little traffic for its own CrUX entry, so the numbers describe
|
|
546
|
+
the whole origin. They are still real, but they do not describe the page you
|
|
547
|
+
asked about. Two different URLs reporting identical p75 values are both being
|
|
548
|
+
served origin data.
|
|
549
|
+
|
|
550
|
+
### A tool shows `UNAVAILABLE`
|
|
551
|
+
|
|
552
|
+
Its CLI is not on PATH. Install it (see
|
|
553
|
+
[prerequisites](#prerequisites)) or ignore it — the rest of the gate still runs.
|
|
554
|
+
|
|
555
|
+
### Lighthouse scores look far worse than expected
|
|
556
|
+
|
|
557
|
+
Check `form_factor`. The mobile profile applies a 4× CPU slowdown and simulated
|
|
558
|
+
slow 4G; it is not comparable to a desktop run of the same page.
|