cdp-mcp 0.1.2 → 0.2.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 +100 -37
- package/dist/contract.d.ts +11 -0
- package/dist/contract.js +11 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/locator.d.ts +108 -0
- package/dist/locator.js +176 -0
- package/dist/locator.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/dist/session/browser.d.ts +29 -0
- package/dist/session/browser.js +17 -2
- package/dist/session/browser.js.map +1 -1
- package/dist/session/buffers.d.ts +48 -0
- package/dist/session/pause.d.ts +21 -0
- package/dist/session/state.d.ts +53 -0
- package/dist/sourcemap/loader.d.ts +4 -0
- package/dist/sourcemap/normalize.d.ts +2 -0
- package/dist/sourcemap/store.d.ts +57 -0
- package/dist/tools/_locator_runtime.d.ts +31 -0
- package/dist/tools/_locator_runtime.js +243 -0
- package/dist/tools/_locator_runtime.js.map +1 -0
- package/dist/tools/_register.d.ts +2 -0
- package/dist/tools/breakpoints.d.ts +4 -0
- package/dist/tools/console.d.ts +2 -0
- package/dist/tools/dom.d.ts +2 -0
- package/dist/tools/dom.js +3 -221
- package/dist/tools/dom.js.map +1 -1
- package/dist/tools/execution.d.ts +29 -0
- package/dist/tools/forms.d.ts +8 -0
- package/dist/tools/forms.js +256 -0
- package/dist/tools/forms.js.map +1 -0
- package/dist/tools/inspect.d.ts +2 -0
- package/dist/tools/nav.d.ts +2 -0
- package/dist/tools/network.d.ts +2 -0
- package/dist/tools/session.d.ts +2 -0
- package/dist/tools/session.js +1 -1
- package/dist/tools/session.js.map +1 -1
- package/dist/tools/source.d.ts +2 -0
- package/dist/tools/storage.d.ts +2 -0
- package/dist/tools/storage.js +296 -0
- package/dist/tools/storage.js.map +1 -0
- package/dist/util/browser-resolve.d.ts +19 -0
- package/dist/util/errors.d.ts +7 -0
- package/dist/util/format.d.ts +20 -0
- package/dist/util/log.d.ts +6 -0
- package/docs/chromium-sandboxing.md +197 -0
- package/docs/known-chromium-gaps.md +138 -0
- package/docs/launchd-service.md +217 -0
- package/docs/local-l3-e2e-setup.md +199 -0
- package/docs/systemd-service.md +233 -0
- package/package.json +18 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Known Chromium gaps
|
|
2
|
+
|
|
3
|
+
Specs in the L3 e2e suite that fail (or fail intermittently) on Chromium but
|
|
4
|
+
pass on Chrome stable. Every entry below is a real coverage gap on Linux
|
|
5
|
+
ARM64 + Chromium (the day-1 primary target) — not a CI-only concession.
|
|
6
|
+
|
|
7
|
+
If the gap is mitigatable in production code, link the fix PR. If it's a CDP
|
|
8
|
+
protocol-version difference, link the Chromium release that includes the fix.
|
|
9
|
+
|
|
10
|
+
| Spec | Skip tag | CDP method missing/changed | Chromium version that fixes it | Tracking |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| _none yet — populate as L3 lands_ | | | | |
|
|
13
|
+
|
|
14
|
+
## Pre-flagged risks (not yet observed; documented for triage)
|
|
15
|
+
|
|
16
|
+
These are known protocol-version-sensitive areas the test+eval plan flagged
|
|
17
|
+
as risky during planning. Add a row above when one of them actually fires
|
|
18
|
+
on the e2e suite.
|
|
19
|
+
|
|
20
|
+
- **`Network.loadNetworkResource`** — Used by `src/sourcemap/loader.ts:113`
|
|
21
|
+
to fetch source maps through the browser's network stack (so cookies/
|
|
22
|
+
origin/auth flow naturally). Older Chromium revisions ship a more limited
|
|
23
|
+
param set (no `options.includeCredentials`, no `options.disableCache`);
|
|
24
|
+
the production code already has a Node-fetch fallback, but verify the
|
|
25
|
+
fallback path is exercised under the older Chromium.
|
|
26
|
+
|
|
27
|
+
- **`Page.captureScreenshot`** flag set — `captureBeyondViewport` and
|
|
28
|
+
`quality` (when `format=jpeg`) gained options across versions. The
|
|
29
|
+
screenshot e2e spec asserts byte-shape, not flag-respect, so this is
|
|
30
|
+
most likely to surface as a "bytes don't match" assertion under older
|
|
31
|
+
Chromium.
|
|
32
|
+
|
|
33
|
+
## Conventions
|
|
34
|
+
|
|
35
|
+
- Add a `// @chromium-skip — <gap-id>` comment on the spec's `it()` line.
|
|
36
|
+
- Set the spec to `it.skipIf(process.env.CDP_TEST_BROWSER === "chromium")` or
|
|
37
|
+
use vitest's `.skip` with a runtime check.
|
|
38
|
+
- Every skip MUST have a corresponding row in the table above. **Enforced** by
|
|
39
|
+
`scripts/check-chromium-skips.mjs` — runs as `pretest:e2e` on every PR and
|
|
40
|
+
also as `npm run lint:chromium-skips`. Greps `test/e2e/**/*.test.ts` for
|
|
41
|
+
`@chromium-skip` tags and `it.skipIf`/`describe.skipIf` Chromium guards,
|
|
42
|
+
parses the table above, exits 1 if any skip lacks a row OR any row points
|
|
43
|
+
at a spec that no longer exists. Zero-skip state (the current state) is
|
|
44
|
+
fine — the script is a no-op.
|
|
45
|
+
|
|
46
|
+
_(no entries below this line yet means no L3 specs needed a Chromium skip)_
|
|
47
|
+
|
|
48
|
+
## Known host gaps (not Chromium-version issues)
|
|
49
|
+
|
|
50
|
+
These are host/library combinations where the e2e suite cannot run end-to-
|
|
51
|
+
end, separate from the per-Chromium-version skip mechanism above. Listed
|
|
52
|
+
here so future contributors don't waste a debug cycle.
|
|
53
|
+
|
|
54
|
+
- **Windows 11 + chrome-launcher 1.2.1.** `chrome-launcher.launch()`'s
|
|
55
|
+
internal startup-port poll always fails with `ECONNREFUSED` on this Win11
|
|
56
|
+
configuration, regardless of headless mode (`--headless=new`, classic
|
|
57
|
+
`--headless`, or non-headless), browser (Chrome stable from Program
|
|
58
|
+
Files, Playwright-bundled Chromium under `~/AppData/Local/ms-playwright/
|
|
59
|
+
chromium-XXXX/chrome-win64/chrome.exe`), or how the port is selected
|
|
60
|
+
(chrome-launcher-managed vs explicit). Spawning `chrome.exe` directly via
|
|
61
|
+
`Start-Process` and probing `/json/version` over HTTP works fine — only
|
|
62
|
+
chrome-launcher's launch path fails. The same code works on Linux (CI)
|
|
63
|
+
and is widely used elsewhere, so this is a Windows-host quirk rather
|
|
64
|
+
than a cdp-mcp issue. **Workaround**: run L3 changes under WSL2
|
|
65
|
+
(Ubuntu) or push and let CI validate (but see WSL2 caveat below). Unit
|
|
66
|
+
+ L2 tests work fine on native Windows.
|
|
67
|
+
|
|
68
|
+
*Originally hit on agents/l3-impl during PR #11 implementation.
|
|
69
|
+
Cross-confirmed by Codex reviewer who diagnosed the on-CI failure
|
|
70
|
+
separately — turned out to be a different root cause (Codex blocker on
|
|
71
|
+
--remote-debugging-port=0 in chromeFlags overriding chrome-launcher's
|
|
72
|
+
own port). After that fix, CI on Linux is the live validation; Win11
|
|
73
|
+
local-host status remains as documented here.*
|
|
74
|
+
|
|
75
|
+
- **macOS arm64 + system unbranded Chromium (brew cask).** The `chromium`
|
|
76
|
+
Homebrew cask is **deprecated** ("does not pass the macOS Gatekeeper
|
|
77
|
+
check; will be disabled 2026-09-01"). Install completes and the wrapper
|
|
78
|
+
script lands at `/opt/homebrew/bin/chromium`, but on first launch
|
|
79
|
+
Gatekeeper rejects the `.app` as "damaged" and the binary is unusable
|
|
80
|
+
for unattended e2e/eval runs. Workaround for darwin-arm64: use Playwright
|
|
81
|
+
Chrome-for-Testing (`npx playwright install chromium`) — `resolveBrowser`
|
|
82
|
+
picks it up automatically from `~/Library/Caches/ms-playwright/chromium-
|
|
83
|
+
<rev>/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/
|
|
84
|
+
Google Chrome for Testing` (CfT layout added to `pickPlaywrightExe` in
|
|
85
|
+
the same PR that landed this entry). The resolver also explicitly skips
|
|
86
|
+
the brew-cask wrapper on darwin (`isBrewCaskChromium`) so users who
|
|
87
|
+
tried the deprecated cask first — and then turn to Playwright per this
|
|
88
|
+
entry — fall through to the Playwright cache instead of getting the
|
|
89
|
+
Gatekeeper-rejected wrapper back from Step 2. Functionally Chromium-
|
|
90
|
+
channel at a fixed protocol revision, but Google-branded — call it out
|
|
91
|
+
if your eval needs *unbranded* Chromium. Building Chromium from source
|
|
92
|
+
on macOS is multi-hour + multi-GB ongoing maintenance; not a viable
|
|
93
|
+
automation path.
|
|
94
|
+
|
|
95
|
+
*Originally hit while validating set_breakpoint idempotency on a
|
|
96
|
+
macOS arm64 host.*
|
|
97
|
+
|
|
98
|
+
- **WSL2 (Ubuntu) + snap-installed chromium.** Default-template Ubuntu on
|
|
99
|
+
WSL2 ships chromium as a snap (`/snap/bin/chromium`), which runs in a
|
|
100
|
+
confined namespace. chrome-launcher launches the binary successfully
|
|
101
|
+
(visible window appears under WSLg) but its startup-port poll
|
|
102
|
+
`ECONNREFUSEs` on iter 1 and often iter 2 — the debug port either
|
|
103
|
+
binds to a different port than chrome-launcher polled (race) or is
|
|
104
|
+
invisible across the snap sandbox boundary. After 2-4 retries the
|
|
105
|
+
agent's tool-use loop eventually picks an instance that responds, so
|
|
106
|
+
the eval does run, but every trial pays an inflated cost in retry
|
|
107
|
+
iterations and the trace is contaminated with WARN entries that look
|
|
108
|
+
like real failures. Same chrome-launcher code path is clean on macOS,
|
|
109
|
+
Linux native, and Linux CI.
|
|
110
|
+
|
|
111
|
+
**Workaround options**: (a) **prefer macOS or Linux native** for
|
|
112
|
+
interactive eval iteration — a macOS arm64 host confirmed clean;
|
|
113
|
+
(b) install non-snap chromium in WSL2 via apt or
|
|
114
|
+
symlink Playwright's bundled chromium to `/usr/local/bin/chromium-browser`
|
|
115
|
+
before the snap path; (c) accept the noise and rely on CI for the
|
|
116
|
+
authoritative signal. Don't rely on WSL2 for eval validation runs.
|
|
117
|
+
|
|
118
|
+
*A re-run on the same commit (`f0ce92a`) on a native host showed 0
|
|
119
|
+
chrome-launcher errors, isolating the cause to the WSL2 + snap-chromium
|
|
120
|
+
combination rather than the harness.*
|
|
121
|
+
|
|
122
|
+
- **Ubuntu 23.10+ (incl. 24.04) + Playwright-bundled Chromium.** Recent
|
|
123
|
+
Ubuntu kernels restrict unprivileged user namespaces via AppArmor, and
|
|
124
|
+
Playwright-bundled Chromium ships without a SUID `chrome_sandbox`
|
|
125
|
+
helper. Without `--no-sandbox`, Chromium FATALs at startup
|
|
126
|
+
(`zygote_host_impl_linux.cc: No usable sandbox!`) before opening its
|
|
127
|
+
debug port — chrome-launcher's port-poll loop then times out with
|
|
128
|
+
ECONNREFUSED, looking exactly like the WSL2/snap gap above but with a
|
|
129
|
+
different root cause. **Mitigation:** `launchChrome` defaults
|
|
130
|
+
`sandbox: false` so `--no-sandbox` is added automatically; eval
|
|
131
|
+
pipelines on this host work out-of-the-box. For the full security model,
|
|
132
|
+
including `sandbox: true`, AppArmor, snap confinement, and Bubblewrap, see
|
|
133
|
+
[docs/chromium-sandboxing.md](./chromium-sandboxing.md).
|
|
134
|
+
|
|
135
|
+
*First observed on an Ubuntu 24.04 arm64 host (Parallels VM) while
|
|
136
|
+
validating the L4 eval suite. Quick eval went from FAIL/$0.34/445s
|
|
137
|
+
(chrome-launcher retry storm) to PASS/$0.31/107s once `--no-sandbox`
|
|
138
|
+
was the default.*
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# macOS: Run as a Persistent Service (launchd)
|
|
2
|
+
|
|
3
|
+
Register `cdp-mcp` as a launchd user agent so it starts automatically on login
|
|
4
|
+
and exposes the MCP SSE endpoint on `127.0.0.1:9719`.
|
|
5
|
+
|
|
6
|
+
Persistent service mode is useful for MCP clients that support SSE because the
|
|
7
|
+
`cdp-mcp` process and its browser/CDP session can survive MCP client restarts or
|
|
8
|
+
reconnects. It does **not** persist state across service-process restarts.
|
|
9
|
+
|
|
10
|
+
> Security note: the local SSE endpoint has no authentication. MCP tools include
|
|
11
|
+
> in-page JavaScript evaluation and filesystem writes via screenshot paths. Only
|
|
12
|
+
> run a persistent service on trusted single-user machines, and do not bind it to
|
|
13
|
+
> non-loopback interfaces unless you understand the `--allow-remote` exposure.
|
|
14
|
+
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
- [1. Install the server](#1-install-the-server)
|
|
18
|
+
- [2. Create the plist](#2-create-the-plist)
|
|
19
|
+
- [3. Load and start](#3-load-and-start)
|
|
20
|
+
- [4. Verify](#4-verify)
|
|
21
|
+
- [5. Configure an MCP client](#5-configure-an-mcp-client)
|
|
22
|
+
- [6. Logs](#6-logs)
|
|
23
|
+
- [7. Stop / uninstall](#7-stop--uninstall)
|
|
24
|
+
- [8. Upgrade](#8-upgrade)
|
|
25
|
+
- [Troubleshooting](#troubleshooting)
|
|
26
|
+
|
|
27
|
+
## 1. Install the server
|
|
28
|
+
|
|
29
|
+
Requires Node.js 20+ and a local Chrome/Chromium browser.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g cdp-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Verify with `cdp-mcp --help`. The package ships prebuilt `dist/`, so there is no
|
|
36
|
+
build step and no repo checkout needed.
|
|
37
|
+
|
|
38
|
+
If `launch_chrome` cannot find Chrome/Chromium automatically, set `CHROME_PATH`
|
|
39
|
+
in the plist generated below.
|
|
40
|
+
|
|
41
|
+
## 2. Create the plist
|
|
42
|
+
|
|
43
|
+
Run this from any directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# If you use fnm, nvm, or another Node version manager, set these variables to
|
|
47
|
+
# stable paths before running this snippet. Example:
|
|
48
|
+
# NODE_BIN="$HOME/.local/share/fnm/aliases/default/bin/node"
|
|
49
|
+
# CDP_SCRIPT="$HOME/.local/share/fnm/aliases/default/bin/cdp-mcp"
|
|
50
|
+
NODE_BIN="${NODE_BIN:-$(command -v node)}"
|
|
51
|
+
CDP_SCRIPT="${CDP_SCRIPT:-$(command -v cdp-mcp)}"
|
|
52
|
+
CHROME_PATH="${CHROME_PATH:-}"
|
|
53
|
+
|
|
54
|
+
if [ -z "$NODE_BIN" ]; then
|
|
55
|
+
echo "Error: node not found in PATH. Install Node 20+ first." >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
if [ -z "$CDP_SCRIPT" ]; then
|
|
59
|
+
echo "Error: cdp-mcp not found. Run 'npm install -g cdp-mcp' first." >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
xml_escape() {
|
|
64
|
+
printf '%s' "$1" \
|
|
65
|
+
| sed \
|
|
66
|
+
-e 's/&/\&/g' \
|
|
67
|
+
-e 's/</\</g' \
|
|
68
|
+
-e 's/>/\>/g' \
|
|
69
|
+
-e 's/"/\"/g' \
|
|
70
|
+
-e "s/'/\'/g"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
NODE_DIR="$(dirname "$NODE_BIN")"
|
|
74
|
+
mkdir -p ~/Library/LaunchAgents ~/Library/Logs/cdp-mcp
|
|
75
|
+
ESC_NODE=$(xml_escape "$NODE_BIN")
|
|
76
|
+
ESC_NODE_DIR=$(xml_escape "$NODE_DIR")
|
|
77
|
+
ESC_CDP=$(xml_escape "$CDP_SCRIPT")
|
|
78
|
+
ESC_HOME=$(xml_escape "$HOME")
|
|
79
|
+
ESC_CHROME=$(xml_escape "$CHROME_PATH")
|
|
80
|
+
cat > ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist <<PLIST
|
|
81
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
83
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
84
|
+
<plist version="1.0">
|
|
85
|
+
<dict>
|
|
86
|
+
<key>Label</key>
|
|
87
|
+
<string>io.github.lcjanke2020.cdp-mcp</string>
|
|
88
|
+
<key>ProgramArguments</key>
|
|
89
|
+
<array>
|
|
90
|
+
<string>$ESC_NODE</string>
|
|
91
|
+
<string>$ESC_CDP</string>
|
|
92
|
+
<string>--port</string>
|
|
93
|
+
<string>9719</string>
|
|
94
|
+
</array>
|
|
95
|
+
<key>RunAtLoad</key>
|
|
96
|
+
<true/>
|
|
97
|
+
<key>KeepAlive</key>
|
|
98
|
+
<true/>
|
|
99
|
+
<key>StandardOutPath</key>
|
|
100
|
+
<string>$ESC_HOME/Library/Logs/cdp-mcp/server.stdout.log</string>
|
|
101
|
+
<key>StandardErrorPath</key>
|
|
102
|
+
<string>$ESC_HOME/Library/Logs/cdp-mcp/server.stderr.log</string>
|
|
103
|
+
<key>EnvironmentVariables</key>
|
|
104
|
+
<dict>
|
|
105
|
+
<key>PATH</key>
|
|
106
|
+
<string>$ESC_NODE_DIR:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
107
|
+
$(if [ -n "$CHROME_PATH" ]; then printf ' <key>CHROME_PATH</key>\n <string>%s</string>\n' "$ESC_CHROME"; fi)
|
|
108
|
+
</dict>
|
|
109
|
+
</dict>
|
|
110
|
+
</plist>
|
|
111
|
+
PLIST
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The plist invokes `node` directly with the `cdp-mcp` script path. That makes the
|
|
115
|
+
`NODE_BIN` override authoritative even when your shell uses a Node version
|
|
116
|
+
manager.
|
|
117
|
+
|
|
118
|
+
## 3. Load and start
|
|
119
|
+
|
|
120
|
+
On macOS 10.15+, use:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
|
|
124
|
+
launchctl kickstart -k gui/$UID/io.github.lcjanke2020.cdp-mcp
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Older macOS releases also support:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
launchctl load ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 4. Verify
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
launchctl print gui/$UID/io.github.lcjanke2020.cdp-mcp
|
|
137
|
+
lsof -i :9719
|
|
138
|
+
curl -v --max-time 2 http://127.0.0.1:9719/sse 2>&1 | head -20
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `curl` command should show a `200 OK` response and SSE event output. A
|
|
142
|
+
timeout after the first event is expected because `/sse` keeps the connection
|
|
143
|
+
open. The server also sends periodic SSE keepalive comments by default; tune
|
|
144
|
+
with `CDP_MCP_SSE_KEEPALIVE_MS` only if your MCP client needs a different idle
|
|
145
|
+
interval.
|
|
146
|
+
|
|
147
|
+
## 5. Configure an MCP client
|
|
148
|
+
|
|
149
|
+
Point an SSE-capable MCP client at:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
http://127.0.0.1:9719/sse
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For example, clients that use JSON MCP server config commonly use:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"cdp-mcp": {
|
|
161
|
+
"type": "sse",
|
|
162
|
+
"url": "http://127.0.0.1:9719/sse"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
SSE mode is single-client today. Multiple MCP clients connected to the same
|
|
169
|
+
service share one process-global browser/CDP session and can interfere with each
|
|
170
|
+
other. Use one active debugging client per service, or run separate services on
|
|
171
|
+
separate ports.
|
|
172
|
+
|
|
173
|
+
A reconnecting client resumes the prior session. If you want a clean browser
|
|
174
|
+
session after reconnecting, call `close_session` before launching or attaching
|
|
175
|
+
again.
|
|
176
|
+
|
|
177
|
+
## 6. Logs
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
tail -f ~/Library/Logs/cdp-mcp/server.stderr.log
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## 7. Stop / uninstall
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
launchctl bootout gui/$UID/io.github.lcjanke2020.cdp-mcp
|
|
187
|
+
rm ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Older macOS releases also support:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
launchctl unload ~/Library/LaunchAgents/io.github.lcjanke2020.cdp-mcp.plist
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 8. Upgrade
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm install -g cdp-mcp@latest
|
|
200
|
+
launchctl kickstart -k gui/$UID/io.github.lcjanke2020.cdp-mcp
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Restart or reconnect your MCP client after a server upgrade so it reloads tool
|
|
204
|
+
schemas.
|
|
205
|
+
|
|
206
|
+
## Troubleshooting
|
|
207
|
+
|
|
208
|
+
| Symptom | Fix |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `bootstrap` says service already loaded | Run `launchctl bootout gui/$UID/io.github.lcjanke2020.cdp-mcp`, then bootstrap again |
|
|
211
|
+
| Service exits immediately | Check `~/Library/Logs/cdp-mcp/server.stderr.log`; usually `cdp-mcp` is not installed, Node is too old, or a version-manager path moved |
|
|
212
|
+
| Port 9719 is already in use | Check `lsof -i :9719`, then stop the other process or change the port in the plist |
|
|
213
|
+
| MCP client rejects the config | Confirm the client supports SSE MCP servers and include both `"type": "sse"` and the `/sse` URL if your client uses JSON config |
|
|
214
|
+
| `launch_chrome` cannot find Chrome | Set `CHROME_PATH` before generating the plist, or edit the plist environment and reload the service |
|
|
215
|
+
| Service not starting after reboot | Verify the plist is in `~/Library/LaunchAgents/`, not `LaunchDaemons` |
|
|
216
|
+
| Service not starting after reboot with fnm/nvm | Version-manager shell paths can be ephemeral. Recreate the plist with stable `NODE_BIN` and `CDP_SCRIPT` paths, or install with a system Node |
|
|
217
|
+
| `already_session` after reconnecting | The prior browser/CDP session is still alive. Resume it, or call `close_session` before starting fresh |
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Local L3 e2e setup (Playwright Chromium + AppArmor)
|
|
2
|
+
|
|
3
|
+
**Last updated: 2026-06-09**
|
|
4
|
+
|
|
5
|
+
A step-by-step runbook for getting `npm run test:e2e` (the L3 real-browser
|
|
6
|
+
suite) passing on a local Linux machine **with Chromium's sandbox on**. This is
|
|
7
|
+
the practical companion to [`chromium-sandboxing.md`](./chromium-sandboxing.md);
|
|
8
|
+
read that for the full `--no-sandbox` / `sandbox: true` threat model and the
|
|
9
|
+
validated-hosts table.
|
|
10
|
+
|
|
11
|
+
Assumes Ubuntu (23.10+/24.04). Other distributions may need no host-side work at
|
|
12
|
+
all — see "Other distributions" below.
|
|
13
|
+
|
|
14
|
+
## Why this is needed on Ubuntu
|
|
15
|
+
|
|
16
|
+
The L3 e2e harness launches Chromium with the sandbox **on** when running
|
|
17
|
+
locally; it only adds `--no-sandbox` when the `CI` env var is set
|
|
18
|
+
(`test/e2e/setup/global.ts`). That is deliberate — locally we want the sandbox
|
|
19
|
+
when the host can provide it.
|
|
20
|
+
|
|
21
|
+
But recent Ubuntu releases ship:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
kernel.apparmor_restrict_unprivileged_userns = 1
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
which blocks the unprivileged **user namespace** that Chromium's sandbox needs.
|
|
28
|
+
Playwright-bundled Chromium does not ship a SUID `chrome_sandbox` helper, so on a
|
|
29
|
+
stock Ubuntu host a sandbox-on launch fails before the DevTools port opens:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
zygote_host_impl_linux.cc: No usable sandbox!
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From `chrome-launcher` this usually surfaces as a startup port-poll timeout or
|
|
36
|
+
`ECONNREFUSED`.
|
|
37
|
+
|
|
38
|
+
The fix is an AppArmor profile that grants `userns,` to the Playwright Chromium
|
|
39
|
+
binary, giving it a stable named label that is allowed to create the user
|
|
40
|
+
namespace. The steps below install Chromium, confirm the resolver finds it,
|
|
41
|
+
attach the profile, and run the suite.
|
|
42
|
+
|
|
43
|
+
## 1. Install Playwright Chromium
|
|
44
|
+
|
|
45
|
+
From the repo:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx --yes playwright install chromium
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This drops a managed Chromium into the per-user cache:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
~/.cache/ms-playwright/chromium-<rev>/chrome-linux*/chrome
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The leaf directory varies by Playwright version and arch — `chrome-linux` on
|
|
58
|
+
ARM64 and older builds, `chrome-linux64` on x86_64 with the newer
|
|
59
|
+
Chrome-for-Testing layout (the resolver and the AppArmor glob below cover both).
|
|
60
|
+
Install it for **each OS user** that will run the suite — the cache is
|
|
61
|
+
per-`$HOME`.
|
|
62
|
+
|
|
63
|
+
## 2. Verify the resolver finds it
|
|
64
|
+
|
|
65
|
+
The launcher resolver (`src/util/browser-resolve.ts`) finds Chromium in this
|
|
66
|
+
order: an explicit `CDP_TEST_BROWSER_PATH`, then a system `chromium` on `PATH`,
|
|
67
|
+
then the Playwright cache. After a build, confirm what it picks:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm run build
|
|
71
|
+
node --input-type=module \
|
|
72
|
+
-e "import('./dist/util/browser-resolve.js').then(m => console.log(JSON.stringify(m.resolveBrowser(), null, 2)))"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This runbook targets the **Playwright-cache** binary, so expect
|
|
76
|
+
`source: "playwright-cache"` and a `binaryPath` under `~/.cache/ms-playwright/`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"binaryPath": "/home/<user>/.cache/ms-playwright/chromium-<rev>/chrome-linux/chrome",
|
|
81
|
+
"choice": "chromium",
|
|
82
|
+
"snapConfined": false,
|
|
83
|
+
"source": "playwright-cache"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If you have a system Chromium on `PATH` (apt `/usr/bin/chromium`, snap
|
|
88
|
+
`/snap/bin/chromium`), the resolver returns that first with
|
|
89
|
+
`source: "which-chromium"` — that binary is *not* covered by the AppArmor
|
|
90
|
+
profile below (snap brings its own confinement; apt Chromium is a separate
|
|
91
|
+
sandbox story). To exercise the Playwright binary under this profile, point the
|
|
92
|
+
suite at it explicitly:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
export CDP_TEST_BROWSER_PATH="$(ls -d ~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome | head -1)"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 3. Attach the AppArmor profile
|
|
99
|
+
|
|
100
|
+
First confirm the kernel knob is the restrictive default:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
sysctl kernel.apparmor_restrict_unprivileged_userns # = 1 on stock Ubuntu 24.04
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If it is `0` (some hosts turn it off system-wide), Chromium's sandbox already
|
|
107
|
+
works and you can skip to step 5.
|
|
108
|
+
|
|
109
|
+
Create a profile that grants `userns,` to the Playwright Chromium binary path.
|
|
110
|
+
This mirrors the shape of Ubuntu's stock `chrome` / `msedge` / `brave` profiles
|
|
111
|
+
(a named-unconfined profile that opts into user namespaces):
|
|
112
|
+
|
|
113
|
+
```apparmor
|
|
114
|
+
# /etc/apparmor.d/cdp-mcp-chromium
|
|
115
|
+
abi <abi/4.0>,
|
|
116
|
+
include <tunables/global>
|
|
117
|
+
|
|
118
|
+
profile cdp-mcp-chromium /home/*/.cache/ms-playwright/chromium-*/chrome-linux*/chrome flags=(unconfined) {
|
|
119
|
+
userns,
|
|
120
|
+
|
|
121
|
+
include if exists <local/cdp-mcp-chromium>
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `chrome-linux*` component matches both the `chrome-linux` (ARM64/older) and
|
|
126
|
+
`chrome-linux64` (x86_64 Chrome-for-Testing) layouts — in AppArmor `*` matches
|
|
127
|
+
within a single path segment, so a too-specific `chrome-linux` would silently
|
|
128
|
+
fail to attach on x86_64. The `/home/*/` glob matches any user's Playwright
|
|
129
|
+
cache; if you prefer to scope it to specific accounts, replace `*` with a brace
|
|
130
|
+
list of usernames, e.g. `/home/{alice,bob}/.cache/...`.
|
|
131
|
+
|
|
132
|
+
Load it (profiles in `/etc/apparmor.d/` also auto-load at boot):
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
sudo apparmor_parser -r /etc/apparmor.d/cdp-mcp-chromium
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 4. Verify the label attaches
|
|
139
|
+
|
|
140
|
+
Launch the bundled Chromium sandbox-on and read its AppArmor label — it must be
|
|
141
|
+
the named profile, not bare `unconfined`:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
BIN=$(ls -d ~/.cache/ms-playwright/chromium-*/chrome-linux*/chrome | head -1)
|
|
145
|
+
"$BIN" --headless=new --no-startup-window --remote-debugging-port=0 \
|
|
146
|
+
--user-data-dir=$(mktemp -d) about:blank & pid=$!
|
|
147
|
+
sleep 3; cat /proc/$pid/attr/current # -> cdp-mcp-chromium (unconfined)
|
|
148
|
+
kill $pid
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If this prints `cdp-mcp-chromium (unconfined)`, the profile is attached. A bare
|
|
152
|
+
`unconfined` means the binary path didn't match the profile's glob — re-check
|
|
153
|
+
the cache path against the profile.
|
|
154
|
+
|
|
155
|
+
## 5. Run L3
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
npm run test:e2e
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
With the sandbox on and the profile attached, the suite should pass, e.g.:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
Test Files 10 passed (10)
|
|
165
|
+
Tests 29 passed (29)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Fallback (before AppArmor is configured)
|
|
169
|
+
|
|
170
|
+
The L3 harness adds `--no-sandbox` when `CI` is set, so you can run the suite
|
|
171
|
+
without the profile as a lower-security stopgap:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
env CI=1 npm run test:e2e
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This keeps work moving, but the AppArmor profile is the desired long-term
|
|
178
|
+
posture so that plain `npm run test:e2e` exercises sandbox-on Chromium. See
|
|
179
|
+
[`chromium-sandboxing.md`](./chromium-sandboxing.md) for why `--no-sandbox`
|
|
180
|
+
widens the blast radius of a compromised renderer.
|
|
181
|
+
|
|
182
|
+
## Other distributions
|
|
183
|
+
|
|
184
|
+
This profile work is Ubuntu-specific. Distributions that don't restrict
|
|
185
|
+
unprivileged user namespaces by default (or use SELinux instead of AppArmor,
|
|
186
|
+
e.g. Fedora) generally run sandbox-on Chromium without a host-side profile.
|
|
187
|
+
Validate the actual host before assuming the Ubuntu steps are required — check
|
|
188
|
+
`sysctl kernel.apparmor_restrict_unprivileged_userns` (absent or `0` means no
|
|
189
|
+
AppArmor userns restriction to work around).
|
|
190
|
+
|
|
191
|
+
## Related
|
|
192
|
+
|
|
193
|
+
- [`docs/chromium-sandboxing.md`](./chromium-sandboxing.md) — the canonical
|
|
194
|
+
`--no-sandbox` / `sandbox: true` threat model, the AppArmor / userns / snap /
|
|
195
|
+
Bubblewrap mechanism map, and the validated-hosts table.
|
|
196
|
+
- [`docs/known-chromium-gaps.md`](./known-chromium-gaps.md) — per-spec
|
|
197
|
+
Chromium-vs-Chrome gaps and host-OS workarounds.
|
|
198
|
+
- [README §L3](../README.md) — browser selection, `CDP_TEST_BROWSER_PATH`, and
|
|
199
|
+
the per-platform support matrix.
|