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.
Files changed (42) hide show
  1. package/README.md +84 -376
  2. package/dist/index.js +4 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/mappers/labFieldComparator.d.ts +62 -0
  5. package/dist/mappers/labFieldComparator.js +134 -0
  6. package/dist/mappers/labFieldComparator.js.map +1 -0
  7. package/dist/mappers/psiAggregator.d.ts +130 -0
  8. package/dist/mappers/psiAggregator.js +293 -0
  9. package/dist/mappers/psiAggregator.js.map +1 -0
  10. package/dist/mappers/webVitalsMapper.d.ts +52 -0
  11. package/dist/mappers/webVitalsMapper.js +131 -0
  12. package/dist/mappers/webVitalsMapper.js.map +1 -0
  13. package/dist/tools/performanceAudit.d.ts +2 -0
  14. package/dist/tools/performanceAudit.js +446 -0
  15. package/dist/tools/performanceAudit.js.map +1 -0
  16. package/dist/tools/performanceAuditPlan.d.ts +2 -0
  17. package/dist/tools/performanceAuditPlan.js +438 -0
  18. package/dist/tools/performanceAuditPlan.js.map +1 -0
  19. package/dist/utils/csvReader.d.ts +20 -0
  20. package/dist/utils/csvReader.js +172 -0
  21. package/dist/utils/csvReader.js.map +1 -0
  22. package/dist/utils/httpClient.d.ts +84 -0
  23. package/dist/utils/httpClient.js +171 -0
  24. package/dist/utils/httpClient.js.map +1 -0
  25. package/dist/utils/psiAuth.d.ts +26 -0
  26. package/dist/utils/psiAuth.js +36 -0
  27. package/dist/utils/psiAuth.js.map +1 -0
  28. package/dist/utils/psiParser.d.ts +124 -0
  29. package/dist/utils/psiParser.js +200 -0
  30. package/dist/utils/psiParser.js.map +1 -0
  31. package/dist/utils/publicUrl.d.ts +17 -0
  32. package/dist/utils/publicUrl.js +115 -0
  33. package/dist/utils/publicUrl.js.map +1 -0
  34. package/dist/utils/sitemapReader.d.ts +27 -0
  35. package/dist/utils/sitemapReader.js +272 -0
  36. package/dist/utils/sitemapReader.js.map +1 -0
  37. package/dist/utils/urlClassifier.d.ts +45 -0
  38. package/dist/utils/urlClassifier.js +267 -0
  39. package/dist/utils/urlClassifier.js.map +1 -0
  40. package/docs/manual.md +558 -0
  41. package/docs/psi-report-spec.md +174 -0
  42. 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.