nfunc-mcp 0.3.0 → 0.5.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 +121 -379
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mappers/a11yDedupe.js +38 -7
- package/dist/mappers/a11yDedupe.js.map +1 -1
- package/dist/mappers/defectFormatter.d.ts +9 -1
- package/dist/mappers/defectFormatter.js +53 -13
- package/dist/mappers/defectFormatter.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/priorityMapper.d.ts +42 -0
- package/dist/mappers/priorityMapper.js +58 -0
- package/dist/mappers/priorityMapper.js.map +1 -1
- 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/runComparator.d.ts +85 -0
- package/dist/mappers/runComparator.js +165 -0
- package/dist/mappers/runComparator.js.map +1 -0
- package/dist/mappers/wcagLevels.d.ts +73 -0
- package/dist/mappers/wcagLevels.js +320 -0
- package/dist/mappers/wcagLevels.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/accessibility.d.ts +1 -0
- package/dist/tools/accessibility.js +488 -63
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/lighthouse.js +370 -102
- package/dist/tools/lighthouse.js.map +1 -1
- 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/batchState.d.ts +75 -0
- package/dist/utils/batchState.js +128 -0
- package/dist/utils/batchState.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/outputParsers.js +26 -30
- package/dist/utils/outputParsers.js.map +1 -1
- 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 +135 -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/dist/utils/urlInput.d.ts +30 -0
- package/dist/utils/urlInput.js +130 -0
- package/dist/utils/urlInput.js.map +1 -0
- package/docs/manual.md +769 -0
- package/docs/psi-report-spec.md +174 -0
- package/package.json +13 -3
package/docs/manual.md
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
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
|
+
- [Auditing several URLs at once](#auditing-several-urls-at-once)
|
|
14
|
+
- [Comparing two runs (before vs after)](#comparing-two-runs-before-vs-after)
|
|
15
|
+
5. [PSI performance audit](#psi-performance-audit)
|
|
16
|
+
6. [Priority system](#priority-system)
|
|
17
|
+
7. [Project layout](#project-layout)
|
|
18
|
+
8. [Troubleshooting](#troubleshooting)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
Four of the tools wrap CLIs. Install the ones you need:
|
|
25
|
+
|
|
26
|
+
| Tool | Install | Used by |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Lighthouse | `npm install -g lighthouse` | `run_lighthouse`, `run_qa_gate` (URL) |
|
|
29
|
+
| pa11y | `npm install -g pa11y` | `run_accessibility_check`, `run_qa_gate` (URL) |
|
|
30
|
+
| ESLint | `npm install -g eslint` | `run_static_analysis`, `run_qa_gate` (path) |
|
|
31
|
+
| Semgrep | `brew install semgrep` or `pip install semgrep` | `run_static_analysis`, `run_qa_gate` (path) |
|
|
32
|
+
|
|
33
|
+
Verify:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
lighthouse --version && pa11y --version && eslint --version && semgrep --version
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**You don't need all four.** A missing tool shows `UNAVAILABLE` in the
|
|
40
|
+
scorecard and its findings are skipped; the gate still runs. URL-only runs need
|
|
41
|
+
Lighthouse and pa11y; path-only runs need ESLint and Semgrep.
|
|
42
|
+
|
|
43
|
+
### PageSpeed Insights API key
|
|
44
|
+
|
|
45
|
+
The PSI tools call an HTTP API rather than a CLI, but they want a key:
|
|
46
|
+
|
|
47
|
+
1. Enable the **PageSpeed Insights API** in the Google Cloud console.
|
|
48
|
+
2. Create an API key, restricted to that API.
|
|
49
|
+
3. Put it in the `env` block of your MCP client config — the client launches
|
|
50
|
+
this server, so it owns the environment:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp remove nfunc-mcp -s local
|
|
54
|
+
claude mcp add nfunc-mcp -s local -e PAGESPEED_API_KEY=your_key -- npx -y nfunc-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Confirm with any `plan_performance_audit` call: the response carries
|
|
58
|
+
`api_key: { present: true, source: "env" }` and never the key itself.
|
|
59
|
+
|
|
60
|
+
**Without a key the tools still load but are capped at 4 runs.** The shared
|
|
61
|
+
anonymous quota is exhausted in practice — expect `429 Quota exceeded` on the
|
|
62
|
+
first real request. A key gives 25,000 requests/day.
|
|
63
|
+
|
|
64
|
+
An `api_key` tool input also works but is discouraged: it lands in the
|
|
65
|
+
conversation transcript and in client logs. It is redacted from every error
|
|
66
|
+
message either way.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Install and register
|
|
71
|
+
|
|
72
|
+
### Option A — npm (recommended)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
claude mcp add nfunc-mcp -- npx -y nfunc-mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`npx` fetches and runs the server. No clone, no build.
|
|
79
|
+
|
|
80
|
+
### Option B — manual config
|
|
81
|
+
|
|
82
|
+
In `~/.claude.json` under `mcpServers`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"nfunc-mcp": {
|
|
88
|
+
"command": "npx",
|
|
89
|
+
"args": ["-y", "nfunc-mcp"],
|
|
90
|
+
"env": { "PAGESPEED_API_KEY": "your_key" }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Option C — from source (contributors)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone https://github.com/Hiddensound/NFunc_MCP.git
|
|
100
|
+
cd NFunc_MCP
|
|
101
|
+
npm install
|
|
102
|
+
npm run build
|
|
103
|
+
claude mcp add nfunc-mcp -- node /absolute/path/to/NFunc_MCP/dist/index.js
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Script | Purpose |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `npm run build` | Compile TypeScript → `dist/` |
|
|
109
|
+
| `npm start` | Run the compiled server |
|
|
110
|
+
| `npm run dev` | Run from source with hot reload (`tsx watch`) |
|
|
111
|
+
|
|
112
|
+
For local development, registering `tsx src/index.ts` instead of
|
|
113
|
+
`dist/index.js` means edits need only an MCP reconnect, not a rebuild.
|
|
114
|
+
|
|
115
|
+
### Verify
|
|
116
|
+
|
|
117
|
+
1. `/mcp` in Claude Code — `nfunc-mcp` shows as connected.
|
|
118
|
+
2. Ask: *"Call the nfunc-mcp ping tool."*
|
|
119
|
+
3. Expect `{ "status": "ok", "timestamp": "..." }`
|
|
120
|
+
|
|
121
|
+
After changing the server's code or environment, reconnect via `/mcp` — the
|
|
122
|
+
running process does not pick up changes on its own.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## `run_qa_gate` reference
|
|
127
|
+
|
|
128
|
+
### Inputs
|
|
129
|
+
|
|
130
|
+
Both `url` and `path` are optional; at least one is required.
|
|
131
|
+
|
|
132
|
+
| Input | Type | When to provide |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `url` | string (URL) | A running page — production, staging, preview, or localhost. Enables Lighthouse and pa11y. |
|
|
135
|
+
| `path` | string (path) | A local codebase. Enables ESLint and Semgrep. |
|
|
136
|
+
| `context` | string | Optional free-text description (e.g. `"React e-commerce checkout"`). Helps interpretation. |
|
|
137
|
+
| `form_factor` | `mobile` \| `desktop` \| `both` | Optional, default `mobile`. See [mobile vs desktop](#mobile-vs-desktop). |
|
|
138
|
+
| `a11y_runner` | `htmlcs` \| `axe` \| `both` | Optional, default `htmlcs`. See [choosing an engine](#choosing-an-accessibility-engine). |
|
|
139
|
+
|
|
140
|
+
### Release readiness tiers
|
|
141
|
+
|
|
142
|
+
| Value | Meaning | Condition |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `BLOCKED` | Cannot ship | Any P1 finding |
|
|
145
|
+
| `CONDITIONAL` | Shippable with caveats | P2 findings, no P1s |
|
|
146
|
+
| `ADVISORY` | Safe to ship; P3s are tech debt | Only P3 findings |
|
|
147
|
+
| `CLEAR` | No issues detected | Zero findings |
|
|
148
|
+
|
|
149
|
+
### Composite score
|
|
150
|
+
|
|
151
|
+
A single 0–100 health measure. Start at 100 and deduct: **P1 −15, P2 −7,
|
|
152
|
+
P3 −3**, floored at 0. Most useful as a trend line across sprints rather than
|
|
153
|
+
as an absolute grade.
|
|
154
|
+
|
|
155
|
+
### Scorecard
|
|
156
|
+
|
|
157
|
+
One line per tool:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
[
|
|
161
|
+
{ "tool": "Lighthouse", "gate": "WARN", "score": 75,
|
|
162
|
+
"breakdown": { "performance": 52, "accessibility": 98, "seo": 100, "best-practices": 58 } },
|
|
163
|
+
{ "tool": "pa11y", "gate": "PASS", "issues": 0 },
|
|
164
|
+
{ "tool": "ESLint / Semgrep", "gate": "SKIPPED" }
|
|
165
|
+
]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Gate | Meaning |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `PASS` | No issues at this tool's threshold |
|
|
171
|
+
| `WARN` | Issues below the FAIL threshold |
|
|
172
|
+
| `FAIL` | P1-level issues (or Lighthouse average < 50) |
|
|
173
|
+
| `SKIPPED` | Input not provided |
|
|
174
|
+
| `UNAVAILABLE` | Tool invoked but not installed, or errored |
|
|
175
|
+
|
|
176
|
+
### Cross-tool corroboration
|
|
177
|
+
|
|
178
|
+
When Lighthouse and pa11y independently flag the same accessibility gap, the
|
|
179
|
+
findings are **merged** into `corroborated_findings`, **promoted one tier**
|
|
180
|
+
(P3→P2, P2→P1), and annotated `confidence: "high"` with
|
|
181
|
+
`confirmed_by: ["lighthouse", "pa11y"]`.
|
|
182
|
+
|
|
183
|
+
Two independent tools agreeing is stronger evidence than either alone, so these
|
|
184
|
+
appear above all other findings and bubble to the top of `top_issues`.
|
|
185
|
+
|
|
186
|
+
**Rule 1 — accessibility mapping:**
|
|
187
|
+
|
|
188
|
+
| Lighthouse audit | pa11y technique |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `color-contrast` | `.G18`, `.G145`, `.G174` |
|
|
191
|
+
| `image-alt` | `.H37`, `.H67`, `.F65` |
|
|
192
|
+
| `label` | `.H44`, `.F68`, `.H91.Input` |
|
|
193
|
+
| `link-name` | `.H30`, `.H91.A.` |
|
|
194
|
+
| `html-has-lang` | `.H57` |
|
|
195
|
+
| `button-name` | `.H91.Button` |
|
|
196
|
+
| *(and more)* | |
|
|
197
|
+
|
|
198
|
+
**Rule 2 — performance ↔ code:** when a Lighthouse performance finding's
|
|
199
|
+
display value contains a filename that also appears in a static analysis
|
|
200
|
+
finding, the static finding is attached as `related_findings`. They are linked
|
|
201
|
+
by reference, not merged.
|
|
202
|
+
|
|
203
|
+
### HTML report
|
|
204
|
+
|
|
205
|
+
Every call writes a self-contained HTML file to `/tmp` and returns its path as
|
|
206
|
+
`report_file`. It contains the readiness banner, a composite-score gauge,
|
|
207
|
+
the scorecard, cross-confirmed findings, and collapsible finding cards grouped
|
|
208
|
+
by priority. Inline CSS, no server needed.
|
|
209
|
+
|
|
210
|
+
### Output shape
|
|
211
|
+
|
|
212
|
+
```jsonc
|
|
213
|
+
{
|
|
214
|
+
"release_readiness": "BLOCKED", // BLOCKED | CONDITIONAL | ADVISORY | CLEAR
|
|
215
|
+
"composite_score": 22, // 0–100
|
|
216
|
+
"report_file": "file:///tmp/qa-report-xxx.html",
|
|
217
|
+
"scorecard": [ ... ],
|
|
218
|
+
"eslint_config_used": "project", // only when path was supplied
|
|
219
|
+
"summary": "110 findings (101 P1, 9 P2) across 2 tools. ...",
|
|
220
|
+
"corroborated_findings": [ ... ], // cross-confirmed, confidence: "high"
|
|
221
|
+
"top_issues": [ ... ], // top 3 (corroborated first)
|
|
222
|
+
"all_findings": [ ... ], // all, sorted by priority
|
|
223
|
+
"correlations_found": 1,
|
|
224
|
+
"errors": [ ... ] // only if a tool errored
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Each finding:
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{
|
|
232
|
+
"priority": "P1",
|
|
233
|
+
"title": "Largest Contentful Paint",
|
|
234
|
+
"description": "Users see main content 34s after navigation...",
|
|
235
|
+
"evidence": { "audit_id": "largest-contentful-paint", "value": "34.3 s" },
|
|
236
|
+
"source_tool": "lighthouse",
|
|
237
|
+
// corroborated findings also carry:
|
|
238
|
+
"confirmed_by": ["lighthouse", "pa11y"],
|
|
239
|
+
"confidence": "high"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Individual tool reference
|
|
246
|
+
|
|
247
|
+
### `run_lighthouse`
|
|
248
|
+
|
|
249
|
+
Returns `url`, `form_factor`, `scores` per category, `ttfb_ms`, and
|
|
250
|
+
priority-ordered `findings`.
|
|
251
|
+
|
|
252
|
+
#### Mobile vs desktop
|
|
253
|
+
|
|
254
|
+
`form_factor` accepts `desktop` (default), `mobile`, or `both`.
|
|
255
|
+
|
|
256
|
+
**This default deliberately differs from the Lighthouse CLI's**, which is
|
|
257
|
+
mobile: a 412×823 screen, mid-range Android user agent, simulated slow 4G, and
|
|
258
|
+
a **4× CPU slowdown**. That profile reports substantially lower performance
|
|
259
|
+
scores for the same page, so `mobile` here is not a like-for-like comparison
|
|
260
|
+
with a default CLI run — check `form_factor` in the response before comparing
|
|
261
|
+
two reports.
|
|
262
|
+
|
|
263
|
+
**The two are not interchangeable.** They render different DOM, so they find
|
|
264
|
+
different defects, not just different numbers. On one commerce category page:
|
|
265
|
+
|
|
266
|
+
| | Mobile | Desktop |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| performance | 54 | 62 |
|
|
269
|
+
| accessibility | **87** | **73** |
|
|
270
|
+
| seo | 77 | 69 |
|
|
271
|
+
|
|
272
|
+
Five accessibility audits failed on desktop that mobile never reported —
|
|
273
|
+
`image-alt`, `aria-required-children`, `aria-required-parent`,
|
|
274
|
+
`aria-allowed-attr`, `aria-valid-attr-value` — while three others failed only
|
|
275
|
+
on mobile. Neither profile is a superset of the other.
|
|
276
|
+
|
|
277
|
+
With `both`, the two run concurrently (little more wall time than one), `scores`
|
|
278
|
+
is keyed by form factor, and each finding carries `affects_form_factors` and
|
|
279
|
+
`form_factor_specific`.
|
|
280
|
+
|
|
281
|
+
### `run_accessibility_check`
|
|
282
|
+
|
|
283
|
+
pa11y at WCAG 2 AA by default, violations only. Returns `url`, `standard`,
|
|
284
|
+
`runners`, `violation_count`, `raw_violation_count`, `findings`.
|
|
285
|
+
|
|
286
|
+
`raw_violation_count` versus `violation_count` shows the dedup at work: a rule
|
|
287
|
+
failing on more than two elements collapses into one systemic finding carrying
|
|
288
|
+
`distinct_elements` and the full `sample_selectors` list, rather than one line
|
|
289
|
+
per element. Three gallery images with no alt text are one template to fix, not
|
|
290
|
+
three authoring mistakes.
|
|
291
|
+
|
|
292
|
+
This matters across pages as much as within one. Before the threshold was
|
|
293
|
+
lowered, a homepage with 11 duplicate ids collapsed to a single finding while a
|
|
294
|
+
category page with 10 listed every one — the same component, but one page
|
|
295
|
+
appeared four times worse. Collapsing consistently is what makes per-page counts
|
|
296
|
+
comparable at all.
|
|
297
|
+
|
|
298
|
+
#### WCAG conformance and priority
|
|
299
|
+
|
|
300
|
+
Findings are priced by what they cost a **conformance claim**, not by how bad
|
|
301
|
+
the defect feels. `target_level` names the level the project has committed to —
|
|
302
|
+
default `AA`, the legal and industry bar for essentially all commercial work.
|
|
303
|
+
|
|
304
|
+
| Finding | Priority | Why |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| Level **A** criterion fails | **P1** | The floor. While any Level A criterion fails, no higher level is achievable — AA conformance is impossible regardless of how the AA-specific criteria score |
|
|
307
|
+
| Level **AA** criterion fails | **P2** | Blocks an AA commitment |
|
|
308
|
+
| Criterion **above** the target | **P3** | An enhancement, not a gap. `target-size` is 2.5.5, Level **AAA** in WCAG 2.1 — it should not fail an AA audit |
|
|
309
|
+
| Not a success criterion | **P3** | A best-practice rule. Worth fixing; does not affect a conformance claim |
|
|
310
|
+
|
|
311
|
+
Two demotions apply after that: axe's `needsFurtherReview` (a maybe should not
|
|
312
|
+
gate a release as hard as a certainty) and an axe impact of `minor` — the "very
|
|
313
|
+
minor AA issue" tier.
|
|
314
|
+
|
|
315
|
+
Every finding carries `wcag_criterion`, `wcag_name`, `wcag_level` and
|
|
316
|
+
`blocks_target` in its evidence, and its description states what the failure
|
|
317
|
+
means for the claim.
|
|
318
|
+
|
|
319
|
+
Each run also returns a `conformance` block, and a batch adds a cross-page
|
|
320
|
+
rollup:
|
|
321
|
+
|
|
322
|
+
```jsonc
|
|
323
|
+
"conformance": {
|
|
324
|
+
"target_level": "AA",
|
|
325
|
+
"conformant": false,
|
|
326
|
+
"failing_criteria": { "A": 6, "AA": 1, "AAA": 0 },
|
|
327
|
+
"beyond_target": 0,
|
|
328
|
+
"failed_criteria": [
|
|
329
|
+
{ "criterion": "4.1.1", "name": "Parsing", "level": "A", "findings": 5, "blocks_target": true }
|
|
330
|
+
],
|
|
331
|
+
"summary": "Not Level AA conformant. 6 Level A criteria fail... Level A is the floor..."
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**The unit is the criterion, not the finding.** Twelve findings against one
|
|
336
|
+
criterion is one thing to fix and one line in a conformance statement. The
|
|
337
|
+
finding count answers "how much work"; the criterion count answers "are we
|
|
338
|
+
conformant". A report giving only the first is how a page ends up described as
|
|
339
|
+
having 22 accessibility issues when it fails five criteria.
|
|
340
|
+
|
|
341
|
+
Criterion levels are transcribed from
|
|
342
|
+
[WCAG 2.1](https://www.w3.org/TR/WCAG21/). htmlcs encodes the criterion in its
|
|
343
|
+
rule code; axe does not expose WCAG tags through pa11y, so its rule ids go
|
|
344
|
+
through a lookup table in `src/mappers/wcagLevels.ts`. Rules axe classifies as
|
|
345
|
+
best-practice map to no criterion deliberately — reporting one as a conformance
|
|
346
|
+
failure would overstate the legal position.
|
|
347
|
+
|
|
348
|
+
**Automated testing reaches roughly a third of WCAG criteria.** A `conformant:
|
|
349
|
+
true` result means nothing automated failed, not that the page conforms. Focus
|
|
350
|
+
order, keyboard traps, meaningful sequence, error suggestion and content on
|
|
351
|
+
hover all need a human.
|
|
352
|
+
|
|
353
|
+
#### Choosing an accessibility engine
|
|
354
|
+
|
|
355
|
+
| Engine | Strongest at | Severity source |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `htmlcs` (default) | WCAG techniques, document structure, form labelling, duplicate ids | WCAG technique class |
|
|
358
|
+
| `axe` | **ARIA** — invalid roles, missing required parent/child relationships, prohibited and unsupported attributes — and computed colour contrast | axe's own `impact` |
|
|
359
|
+
|
|
360
|
+
**Reach for `axe` whenever the work involves ARIA, a component library, or a
|
|
361
|
+
design system.** The overlap is smaller than expected. On the same page:
|
|
362
|
+
|
|
363
|
+
- htmlcs found unlabelled inputs, forms with no submit mechanism, and ten
|
|
364
|
+
duplicate ids that axe did not report.
|
|
365
|
+
- axe found `aria-allowed-attr`, `aria-prohibited-attr`,
|
|
366
|
+
`aria-required-parent`, `aria-required-children` and `image-alt` failures
|
|
367
|
+
htmlcs missed entirely.
|
|
368
|
+
|
|
369
|
+
`both` runs them concurrently and merges. An element flagged by both appears
|
|
370
|
+
twice, because they emit different rule codes — deliberate, since two engines
|
|
371
|
+
agreeing is corroboration worth seeing.
|
|
372
|
+
|
|
373
|
+
axe findings carry `axe_impact`, and `needs_manual_review: true` where axe
|
|
374
|
+
wants human confirmation. Those are demoted one tier: a maybe should not gate a
|
|
375
|
+
release as hard as a certainty.
|
|
376
|
+
|
|
377
|
+
### `run_static_analysis`
|
|
378
|
+
|
|
379
|
+
ESLint and Semgrep in parallel against a local directory. Uses the project's own
|
|
380
|
+
ESLint config when it finds one, otherwise a QA-focused baseline. Returns
|
|
381
|
+
`path`, `tools_run`, `eslint_config_used`, `issue_count`, `findings`,
|
|
382
|
+
`warnings`.
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
### Auditing several URLs at once
|
|
386
|
+
|
|
387
|
+
`run_lighthouse` and `run_accessibility_check` both accept three input shapes in
|
|
388
|
+
the same `url` field, and detect which they were given:
|
|
389
|
+
|
|
390
|
+
| You pass | Detected as |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `https://site.com/page` | a single URL — one report, returned immediately |
|
|
393
|
+
| `https://a.com, https://b.com` (or newline-separated) | a list — batch mode |
|
|
394
|
+
| `./top-pages.csv` | a CSV — the URL column is found by name or by content |
|
|
395
|
+
|
|
396
|
+
A bare domain gets `https://` assumed, duplicates are dropped, and unparseable
|
|
397
|
+
entries are reported rather than silently skipped. An explicit `urls` array
|
|
398
|
+
works too.
|
|
399
|
+
|
|
400
|
+
**One URL behaves exactly as before** — same response shape, no batch fields.
|
|
401
|
+
Several URLs switch to batch mode:
|
|
402
|
+
|
|
403
|
+
- Each call is bounded by `max_seconds_per_call` (default **100 s**, chosen to
|
|
404
|
+
stay under the 120 s at which Claude Code backgrounds a tool call) and returns
|
|
405
|
+
a `cursor`. Keep calling until `complete` is true.
|
|
406
|
+
- Every report is written to `output_dir` as it lands — the **raw** Lighthouse
|
|
407
|
+
LHR, so the individual audits survive — and merged into a running
|
|
408
|
+
`_index.json`.
|
|
409
|
+
- **Re-run to fill gaps.** Call again with the same input and *no cursor*;
|
|
410
|
+
completed URL/variant pairs are skipped automatically. `skip_completed: false`
|
|
411
|
+
forces fresh measurements.
|
|
412
|
+
- The final call adds an `aggregate` block. Quote its numbers rather than
|
|
413
|
+
recomputing them.
|
|
414
|
+
|
|
415
|
+
Lighthouse runs **sequentially** in batch mode, unlike the single-URL path.
|
|
416
|
+
Two Chrome instances on one machine contend for CPU, and a performance audit
|
|
417
|
+
whose numbers came from a half-busy machine is not worth having. pa11y still
|
|
418
|
+
runs its engines concurrently — it is not measuring time.
|
|
419
|
+
|
|
420
|
+
#### What the aggregates tell you
|
|
421
|
+
|
|
422
|
+
`run_lighthouse` returns per-strategy means, a **per-template rollup** (the same
|
|
423
|
+
classifier the PSI plan tool uses, so twelve product URLs report as "PDP average
|
|
424
|
+
61" rather than as twelve rows), CWV verdict tallies, and outlier detection
|
|
425
|
+
against the median.
|
|
426
|
+
|
|
427
|
+
`run_accessibility_check` returns something a single-page run cannot: **which
|
|
428
|
+
rules fail across most pages**. A rule failing on 80%+ of pages is marked
|
|
429
|
+
`shared_layout: true` — it lives in the header, footer or base template, so one
|
|
430
|
+
fix clears every page. A rule failing on one page is that page's own bug. On a
|
|
431
|
+
four-page sample, `color-contrast` hit 4/4 while `image-alt` and `link-name` hit
|
|
432
|
+
1/4: two completely different pieces of work, and volume alone cannot separate
|
|
433
|
+
them.
|
|
434
|
+
|
|
435
|
+
#### Mobile accessibility
|
|
436
|
+
|
|
437
|
+
`form_factor` on `run_accessibility_check` defaults to `desktop` and accepts
|
|
438
|
+
`mobile` or `both`. Mobile emulates 412×823 at 2× DPR with touch, matching
|
|
439
|
+
`run_lighthouse`'s mobile profile so the two describe the same rendered page.
|
|
440
|
+
|
|
441
|
+
The viewport genuinely applies — a screenshot from the mobile run measures
|
|
442
|
+
824×23418 against 1280×2418 for the default. **But temper expectations:** on a
|
|
443
|
+
test site, neither htmlcs nor axe reported a single different violation between
|
|
444
|
+
the two viewports, because the rules both engines run here are structural —
|
|
445
|
+
missing labels, duplicate ids, absent alt text — and structure does not change
|
|
446
|
+
with width. It earns its keep on sites whose mobile DOM genuinely differs (a
|
|
447
|
+
hamburger nav, different components rendered), which is common on real commerce
|
|
448
|
+
sites.
|
|
449
|
+
|
|
450
|
+
For viewport-*dependent* accessibility defects today, `run_lighthouse` with
|
|
451
|
+
`form_factor: "both"` is the stronger tool: it reported `target-size` tagged
|
|
452
|
+
`form_factor_specific: true`, a touch-target failure that exists only on mobile
|
|
453
|
+
and that pa11y did not surface at all.
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
### Comparing two runs (before vs after)
|
|
457
|
+
|
|
458
|
+
A snapshot answers "what is wrong with this page". A developer about to open a
|
|
459
|
+
PR is asking something else: *did my fix work, and did I break anything?* A
|
|
460
|
+
violation count cannot separate those.
|
|
461
|
+
|
|
462
|
+
Both tools take `baseline_dir`. Point it at an earlier `output_dir`:
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
# 1. capture a baseline before touching anything
|
|
466
|
+
Run an accessibility check on http://localhost:3000, save to ./a11y-base
|
|
467
|
+
|
|
468
|
+
# 2. make the fix, then re-scan against it
|
|
469
|
+
Scan http://localhost:3000 again and compare to ./a11y-base
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The response gains a `comparison` block:
|
|
473
|
+
|
|
474
|
+
```jsonc
|
|
475
|
+
"comparison": {
|
|
476
|
+
"verdict": "mixed", // clean | improved | mixed | regression | unchanged
|
|
477
|
+
"summary": "5 defect(s) fixed, but 1 newly introduced (worst: P2). Total went 6 to 2; the drop is real but incomplete — check newly_introduced before treating this as a clean fix.",
|
|
478
|
+
"fixed": [{ "id": "image-alt", "was": "P1", ... }],
|
|
479
|
+
"still_failing": [{ "id": "color-contrast", "priority": "P3", ... }],
|
|
480
|
+
"newly_introduced": [{ "id": "aria-valid-attr-value", "priority": "P2", ... }],
|
|
481
|
+
"score_changes": [{ "category": "accessibility", "before": 94, "after": 64, "delta": -30 }]
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`newly_introduced` is the half that earns this feature. Measured on a real
|
|
486
|
+
page: adding `alt` text, an `aria-label` and a `<label>` fixed **five P1s** and
|
|
487
|
+
introduced one **P2**, because the `aria-labelledby` also added pointed at an id
|
|
488
|
+
that did not exist. A Lighthouse run against a page given a render-blocking
|
|
489
|
+
script reported `regression` with accessibility **94 → 64** and named all four
|
|
490
|
+
injected defects.
|
|
491
|
+
|
|
492
|
+
Notes on how it behaves:
|
|
493
|
+
|
|
494
|
+
- **This works with a single URL**, not only batches. A single-URL run touches
|
|
495
|
+
disk only when `output_dir` is given, so a one-off check stays a one-off while
|
|
496
|
+
the same call can seed a baseline.
|
|
497
|
+
- **`localhost` is fully supported** — pa11y and Lighthouse run Chrome on your
|
|
498
|
+
machine. This is the pre-PR check PSI cannot do.
|
|
499
|
+
- **`baseline_dir` may equal `output_dir`.** The baseline is read before
|
|
500
|
+
anything is written, so "compare against the last run in here" works.
|
|
501
|
+
- **`skip_completed` flips to `false` when comparing.** Re-measuring is the
|
|
502
|
+
whole point; skipping completed work would compare a run against itself.
|
|
503
|
+
- **Comparison is per `(url, variant, defect id)`.** Only runs present on both
|
|
504
|
+
sides are compared — a page that was not re-tested is reported under
|
|
505
|
+
`not_in_current` rather than counted as fixed, since a defect can only be
|
|
506
|
+
called fixed if the page was measured again.
|
|
507
|
+
- A defect that changed priority between runs counts as **still failing**, not
|
|
508
|
+
as fixed-and-reintroduced. It is the same defect on the same element.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## PSI performance audit
|
|
513
|
+
|
|
514
|
+
`plan_performance_audit` and `run_performance_audit` wrap the Google PageSpeed
|
|
515
|
+
Insights API. They are **exclusive and opt-in** — `run_qa_gate` never calls
|
|
516
|
+
them. Use them when someone asks for a PSI audit, a Core Web Vitals report, or
|
|
517
|
+
real-user field data.
|
|
518
|
+
|
|
519
|
+
### Why two tools
|
|
520
|
+
|
|
521
|
+
An MCP tool cannot ask a question mid-call, and a useful audit needs decisions
|
|
522
|
+
first — which URLs, how many per template, what to do about pages PSI cannot
|
|
523
|
+
reach. So:
|
|
524
|
+
|
|
525
|
+
1. `plan_performance_audit` discovers, classifies and costs the run, then
|
|
526
|
+
returns a `questions` array. **Spends no quota.**
|
|
527
|
+
2. Those questions go to the user.
|
|
528
|
+
3. `run_performance_audit` executes the approved page list.
|
|
529
|
+
|
|
530
|
+
A misclassified template should cost a conversation turn, not forty API calls.
|
|
531
|
+
|
|
532
|
+
### What PSI adds over `run_lighthouse`
|
|
533
|
+
|
|
534
|
+
One call returns two independent datasets: a Lighthouse run on Google's
|
|
535
|
+
infrastructure (**lab**) and Chrome UX Report data for the URL (**field** — real
|
|
536
|
+
users, 28-day 75th percentile). `run_lighthouse` gives you the first. Only PSI
|
|
537
|
+
gives the second, and the disagreement is the point:
|
|
538
|
+
|
|
539
|
+
| Lab | Field | Meaning |
|
|
540
|
+
|---|---|---|
|
|
541
|
+
| Pass | Pass | Genuinely fine |
|
|
542
|
+
| Fail | Pass | Lab profile harsher than the real audience — deprioritise |
|
|
543
|
+
| **Pass** | **Fail** | **The test environment is lying to you.** Real users hit something the simulation does not |
|
|
544
|
+
| Fail | Fail | Confirmed by two independent measurements |
|
|
545
|
+
|
|
546
|
+
Row three is invisible to every other tool here. On one commerce homepage the
|
|
547
|
+
lab reported a perfect CLS of 0 while real users were at 0.55 — 5.5× the "poor"
|
|
548
|
+
threshold, affecting 70% of them. On another site the lab reported TTFB of 2 ms
|
|
549
|
+
(Google's network sits next to the origin) against 1.5 s in the field.
|
|
550
|
+
|
|
551
|
+
CrUX is **not real-time** — it is a 28-day trailing aggregate. It is valuable
|
|
552
|
+
because it is real users, not because it is current.
|
|
553
|
+
|
|
554
|
+
### Where PSI does not work
|
|
555
|
+
|
|
556
|
+
- **Localhost and private hosts** — PSI fetches from Google's infrastructure.
|
|
557
|
+
Rejected at preflight; use `run_lighthouse`.
|
|
558
|
+
- **Cart, checkout, account pages** — PSI fetches anonymously, so it would
|
|
559
|
+
measure an empty cart or a login redirect. The plan tool flags these and
|
|
560
|
+
routes them to `run_lighthouse`, which can carry session cookies.
|
|
561
|
+
- **Low-traffic URLs** — reachable, but with little or no CrUX data. You get a
|
|
562
|
+
lab-only audit, labelled as such. Staging and preview deployments are always
|
|
563
|
+
in this category, which is why PSI is optional for non-production and
|
|
564
|
+
authoritative for hosted sites.
|
|
565
|
+
|
|
566
|
+
### URL discovery
|
|
567
|
+
|
|
568
|
+
`discovery` accepts:
|
|
569
|
+
|
|
570
|
+
| Mode | Behaviour |
|
|
571
|
+
|---|---|
|
|
572
|
+
| `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. |
|
|
573
|
+
| `list` | Explicit `urls` array. |
|
|
574
|
+
| `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. |
|
|
575
|
+
| `crawl` | Not implemented, and declined deliberately — see below. |
|
|
576
|
+
|
|
577
|
+
**Why there is no crawler.** Static link extraction was measured against real
|
|
578
|
+
homepages and found 11 internal paths on nodejs.org (whose sitemap has 1,723),
|
|
579
|
+
7 on gap.com, and nothing at all on a site behind a bot wall. The pages most
|
|
580
|
+
likely to lack a sitemap are the same ones that are client-rendered or
|
|
581
|
+
bot-protected, so a crawler fails precisely where it would be needed. An
|
|
582
|
+
analytics top-pages export is a better input anyway, being weighted by real
|
|
583
|
+
traffic. If one is ever built it should drive a headless browser rather than
|
|
584
|
+
parse static HTML.
|
|
585
|
+
|
|
586
|
+
Discovered URLs are clustered into **templates** by path shape, and the plan
|
|
587
|
+
tool proposes representative samples per template. Passing a URL with a path
|
|
588
|
+
(e.g. `https://site.com/shop/`) scopes discovery to that subtree.
|
|
589
|
+
|
|
590
|
+
### Chunking and time
|
|
591
|
+
|
|
592
|
+
PSI is slow and erratic: measured latency on live runs ranged from **10 s to
|
|
593
|
+
57 s for the same URL**, with occasional hangs and intermittent 500s. Roughly
|
|
594
|
+
one run in three failed on one origin.
|
|
595
|
+
|
|
596
|
+
So `run_performance_audit` runs in chunks. Each call is bounded by
|
|
597
|
+
`max_seconds_per_call` (default 150) and returns a `cursor`; keep calling until
|
|
598
|
+
`complete` is true. Raw reports are written to `output_dir` as they land and
|
|
599
|
+
merged into `_index.json`, so nothing completed is ever lost.
|
|
600
|
+
|
|
601
|
+
**Re-run to fill gaps.** Call again with the same pages and *no cursor* —
|
|
602
|
+
completed page/strategy pairs are skipped automatically, so only failures are
|
|
603
|
+
retried. Pass `skip_completed: false` to force fresh measurements.
|
|
604
|
+
|
|
605
|
+
`runs_per_url` defaults to 1. Raising it to 3 takes the median run and removes
|
|
606
|
+
single-run outlier risk, at three times the wall clock.
|
|
607
|
+
|
|
608
|
+
### Reading the output
|
|
609
|
+
|
|
610
|
+
The final call returns an `aggregate` block with every cross-page number:
|
|
611
|
+
per-strategy means, `lab_metric_failures`, CWV verdict tallies,
|
|
612
|
+
`lab_vs_field_summary`, and outliers. **Quote those rather than recomputing
|
|
613
|
+
them.**
|
|
614
|
+
|
|
615
|
+
Two redundancy rules apply there:
|
|
616
|
+
|
|
617
|
+
- **Systemic collapse** — a vital failing on 80%+ of runs *with little
|
|
618
|
+
variation between pages* collapses into one site-wide finding. Computed only
|
|
619
|
+
from URL-level field data, since origin-level CrUX is one number repeated and
|
|
620
|
+
cannot evidence a claim about variation.
|
|
621
|
+
- **Component suppression** — FCP is folded into LCP when both fail on a page,
|
|
622
|
+
since FCP is a component of LCP rather than an independent defect.
|
|
623
|
+
|
|
624
|
+
`field_source` on every field metric says whether the number describes the URL
|
|
625
|
+
or the whole origin. **PSI substitutes origin data silently**, so trust that
|
|
626
|
+
field rather than the presence of a populated field block.
|
|
627
|
+
|
|
628
|
+
[`psi-report-spec.md`](psi-report-spec.md) is the full guide to turning this
|
|
629
|
+
output into a written report.
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## Priority system
|
|
634
|
+
|
|
635
|
+
| Priority | Meaning | Lighthouse | WCAG | ESLint / Semgrep | CrUX field |
|
|
636
|
+
|---|---|---|---|---|---|
|
|
637
|
+
| P1 | Blocker — fix before shipping | Score < 50 | **Level A failure** — puts the target out of reach | Semgrep security, ESLint error | Core vital rated poor |
|
|
638
|
+
| P2 | Warning — track before merging | 50–79 | **Level AA failure** | ESLint warning | Needs improvement, or any diagnostic |
|
|
639
|
+
| P3 | Advisory — log as tech debt | 80–89 | **Above the target**, or a best-practice rule | — | — |
|
|
640
|
+
| *(suppressed)* | Passing — never reported | ≥ 90 | — | — | Good |
|
|
641
|
+
|
|
642
|
+
Lighthouse findings are actually ranked by `weight × (1 − score)` — the category
|
|
643
|
+
points an audit really costs — rather than by score alone, so a weight-30 metric
|
|
644
|
+
failing outright outranks a weight-1 SEO check that also scores 0.
|
|
645
|
+
|
|
646
|
+
Adjustments:
|
|
647
|
+
|
|
648
|
+
- **Corroborated** findings (two tools agreeing) are promoted one tier.
|
|
649
|
+
- **Field-confirmed** findings are promoted one tier; **lab-only** findings
|
|
650
|
+
contradicted by healthy field data are demoted and tagged `lab_only`.
|
|
651
|
+
- **Non-core vitals** (FCP, TTFB) never exceed P2 — they explain a Core Web
|
|
652
|
+
Vital rather than being one.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Project layout
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
├── src/
|
|
660
|
+
│ ├── index.ts # MCP server bootstrap + tool registration
|
|
661
|
+
│ ├── types.ts # Shared types (Finding, Priority)
|
|
662
|
+
│ ├── config/
|
|
663
|
+
│ │ └── qa-mcp-baseline.eslint.config.js # Fallback ESLint config
|
|
664
|
+
│ ├── tools/ # One file per MCP tool
|
|
665
|
+
│ │ ├── qaGate.ts # Orchestrator
|
|
666
|
+
│ │ ├── lighthouse.ts
|
|
667
|
+
│ │ ├── accessibility.ts
|
|
668
|
+
│ │ ├── staticAnalysis.ts
|
|
669
|
+
│ │ ├── performanceAuditPlan.ts # plan_performance_audit
|
|
670
|
+
│ │ └── performanceAudit.ts # run_performance_audit
|
|
671
|
+
│ ├── mappers/ # Raw output → QA report shape
|
|
672
|
+
│ │ ├── correlator.ts # Cross-tool correlation (Rule 1 + 2)
|
|
673
|
+
│ │ ├── defectFormatter.ts # Findings and defect prose
|
|
674
|
+
│ │ ├── priorityMapper.ts # Score/severity → P1/P2/P3
|
|
675
|
+
│ │ ├── a11yDedupe.ts # Systemic a11y collapse
|
|
676
|
+
│ │ ├── compositeScore.ts # Per-tool sub-scores
|
|
677
|
+
│ │ ├── webVitalsMapper.ts # CrUX thresholds → priorities → prose
|
|
678
|
+
│ │ ├── labFieldComparator.ts # Lab vs field verdicts
|
|
679
|
+
│ │ ├── psiAggregator.ts # Cross-run arithmetic + redundancy rules
|
|
680
|
+
│ │ ├── runComparator.ts # Before/after diff: fixed, still failing, new
|
|
681
|
+
│ │ └── wcagLevels.ts # WCAG 2.1 criteria, levels, conformance rollup
|
|
682
|
+
│ └── utils/ # Cross-tool helpers
|
|
683
|
+
│ ├── shellRunner.ts # Subprocess choke point
|
|
684
|
+
│ ├── httpClient.ts # HTTP choke point (retry, deadline, redaction)
|
|
685
|
+
│ ├── reportGenerator.ts # HTML report builder
|
|
686
|
+
│ ├── outputParsers.ts # Per-tool JSON parsers
|
|
687
|
+
│ ├── psiParser.ts # PSI response → lab + field
|
|
688
|
+
│ ├── psiAuth.ts # API key resolution
|
|
689
|
+
│ ├── sitemapReader.ts # Tiered sitemap discovery
|
|
690
|
+
│ ├── urlClassifier.ts # URL list → page templates
|
|
691
|
+
│ ├── csvReader.ts # URL extraction from CSV
|
|
692
|
+
│ ├── publicUrl.ts # Reachability + session-gate checks
|
|
693
|
+
│ ├── urlInput.ts # One URL / list / CSV → URL array
|
|
694
|
+
│ ├── batchState.ts # Cursor, budget, index merge, gap-fill
|
|
695
|
+
│ ├── eslintConfigDetector.ts
|
|
696
|
+
│ └── toolResponse.ts
|
|
697
|
+
├── docs/
|
|
698
|
+
│ ├── manual.md # This file
|
|
699
|
+
│ └── psi-report-spec.md # How to write the PSI audit report
|
|
700
|
+
└── dist/ # Compiled output (gitignored)
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
External calls go through exactly two choke points: `shellRunner` for
|
|
704
|
+
subprocesses, `httpClient` for HTTP. Anything holding an API key must use
|
|
705
|
+
`httpClient`, which redacts secrets from every error.
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## Troubleshooting
|
|
710
|
+
|
|
711
|
+
### The new tools don't appear after an update
|
|
712
|
+
|
|
713
|
+
Reconnect: `/mcp` → the server → **Reconnect**. The running process does not
|
|
714
|
+
reload code or environment changes on its own.
|
|
715
|
+
|
|
716
|
+
### `429 Quota exceeded` on the first PSI call
|
|
717
|
+
|
|
718
|
+
No API key. The shared anonymous quota is exhausted in practice, so keyless PSI
|
|
719
|
+
fails immediately rather than working slowly. See
|
|
720
|
+
[the key setup](#pagespeed-insights-api-key).
|
|
721
|
+
|
|
722
|
+
### A PSI call gets "moved to the background"
|
|
723
|
+
|
|
724
|
+
Claude Code backgrounds an MCP call at 120 s, and the default
|
|
725
|
+
`max_seconds_per_call` is 150. Results still arrive by notification. To keep
|
|
726
|
+
runs in the foreground, pass `max_seconds_per_call: 100` or lower.
|
|
727
|
+
|
|
728
|
+
### PSI runs fail intermittently
|
|
729
|
+
|
|
730
|
+
Expected — roughly one in three on some origins, as PSI 500s or hangs. Call the
|
|
731
|
+
tool again with the same pages and no cursor; completed pairs are skipped and
|
|
732
|
+
only the gaps are retried.
|
|
733
|
+
|
|
734
|
+
### The plan tool warns about HTTP 403
|
|
735
|
+
|
|
736
|
+
A bot wall (Cloudflare and similar) blocking the preflight. It says nothing
|
|
737
|
+
about PSI, which fetches from Google's address space and is usually
|
|
738
|
+
allowlisted. Not a reason to stop — but if the audit returns nothing, the
|
|
739
|
+
challenge page is the first suspect.
|
|
740
|
+
|
|
741
|
+
### "No sitemap found"
|
|
742
|
+
|
|
743
|
+
Nine locations were tried. Supply URLs with `discovery: "list"`, or point at a
|
|
744
|
+
CSV export — an analytics top-pages export is a better input for a performance
|
|
745
|
+
audit anyway, being weighted by real traffic.
|
|
746
|
+
|
|
747
|
+
### Field data says "site-wide data"
|
|
748
|
+
|
|
749
|
+
That URL has too little traffic for its own CrUX entry, so the numbers describe
|
|
750
|
+
the whole origin. They are still real, but they do not describe the page you
|
|
751
|
+
asked about. Two different URLs reporting identical p75 values are both being
|
|
752
|
+
served origin data.
|
|
753
|
+
|
|
754
|
+
### A tool shows `UNAVAILABLE`
|
|
755
|
+
|
|
756
|
+
Its CLI is not on PATH. Install it (see
|
|
757
|
+
[prerequisites](#prerequisites)) or ignore it — the rest of the gate still runs.
|
|
758
|
+
|
|
759
|
+
### A comparison reports nothing was compared
|
|
760
|
+
|
|
761
|
+
`baseline_dir` had no index, or it covers different URLs than this run. Check
|
|
762
|
+
`not_in_baseline` and `not_in_current` in the comparison block — only runs
|
|
763
|
+
present on both sides can be compared. Create a baseline by running once with
|
|
764
|
+
`output_dir` set.
|
|
765
|
+
|
|
766
|
+
### Lighthouse scores look far worse than expected
|
|
767
|
+
|
|
768
|
+
Check `form_factor`. The mobile profile applies a 4× CPU slowdown and simulated
|
|
769
|
+
slow 4G; it is not comparable to a desktop run of the same page.
|