# PWA Debug Layer

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/aryanduntley/pwa-debug-layer  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/pwa-debug-layer

## Description
Debug PWAs in your real browser via MCP: service-worker, cache, installability & framework state.

## Claude Desktop Quick Installation
Heuristic fallback — verify the package name and runner against the repository README before running it. Uses `npx` (confidence: low):

```json
"mcpServers": {
  "pwa-debug-layer": {
    "command": "npx",
    "args": ["-y","pwa-debug-layer"]
  }
}
```

## Documentation & README

# pwa-debug-layer

An **AI-native debugging layer for PWAs and modern web apps**. It lets an AI agent (e.g. Claude Code via MCP) **see and act on your live, logged-in browser** the way a developer with full DevTools open would — DOM, console, network, framework state, store state, service workers, caches, and direct interaction — as structured data the model consumes natively.

It's built for the questions developers actually search for and that Chrome DevTools makes you assemble by hand: *why won't my service worker update? why is my cache stale? why won't my PWA install? why does this component have the wrong state?* — answered by an agent reading the runtime directly, against **your real browser profile** (extensions, auth, and all), not a sterile automated tab.

### What it answers

The PWA failures developers actually search for, read straight from your live runtime:

- **Why won't my service worker update? Why are some users on old code?** — SW lifecycle with waiting-vs-active versions, plus an update-propagation / version-skew analyzer.
- **Why is my cache stale? Why are chunks 404ing after a deploy?** — CacheStorage contents with age, and a cached-HTML-vs-JS skew check.
- **Why won't my PWA install?** — structured installability diagnostics with per-gap remediation, not just "manifest invalid."
- **What's actually in IndexedDB / localStorage right now?** — live storage inspection (*inspect IndexedDB live*).
- **What can this browser actually do?** — a live capability matrix (Push / Background Sync / Periodic Sync / Badging / File System Access / Window Controls Overlay).

All read from your real, logged-in profile — service-worker, cache, and extension state included — which `chrome-devtools-mcp`'s sterile automated Chrome can't see.

The goal is to eliminate the "user is the AI's eyes and hands" loop. Today, debugging a PWA with AI usually means the human copy/pastes DOM snippets, describes console errors, screenshots UI state, and hand-executes clicks. This project replaces that with direct, structured access.

> **Status: working on Linux.** The full MCP→IPC→native-host→service-worker→page-world round-trip is live, and a broad debugging surface is shipped:
> - **Capture** — console / network / error / DOM-mutation / lifecycle, with persistent ring buffers + disk spill.
> - **Framework introspection** — **React, Vue, Svelte, and Solid** (component/element trees, state, find-by-text/role).
> - **Store introspection** — **Redux, Zustand, Pinia, and Jotai** (read, subscribe, dispatch).
> - **Interaction + touch gestures** — click, fill, submit, hover, focus/blur, select, key/type, **drag, scroll, swipe, tap, double-tap, long-press, pinch**.
> - **Library-popup capture/replay** — WalletConnect / SDK modals: record, replay, tail, failure correlation.
> - **Replay & source maps** — rrweb `session_record`/`session_replay`, `source_map_resolve`.
> - **Browser launcher** — one-call `pdl_launch_browser` with `chrome-devtools-mcp` coexistence.
> - **PWA Runtime Diagnostics** — service-worker lifecycle + versions, CacheStorage contents + age, installability gaps, a live capability matrix, IndexedDB/web-storage inspection, update-propagation / version-skew analysis, and a one-shot runtime-state snapshot.
>
> **Verified on Linux** (the full suite live-tested against a real PWA). macOS/Windows code paths are implemented with unit coverage but still need real-machine retest ([help wanted](#help-wanted-macos--windows-verification)). Firefox is not supported (it doesn't speak CDP).
>
> ⚠️ **macOS / Windows users, beware:** these platforms have **never been run on real hardware** — only unit-tested with injected fakes. Expect rough edges (browser detection, profile paths, native-messaging registration, system-default resolution). Please [open an issue](../../issues) with the failing command and its output — bug reports from real macOS/Windows machines are the single most useful contribution right now. See [Help wanted](#help-wanted-macos--windows-verification) for the specific things to try.

## How it differs from `chrome-devtools-mcp`

Google's [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) gives an AI Chrome DevTools Protocol access (DOM, console, network, screenshots). That covers a lot.

`pwa-debug-layer` is **complementary** — it targets the things CDP can't reach:

- **Your real, logged-in browser.** `chrome-devtools-mcp` spawns a fresh automated Chrome — which shows "controlled by automated test software," blocks extension loading, and has none of your auth/session state, so authenticated apps are hard to debug ([the extensions/auth gap](https://medium.com/@vsanse24/no-extensions-allowed-the-chrome-devtools-mcp-dilemma-d58204aaab1f), [#265](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/265)). pwa-debug-layer's default `existing` mode attaches to **your normal profile**, with its real cookies, extensions, and service-worker state — exactly the real-profile SW/extension visibility people keep asking Google for ([#1173](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1173), [#96](https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/96)).
- **Framework state.** React fiber trees, Vue reactive state, Svelte component graphs, Solid signals — read via the framework's own devtools hooks (`__REACT_DEVTOOLS_GLOBAL_HOOK__`, `__vue_app__`, `_vnode`, etc.). CDP can't see these.
- **Store state.** Redux / Zustand / Pinia / Jotai — read, subscribe, and dispatch.
- **Service-worker, cache & installability state.** SW lifecycle + versions, CacheStorage contents + age, manifest installability gaps, IndexedDB/web storage, and an update-propagation / version-skew analyzer — the highest-volume PWA pain (stale cache, "SW won't update," install failures, "why are some users on old code") that DevTools makes you piece together by hand.
- **Shadow DOM, iframes, dynamically-injected library widgets.** WalletConnect modals, third-party SDK popups, and other widgets that escape standard DOM tooling.
- **Page-world reach in general.** A MAIN-world script we inject reaches things isolated-world content scripts can't, and reaches them earlier than `initScript`-on-next-nav.
- **Persistent ring buffers + rrweb-style replay** across navigations and reloads.
- **Configurable filters** so the AI receives only the slice it asked for — no full-DOM noise.

The two are designed to coexist: install both, the AI uses each for what it does best, with zero tool-surface duplication.

## Architecture

```
┌──────────────────┐  MCP (stdio)  ┌──────────────────────────┐
│  Claude Code     │ ◄───────────► │  Native Messaging Host   │
│  (or any MCP     │               │  - MCP server            │
│   client)        │               │  - Ring buffers          │
└──────────────────┘               │  - Replay/snapshot store │
                                   └────────────┬─────────────┘
                                                │ Native Messaging
                                                │ (JSON over stdio)
                                                ▼
                                   ┌──────────────────────────┐
                                   │  Extension Service Worker│
                                   │  - chrome.debugger (CDP) │
                                   │  - Tab/router            │
                                   └────────────┬─────────────┘
                                                │
                       ┌────────────────────────┼────────────────────────┐
                       ▼                        ▼                        ▼
            ┌────────────────────┐  ┌────────────────────┐  ┌────────────────────┐
            │ Content Script     │  │ Page-World Script  │  │ DevTools Panel     │
            │ (isolated world)   │  │ (MAIN world)       │  │ (planned)          │
            │ - DOM observe      │  │ - React/Vue hooks  │  │ - Human inspector  │
            │ - Action exec      │  │ - fetch/XHR patch  │  │   of AI session    │
            │ - Bridge to SW     │  │ - Bus/RxJS taps    │  │                    │
            └────────────────────┘  └────────────────────┘  └────────────────────┘
                       └────────── live page (the PWA being debugged) ─────────┘
```

Three components, one installable unit:

- **Extension** owns the page (DOM, content scripts, page-world hooks).
- **Native host** owns persistence and the MCP server (long-lived, can hold buffers, can write files).
- **MCP** owns the AI contract.

Each does what only it can. See [`docs/PLAN.md`](docs/PLAN.md) for the full design.

## Browser support

**Chromium-family only**, sideloaded. Tested against:

- Chromium (native package)
- Google Chrome (`.deb` / `.rpm`)
- Brave Browser
- Microsoft Edge (Linux `.deb`)
- Vivaldi
- Opera

macOS Application Support paths and Windows HKCU-registry registration are implemented and have unit-test coverage; the manual round-trip retest currently runs on Linux.

### Snap browsers are not supported

If you installed your browser via **snap** (e.g. `snap install chromium` on Ubuntu), it will not work with `pwa-debug-layer`.

**Why:** snap's `home` interface allows the browser to *read* files in `$HOME` but blocks *exec* of any binary whose resolved path crosses a hidden directory (`~/.nvm/...`, `~/.config/...`). The native messaging host launcher and the node binary it invokes both live under hidden paths in a normal install, so spawn fails with `Permission denied` and the service worker reports `Native host has exited.` There is no fix on the extension/host side that doesn't require copying ~125 MB of node into a non-hidden install dir per registration; not worth the install bloat for a setup most distros let you avoid.

**What to do:** install your Chromium-family browser from a native package source instead:

- **Ubuntu/Debian:** `apt install chromium` from the universe repo if you've enabled the non-snap source, or `apt install brave-browser` / `microsoft-edge-stable` from their respective `.deb` repos. The Chromium tarball from chromium.org also works.
- **Fedora:** `dnf install chromium` is non-snap by default.
- **Arch:** `pacman -S chromium`.

Flatpak browsers are detected and have a manifest written, but flatpak confinement may also block exec — if it fails, run `flatpak override --user --filesystem=host <app-id>` and retry.

## Installation

### Prerequisites

- Node.js ≥ 20.19 (developed on 23.x)
- pnpm
- A Chromium-family browser **not installed via snap** (see above)
- An MCP-aware client (e.g. [Claude Code](https://docs.claude.com/en/docs/claude-code))

### 1. Build the host and extension

```sh
git clone https://github.com/<your-fork>/pwa-debug-layer
cd pwa-debug-layer
pnpm install
pnpm build      # builds packages/host/dist/main.js and packages/extension/dist/
pnpm test       # full workspace unit suite (shared + host + extension)
```

### 2. Add the host to your MCP client

For Claude Code, add to your `.mcp.json` (project-scoped) or `~/.claude/mcp.json` (global):

```json
{
  "mcpServers": {
    "pwa-debug": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/pwa-debug-layer/packages/host/dist/main.js"]
    }
  }
}
```

Restart Claude Code so it picks up the server.

### 3. Load the extension

1. Open `chrome://extensions` (or `brave://extensions`, etc.) in your browser.
2. Toggle **Developer mode** on.
3. Click **Load unpacked** and select `packages/extension/dist/`.
4. Note the extension's ID (shown on the card). Or, ask Claude to discover it for you in the next step — the extension service worker logs `[pwa-debug/sw] id=<id>` on every boot.

### 4. Tell Claude to set it up

The host registration is **AI-driven**. Six MCP tools are exposed for setup; Claude calls them via a guided flow:

> **Set up pwa-debug. The extension ID is `<your-id>`** *(or omit the ID and Claude can fetch it from the SW console if you also have `chrome-devtools-mcp` installed.)*

Claude will:

1. Call `host_status` to see what's already registered.
2. Call `host_register_extension(id)` — this writes a per-browser native-messaging manifest into every detected install (Chromium-family native, macOS Application Support, Windows HKCU registry), and drops a launcher script with an absolute node path (so the host spawns under sandboxed PATH environments).
3. Tell you to reload the extension at `chrome://extensions`.
4. After reload, call `host_status` again to confirm the manifest is on disk and the SW is connecting.

When the round-trip works you'll see in the SW console (`Inspect views: service worker` on the extension card):

```
[pwa-debug/sw] connected to host
[pwa-debug/sw] pong …
[pwa-debug/sw] hello …          (5s after connect — host-pushed message proving bidirectional flow)
```

## Launching a browser + `chrome-devtools-mcp` coexistence

`pwa-debug` and `chrome-devtools-mcp` are **two separate MCP servers** that share one browser. `pwa-debug` launches (or attaches to) a Chromium browser with a live remote-debugging port; `chrome-devtools-mcp` attaches to that same port over CDP. No proxying, no version coupling.

Register both with your client. For Claude Code:

```sh
# pwa-debug (this project) — adjust the path to your checkout
claude mcp add pwa-debug --scope user -- node /absolute/path/to/pwa-debug-layer/packages/host/dist/main.js

# chrome-devtools-mcp (optional but recommended) — runs via npx, no global install
# The --browserUrl port MUST match the port pwa-debug launches on (launch.defaultPort, default 9222).
claude mcp add chrome-devtools --scope user -- npx -y chrome-devtools-mcp@latest --browserUrl http://127.0.0.1:9222
```

### Or install `pwa-debug` as a Claude Code plugin

The repo ships a `.claude-plugin/` manifest whose MCP server runs the **published npm package** (`npx -y @aryanduntley/pwa-debug@latest`), so the plugin installs with **no clone and no build** — and picks up updates with **`/reload-plugins`**, no full restart:

```sh
# Add the marketplace straight from GitHub (no checkout needed)
/plugin marketplace add aryanduntley/pwa-debug-layer
# Install + enable the pwa-debug plugin
/plugin install pwa-debug@pwa-debug
# Bring the MCP server up with no restart:
/reload-plugins
```

> **No build needed.** The plugin's MCP entry is `npx -y @aryanduntley/pwa-debug@latest`, which fetches the prebuilt host (and its bundled extension) from npm. *Working on a local clone instead?* Skip the plugin and register your own build directly: `claude mcp add pwa-debug --scope user -- node /absolute/path/to/pwa-debug-layer/packages/host/dist/main.js`.
>
> **`chrome-devtools-mcp` is still separate.** The plugin declares **only** the `pwa-debug` host — `chrome-devtools-mcp` stays the optional `claude mcp add chrome-devtools …` above (no version coupling, no owning its launch). The bundled **`chrome-devtools-coexistence`** skill walks you through registering it; with a plugin install, its "make the tools appear" step is `/reload-plugins`.

> **Port must match.** `chrome-devtools-mcp`'s `--browserUrl` has to point at the exact port `pwa-debug` opens — the `launch.defaultPort` setting (default `9222`), or the active port from a current `pdl_launch_browser`. Rather than hand-write this, let Claude call **`pdl_register_chrome_devtools`**, which runs the `claude mcp add` above for you pinned to the right port (and **`pdl_check_setup`** flags a registration that is *unpinned* — no `--browserUrl`, so it spawns its own isolated browser — or pointed at the wrong port).

> **Mind the (re)connect.** A registration added or changed via `claude mcp add` only takes effect when the server (re)connects. Fastest path: open **`/mcp`** and **reconnect** `chrome-devtools` — this reloads it from the current registration (verified to pick up a changed `--browserUrl`) with **no full restart and no context loss**. If the client won't load it that way, **fully restart** Claude Code as a fallback. If instead you install `chrome-devtools-mcp` as a **plugin**, run **`/reload-plugins`**. The bundled **`chrome-devtools-coexistence`** skill walks Claude through all paths and, for the full-restart fallback, hands you a context note to paste back afterward.

> The host has no `install`/`serve` subcommand — `dist/main.js` auto-detects its mode: launched by Chrome (argv starts with `chrome-extension://`) it runs as the native-messaging host; launched by your MCP client it runs as the MCP server.

Then let Claude drive setup and launch:

1. **`pdl_check_setup`** — reports `{ ok, gaps[], recommendations[] }`: whether `chrome-devtools-mcp` is **registered** (read from the `claude` CLI) **and pinned with a `--browserUrl`** at the right debug port — it flags both an *unpinned* registration (which would spawn its own isolated browser instead of attaching to yours) and one pointed at the wrong port; the host manifest is installed (including **per active sandbox profile**); the extension dist is present; and an extension ID is registered. Follow its `next_steps` to close any gap.
2. **`pdl_register_chrome_devtools`** — writes the `chrome-devtools-mcp` registration for you, pinned to the active/`launch.defaultPort` port (idempotent; re-points it if it's unpinned or on the wrong port). *(Mutates your MCP config — Claude will ask first; a restart/reconnect follows, see above.)*
3. **`pdl_install_extension`** — copies the extension to `~/Downloads/pwa-debug-extension` (or a `target` you pass) with `chrome://extensions` "Load unpacked" instructions. *(On most browsers a sandbox mode preloads the extension so you can skip this; on **branded Google Chrome 142+** preload is impossible — this is the manual install the sandbox launch will point you to.)*
4. **`pdl_launch_browser`** — launches/attaches a browser with the debug port live and returns the `browserUrl` to hand to `chrome-devtools-mcp`. **Always launch first:** call this *before* any `chrome-devtools-mcp` tool, so the browser + debug port exist for it to attach to. Follow its `next_steps` — they carry the brand/version-specific guidance (preloaded vs. manual *Load unpacked*, or the modern-Chromium port caveat).
5. **`pdl_browser_status`** — shows what's been launched (browser, profile mode, port, pid), re-probes each debug port for liveness, and reports the extension service-worker heartbeat.

**The validated one-profile recipe** (what these tools converge on, verified live on Brave 148):

```
pdl_launch_browser({ browser: "brave", mode: "sandbox-persistent" })
  → dedicated profile at ~/.pwa-debug/profiles/brave with:
      • the native-messaging manifest auto-written into the profile (so the extension connects), and
      • the pwa-debug extension preloaded via --load-extension (Brave honors it), and
      • --remote-debugging-port live (custom profile dir, so Chromium 136+ opens it).
pdl_register_chrome_devtools()  → pins chrome-devtools-mcp at that port
restart / reconnect chrome-devtools-mcp  → it attaches to the SAME browser
```

Now `chrome-devtools-mcp` (CDP) and pwa-debug (extension → page-world) drive the same tab simultaneously — independent, non-contending channels (the extension uses page-world injection, **not** `chrome.debugger`, so there's no CDP-attacher contention).

## Profile modes

`pdl_launch_browser` takes `mode` (default `existing`), `browser` (defaults to your system-default Chromium browser), and `port` (default `9222`).

| Mode | Profile | When to use |
|---|---|---|
| **`existing`** *(default)* | Your normal browser profile | Debugging your real browsing session **with pwa-debug** (your real cookies, extensions, SW/cache state). Degrades gracefully: **(a)** debug port already live → attaches; **(b)** browser running *without* a debug port → opens a new window in the existing session (your extension tools work, `chrome-devtools-mcp` does not); **(c)** browser not running → spawns fresh with your profile. **Modern-Chromium caveat (136+):** Chromium refuses `--remote-debugging-port` on the **default** profile, so on a current browser `existing` mode gives you pwa-debug but **not** a CDP port — the launch reports this (`attached:false`, no false `browserUrl`) and steers you to `sandbox-persistent` for `chrome-devtools-mcp`. |
| **`sandbox-persistent`** | `~/.pwa-debug/profiles/<browser>/` (persists across restarts) | The **canonical both-tools profile** — a dedicated profile beside your main browser, with its own `--user-data-dir` (so the debug port works on Chromium 136+). The launcher auto-writes the native-messaging manifest into the profile and, on most browsers, **preloads** the pwa-debug extension via `--load-extension`. **Branded-Google-Chrome 142+ caveat:** Chrome removed `--load-extension`, so there the launch brings up the **debug port** (for `chrome-devtools-mcp`) but **not** the extension — it returns a one-time manual *Load unpacked* walkthrough (persists in this profile), or steers you to a non-Google Chromium where preload just works. |
| **`sandbox-temp`** | a fresh `mktemp` dir (removed on host shutdown) | One-off / CI / clean-state runs. Same as `sandbox-persistent`, but throwaway — so the Chrome-142+ manual-load step won't persist usefully here; prefer a non-Google Chromium for temp runs that need the extension. |

**Extension preload depends on the browser brand + version** (the launcher detects it from `--version` and adapts):

| Browser | `--load-extension` (sandbox preload) |
|---|---|
| Brave, Chromium, Edge, Opera, Vivaldi, Chrome for Testing | ✅ works on current versions |
| Google Chrome ≤ 136 | ✅ works |
| Google Chrome 137–141 | ✅ works — launcher adds `--disable-features=DisableLoadExtensionCommandLineSwitch` |
| **Google Chrome ≥ 142** | ❌ removed — launcher omits the dead flag and guides a one-time manual *Load unpacked* (or use Brave/Chromium) |

**Recommended coexistence setup:** `sandbox-persistent` on a **non-Google Chromium** (e.g. Brave or Chromium). That single profile gets the extension preloaded **and** a live debug port, so pwa-debug and `chrome-devtools-mcp` drive the same browser with zero manual steps.

## Browser support matrix

Chromium-family only (Firefox doesn't speak CDP). Linux is first-class. macOS/Windows binary detection, profile/user-data-dir paths, **and system-default detection** (macOS LaunchServices, Windows `UserChoice` registry) are all implemented with unit coverage, but **live verification on real macOS/Windows machines is still needed — see [Help wanted](#help-wanted-macos--windows-verification).**

| Browser | PATH names probed | Standard Linux binary | Linux profile dir (`existing` mode) |
|---|---|---|---|
| Chrome | `google-chrome`, `google-chrome-stable` | `/opt/google/chrome/chrome`, `/usr/bin/google-chrome*` | `~/.config/google-chrome` |
| Chromium | `chromium`, `chromium-browser` | `/usr/bin/chromium*`, `/snap/bin/chromium` | `~/.config/chromium` |
| Edge | `microsoft-edge`, `microsoft-edge-stable` | `/opt/microsoft/msedge/msedge` | `~/.config/microsoft-edge` |
| Brave | `brave-browser`, `brave` | `/opt/brave.com/brave/brave-browser` | `~/.config/BraveSoftware/Brave-Browser` |
| Vivaldi | `vivaldi`, `vivaldi-stable` | `/opt/vivaldi/vivaldi` | `~/.config/vivaldi` |
| Opera | `opera` | `/usr/bin/opera`, `/opt/opera/opera` | `~/.config/opera` |

- **System default:** the launcher prefers your system-default browser when you don't pass one — Linux via `xdg-settings get default-web-browser`, macOS via LaunchServices (`defaults read … LSHandlers`), Windows via the HKCU `UrlAssociations\http\UserChoice` ProgId. The macOS/Windows paths are implemented + unit-tested but not yet exercised on a real machine.
- **Default debug port** is `9222` (the `chrome-devtools-mcp` convention); override it without passing `port` each time via the `launch.defaultPort` setting (`settings.set`).
- **Snap profiles:** when launching a snap-packaged browser (`/snap/bin/…`) in `existing` mode, the launcher now resolves its confined profile (`~/snap/<snap>/common/<cfg>`) instead of `~/.config` — but note snap browsers still can't run the native-messaging host (see snap section), so this only matters if/when that confinement is lifted.
- **Brave Shields** can block the content script on a site — set Shields **Down** for the site if `session_ping` reports `page_blocks_scripts`.
- **Snap browsers are unsupported** for the native-messaging host (see below); the launcher can still spawn them, but the host round-trip won't connect. Use a native-package browser.
- **Flatpak** installs get a manifest written, but confinement may block exec — run `flatpak override --user --filesystem=host <app-id>` and retry.

## Help wanted: macOS / Windows verification

Development happens on Linux, so the macOS and Windows code paths are **written and unit-tested with injected fakes, but never run on a real machine.** If you're on macOS or Windows, trying these and reporting back (open an issue with the output) is the single most useful contribution right now:

**macOS**
- `pdl_check_setup` / `pdl_install_extension` — does the extension resolve and copy, and do the `chrome://extensions` instructions work?
- Browser binary detection under `/Applications/*.app/Contents/MacOS/…`.
- System-default detection: `defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers` — does the parser pick the right browser? (Paste the raw output if it doesn't.)
- `pdl_launch_browser` mode `existing` (profile under `~/Library/Application Support/…`) and both sandbox modes.

**Windows**
- Browser detection under `%PROGRAMFILES%` / `%LOCALAPPDATA%`.
- System-default detection: `reg query "HKCU\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice" /v ProgId` — does the ProgId map to the right browser?
- `pdl_launch_browser` (profile under `%LOCALAPPDATA%\…\User Data`) and the HKCU native-messaging registration.

**Any OS**
- `pdl_browser_status` after restarting the host — launches now persist to `launches.json`, so previously-launched browsers should still be listed (with a fresh liveness re-probe). Confirm the list survives a restart and that closed browsers show as not-live.

The launcher never kills your running browser and sandbox modes use throwaway/dedicated profiles, so this is low-risk to try.

## Troubleshooting

### Verification sequence

When something isn't working, walk this ladder — each step localizes the failure:

1. **`pdl_check_setup`** — are all setup gaps closed? (CDP reachable, manifest installed, extension present, ID registered.)
2. **`pdl_browser_status`** — is a browser launched and is its debug port still live? Is the extension service worker connected (recent heartbeat)?
3. **`host_status`** — is the native-messaging host registered and is an NMH instance connected?
4. **`session_ping`** — does a full MCP → SW → page-world round-trip succeed on the active tab? (See the typed `pageWorldError` table below.)

### Common launcher gotchas

- **"I launched in `existing` mode but `chrome-devtools-mcp` can't attach."** Two causes. **(1)** Chrome opens `--remote-debugging-port` only at process start — if the browser was already running without it, `pdl_launch_browser` opens a new window (sub-state **b**) but can't add the port to the live process (`attached:false`). **(2)** On **Chromium 136+**, the port is refused on the *default* profile entirely, even on a fresh start. Either way: use `mode: sandbox-persistent` (a dedicated profile where the port works) for `chrome-devtools-mcp`.
- **Brave/Chrome says "Opening in existing browser session."** That's sub-state **b** — the binary handed your request to the already-running process instead of starting a fresh one with the port. Same fix as above.
- **Sandbox launch came up but pwa-debug never connects (Google Chrome 142+).** Branded Google Chrome 142+ permanently ignores `--load-extension`, so the sandbox can't auto-preload the extension — the launch says so and gives you a one-time `chrome://extensions` → Developer mode → *Load unpacked* walkthrough (it persists in that dedicated profile). Or relaunch with `browser: "brave"` / `"chromium"`, where preload works automatically. (`chrome-devtools-mcp` still works either way — the debug port is live.)
- **`chrome-devtools-mcp` opened its own blank browser instead of using yours.** Its registration is **unpinned** (no `--browserUrl`). Run `pdl_register_chrome_devtools` (or re-add with `--browserUrl http://127.0.0.1:<port>`) and restart/reconnect it; `pdl_check_setup` flags this.
- **Added the MCP server but the tools don't appear.** `.mcp.json` / client MCP config is read at startup — restart your MCP client (or `/mcp`-reconnect that server) after adding `pwa-debug` or `chrome-devtools`.
- **Tools worked, then stopped after I reloaded the extension.** Reloading the extension at `chrome://extensions` detaches content scripts from already-open tabs. Hard-refresh the page tab (Ctrl+Shift+R); the SW also auto-reinjects on the next `session_ping` (look for `pageWorldSelfHealed: true`). Sandbox modes avoid this entirely (extension preloaded before tabs open).


### `session_ping` returns `pageWorld: null` with a typed `pageWorldError`

`session_ping` reports failure modes as **typed codes** in `pageWorldError` (machine-readable) plus the original chrome-runtime string in `pageWorldErrorMessage` (for logs). The MCP `next_steps[]` field carries imperative, code-specific guidance — AI clients should relay it verbatim. Tabs that simply predated the extension reload (the most common dev-loop friction) are auto-recovered by the SW via `chrome.scripting.executeScript`; when that succeeds, `pageWorld` is populated and `pageWorldSelfHealed: true` appears alongside it. The table below is the canonical mapping (single source of truth: `NEXT_STEPS_BY_CODE` in `packages/host/src/mcp/tools/session_ping.ts`).

| `pageWorldError` | What it means | What to do |
|---|---|---|
| *(absent)* with `pageWorldSelfHealed: true` | The static content script was missing on the active tab; the SW silently re-injected `content-script.js` + `page-world.js` and retried. No action needed. | Informational only. |
| `cs_not_attached_refresh_tab` | Auto-recovery was attempted but did not stick (page rejected the injection or reloaded mid-flight). | Hard-refresh the page tab (Ctrl+Shift+R) and retry. If it repeats, reload the extension at `chrome://extensions` then hard-refresh. |
| `page_blocks_scripts` | A content blocker is rejecting the script (Brave Shields, uBlock Origin, AdGuard, or similar). Site CSP is also possible. | **Brave:** click the lion icon → set Shields **Down** for the site → refresh → retry. **uBlock Origin / similar:** disable for this site → refresh → retry. If neither, the site's own CSP is blocking and pwa-debug cannot bypass it. |
| `page_world_blocked` | The content script attached but the MAIN-world page-world bridge cannot be reached — the site's Content-Security-Policy blocks the inline script tag. | Site-level restriction; cannot bypass. Console + network capture may still work via the content-script side, but live page-world reads (state, evaluate) will not. |
| `restricted_url` | The active tab is on a URL browsers do not allow extensions to touch (`chrome://`, `chromewebstore.google.com`, `about:`, `devtools://`, `file://`, `view-source:`, etc.). | Switch focus to a regular `http(s)` tab of the PWA, then retry. |
| `no_active_tab` | No active `http(s)` tab is focused (DevTools window or extension popup may be focused instead). | Focus a regular browser tab and retry. |
| `cs_inject_failed` | The auto-recovery `chrome.scripting.executeScript` itself failed. The extension cannot reach this tab. | Reload the extension at `chrome://extensions` and hard-refresh the page (Ctrl+Shift+R). If it persists, the URL may be one the browser blocks all extensions from — check the address bar. |

To confirm the content script attached after a successful round-trip, open the page tab's DevTools (F12 on the page itself, **not** the SW console) and look for `[pwa-debug/cs] attached at <url>` in the Console.

## MCP tool surface

Every tool returns a structured response of the form `{ ok, data, error?, next_steps[] }`. The `next_steps` array encodes the rules of engagement for the AI — what to call next based on the actual response shape — mirroring the AIMFP `return_statements` pattern. The canonical list lives in `packages/host/src/mcp/tools/index.ts`.

### Host management & setup

| Tool | Purpose |
|---|---|
| `host_status` | Install/liveness state: registered IDs, manifest paths, launcher path, active connections. Cheap, idempotent. **Always call first.** |
| `host_register_extension(id)` / `host_unregister_extension(id)` | Add/remove an extension ID across per-browser manifests + launcher script. Idempotent. |
| `host_list_registrations` / `host_reset` | Read registered IDs; destructive cleanup to re-bootstrap. |
| `session_ping` | Full MCP → IPC → NMH → SW → page-world round-trip with typed `pageWorldError` codes + self-heal. |
| `pdl_check_setup` | Diagnose setup → `{ ok, gaps[], recommendations[] }` (CDP reachable, manifest installed, extension present, ID registered). |
| `pdl_install_extension({ target? })` | Copy the extension to a folder for unpacked install. |

### Browser launcher

| Tool | Purpose |
|---|---|
| `pdl_launch_browser({ browser?, port?, mode? })` | Launch/attach a Chromium browser with a live debug port. `mode`: `existing` (default), `sandbox-persistent`, `sandbox-temp`. Returns `browserUrl` for `chrome-devtools-mcp`. |
| `pdl_browser_status` | Managed launches (browser, profile mode, port, pid) with live debug-port re-probe + extension SW heartbeat. |

### Capture, evaluate, replay

| Tool | Purpose |
|---|---|
| `console_tail` / `network_tail` / `error_tail` | Cursor-paginated, filterable tails of the persistent capture ring buffers (memory + disk spill). |
| `recent_events` | Recent captured events across kinds for quick verification. |
| `evaluate` | Evaluate an expression in the page world. |
| `session_record` / `session_replay` | rrweb session recording + cursor-paginated replay. |
| `source_map_resolve` | Resolve generated stack frames to original `src/…:line:col`. |
| `settings_list_schema` / `settings_get` / `settings_set` | Read the typed settings schema; get/set values (allowlist, capture filters, disk-spill, etc.). |

### PWA runtime diagnostics

The namesake suite — read the debugged PWA's service-worker, cache, installability, storage, and update state. All page-world reads against your **real profile**; CDP / `chrome-devtools-mcp` can't surface these.

| Tool | Purpose |
|---|---|
| `sw_status` | Service-worker registrations: installing/waiting/active versions, `updateViaCache`, controller, and whether an update is stuck **waiting** (the #1 "why isn't my update showing"). |
| `sw_lifecycle_tail` | Cursor-paginated stream of SW lifecycle events (updatefound / statechange / controllerchange). |
| `cache_list` / `cache_inspect` / `cache_match` | CacheStorage caches + per-entry `{ url, status, content-type, size, ageSeconds, cache-control }`, and which cache serves a given URL — the core of stale-cache debugging. |
| `pwa_status` | Display mode / standalone, controller, permissions, and a live capability matrix (Push / Background Sync / Periodic Sync / Badging / File System Access / Window Controls Overlay). |
| `pwa_installability` | Discover + parse the manifest and run installability checks → structured `{ supported, gaps[], fixes[] }` with per-gap remediation. |
| `storage_get` | localStorage / sessionStorage snapshot (capped). |
| `idb_list` / `idb_query` | IndexedDB databases + object-store schema, then a read-only capped slice of records — *inspect IndexedDB live*. |
| `pwa_update_analyze` | Correlates SW status + cache ages + recent 404s into a verdict: waiting-update-on-active-client, cached-HTML-older-than-JS version skew, chunk 404s — *why are some users on old code?* |
| `pwa_snapshot` | One capped runtime-state blob (SW + store + web storage + IndexedDB + cache names) for deterministic bug-repro / hand-off. |

### Framework introspection

Read via each framework's own model — things CDP can't see. Tree/state where the framework persists it; element-level find everywhere.

| Tool family | Purpose |
|---|---|
| `react_tree` / `react_get_state` / `react_find_by_text` / `react_find_by_role` | React fiber-tree introspection + props/state/hooks by stable id + component lookup. |
| `vue_tree` / `vue_get_state` / `vue_find_by_text` / `vue_find_by_role` | Vue 3 `ComponentInternalInstance` tree + reactive state + lookup (parity with React). |
| `svelte_components` / `svelte_find_by_text` / `svelte_find_by_role` | Svelte component discovery + `__svelte_meta` source locations + element lookup (no instance state — Svelte exposes none). |
| `solid_detect` / `solid_find_by_text` / `solid_find_by_role` | Solid detection + element-level find (no persisted tree/state without `@solid-devtools`). |

### Store introspection

| Tool family | Purpose |
|---|---|
| `redux_get_state` / `redux_subscribe` / `redux_tail` / `redux_dispatch` | Redux read, change-delta subscribe/tail, JSONPath-lite slice, (opt-in) dispatch. |
| `store_get_state` / `store_subscribe` / `store_tail` / `store_dispatch` | Framework-agnostic adapter covering **Zustand / Pinia / Jotai** through one contract (read / subscribe / dispatch by name). |

### Interaction & touch gestures

Framework-agnostic native-event sequences dispatched so React/Vue/etc. delegated synthetic-event systems fire. Each targets a unified locator (selector / role / text / framework stable-id).

| Tool | Purpose |
|---|---|
| `pdl_click` / `pdl_dblclick` / `pdl_hover` | Pointer interactions. |
| `pdl_fill` / `pdl_submit` / `pdl_select_option` / `pdl_uncheck` | Form interactions. |
| `pdl_focus` / `pdl_blur` / `pdl_key_press` / `pdl_type_sequence` | Focus + keyboard. |
| `pdl_drag` / `pdl_scroll` / `pdl_swipe` / `pdl_tap` / `pdl_double_tap` / `pdl_long_press` / `pdl_pinch` | Touch/gesture primitives (direction/distance/duration/steps model — no hand-built coordinates). |

### Library popups

| Tool | Purpose |
|---|---|
| `popup_tail` | Tail the library-popup event stream (WalletConnect / RainbowKit / SDK modals, incl. shadow-root + nested). |
| `popup_record` / `popup_replay` | Intent-driven recording of a popup's full primary+nested stream (immune to ring-buffer eviction) + flat/primary/tree replay. |
| `popup_failures` | Correlate a popup's auth/connect failure with the console errors + failed requests during its open window. |

## Roadmap

- **Foundation** ✅ — pnpm workspace + build pipeline; MV3 extension loads cleanly; native-messaging round-trip; AI-managed host registration; cross-platform install (Linux native + macOS + Windows registry; snap unsupported); MCP↔IPC↔NMH↔SW bridge.
- **Capture** ✅ — console / network (fetch/XHR/WebSocket) / DOM-mutation / lifecycle producers; host ring buffers with disk spill + archive pruning; filterable, cursor-paginated `console_tail` / `network_tail`.
- **Framework introspection** ✅ — **React, Vue, Svelte, Solid** (trees/state where the framework persists them, find-by-text/role everywhere); page-world `evaluate`.
- **Store introspection** ✅ — **Redux** (read/subscribe/tail/dispatch) plus a framework-agnostic adapter covering **Zustand, Pinia, Jotai**.
- **Interaction & gestures** ✅ — discrete actions (click/fill/submit/hover/focus/blur/select/key/type) + touch gestures (drag/scroll/swipe/tap/double-tap/long-press/pinch) over a unified locator.
- **Library popups** ✅ — `popup_record` / `popup_replay` / `popup_tail` / `popup_failures` for WalletConnect & SDK modals (incl. shadow-root + nested).
- **Replay & source maps** ✅ — rrweb `session_record` / `session_replay`; `source_map_resolve` for stack frames.
- **Settings** ✅ — typed schema store (allowlist/blocklist, per-kind capture filters, per-site read controls, disk-spill).
- **Browser launcher** ✅ — `pdl_launch_browser` (existing + sandbox-persistent + sandbox-temp), `pdl_check_setup`, `pdl_browser_status`, `pdl_close_browser`, `pdl_install_extension`, `pdl_register_chrome_devtools`, and `chrome-devtools-mcp` coexistence.
- **PWA Runtime Diagnostics** ✅ — the namesake suite, live-verified against a real PWA. Service-worker introspection (`sw_status` + `sw_lifecycle_tail`: installing/waiting/active versions, `updateViaCache`, skipWaiting/claim/controllerchange); CacheStorage inspection (`cache_list` / `cache_inspect` / `cache_match` with size/age + match-by-URL); `pwa_status` (display mode, controller, permissions, live capability matrix — Push/Background Sync/Periodic Sync/Badging/File System Access/Window Controls Overlay); installability diagnostics (`pwa_installability` → structured `{gaps[], fixes[]}`); IndexedDB/web-storage inspection (`idb_list` / `idb_query` / `storage_get`); update-propagation / version-skew analysis (`pwa_update_analyze`); and a one-shot runtime-state snapshot (`pwa_snapshot`). Closes the stale-cache / "SW won't update" / install-failure / version-skew pain cluster against your real profile.
- **Later** — DevTools panel for human observation of an AI session; multi-tab routing model; event-causality graph (click → action → request → SW → cache → re-render); production-safe diagnostic mode for real-user failure capture.
- **Deferred** — Firefox port (needs WebDriver BiDi, not CDP); macOS/Windows live verification ([help wanted](#help-wanted-macos--windows-verification)); mobile; hosted/team mode.
- **Intentionally not pursued** — **Chrome Web Store distribution.** The extension grants broad page access (DOM, framework state, stores, network) and is only meaningful alongside its MCP host. It ships **bundled with the MCP only** and is installed via a manual, dev-mode "Load unpacked" (`pdl_install_extension` hands you the path + steps) — so every user knows exactly what they're running and why. Disabling Chrome Developer mode auto-disables it.

## Code style

- FP-only: pure functions, immutability, no OOP, no classes-with-methods.
- Side effects (CDP calls, file I/O, native messaging, MCP transport) at the edges; core logic pure.
- OOP library interfaces (Chrome APIs, MCP SDK, `winreg`) wrapped in thin functional adapters with injection points for tests.

## Repo layout

```
pwa-debug-layer/
├── packages/
│   ├── host/                Native-messaging host + MCP server (Node, ESM, rollup-bundled)
│   ├── extension/           WebExtension (MV3) — service worker, content script, page-world
│   └── shared/              Cross-package types
├── docs/
│   ├── PLAN.md              Full design doc (architecture, capability matrix, phased plan)
│   └── goals.txt
├── examples/                (future: test PWAs)
└── reference/               Read-only reference checkouts
```

## Contributing

This is a personal project under active redesign. PRs welcome but please open an issue first to discuss scope — the architecture is still settling. The FP / no-OOP discipline applies to all contributions; see `CLAUDE.md` for the full rules.

## License

MIT — see [`LICENSE`](LICENSE).

