cdp-mcp 0.1.2 → 0.1.3
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 +33 -6
- package/docs/chromium-sandboxing.md +191 -0
- package/docs/known-chromium-gaps.md +138 -0
- package/docs/launchd-service.md +217 -0
- package/docs/systemd-service.md +233 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A Model Context Protocol (MCP) server that exposes the Chrome DevTools Protocol
|
|
|
4
4
|
|
|
5
5
|
Designed for agents running in CLIs (Claude Code, GitHub Copilot CLI) that have local source + source-map access. Coordinates flow in TS terms; the server translates to JS for CDP under the hood.
|
|
6
6
|
|
|
7
|
-
**Status:** alpha
|
|
7
|
+
**Status:** alpha. **License:** [MIT](./LICENSE).
|
|
8
8
|
|
|
9
9
|
## What it gives an agent
|
|
10
10
|
|
|
@@ -22,6 +22,34 @@ Auto-attaches to iframes and workers via `Target.setAutoAttach({ flatten: true }
|
|
|
22
22
|
|
|
23
23
|
## Install / build
|
|
24
24
|
|
|
25
|
+
### Runtime install from npm
|
|
26
|
+
|
|
27
|
+
Requires Node.js 20+ and a local Chrome/Chromium browser.
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install -g cdp-mcp
|
|
31
|
+
cdp-mcp # stdio MCP transport
|
|
32
|
+
cdp-mcp --port 9719 # SSE MCP transport on 127.0.0.1:9719
|
|
33
|
+
cdp-mcp --host 0.0.0.0 --port 9719 --allow-remote
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The npm package ships prebuilt `dist/`, so there is no build step for runtime
|
|
37
|
+
use. If `launch_chrome` cannot find Chrome/Chromium automatically, set
|
|
38
|
+
`CHROME_PATH` to the browser binary.
|
|
39
|
+
|
|
40
|
+
For MCP clients that support SSE, you can run `cdp-mcp` as a persistent local
|
|
41
|
+
service:
|
|
42
|
+
|
|
43
|
+
- [macOS launchd user service](docs/launchd-service.md)
|
|
44
|
+
- [Linux systemd user service](docs/systemd-service.md)
|
|
45
|
+
|
|
46
|
+
Persistent service mode keeps the `cdp-mcp` process and current browser/CDP
|
|
47
|
+
session alive across MCP client restarts or reconnects. It does **not** persist
|
|
48
|
+
state across service-process restarts. SSE mode is single-client today; if a
|
|
49
|
+
new client should start fresh, call `close_session` first.
|
|
50
|
+
|
|
51
|
+
### Build from source
|
|
52
|
+
|
|
25
53
|
```sh
|
|
26
54
|
npm install
|
|
27
55
|
npm run build
|
|
@@ -62,11 +90,10 @@ Unit + L2 contract tests (~640ms, no browser, no LLM):
|
|
|
62
90
|
npm test
|
|
63
91
|
```
|
|
64
92
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
See `docs/test-eval-plan.md` for the full pyramid.
|
|
93
|
+
The `test/` tree is the L2 contract layer (every tool exercised against a fake
|
|
94
|
+
CDP — see `test/fake-cdp.ts`); the inline `src/**/*.test.ts` files are L1
|
|
95
|
+
pure-data tests; `evals/**/*.test.ts` cover the L4 harness's
|
|
96
|
+
grader/trace/oracle units. See `docs/test-eval-plan.md` for the full pyramid.
|
|
70
97
|
|
|
71
98
|
### L3 — real-browser end-to-end
|
|
72
99
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Chromium sandboxing
|
|
2
|
+
|
|
3
|
+
**Last updated: 2026-05-16**
|
|
4
|
+
|
|
5
|
+
This project launches Chromium in two different contexts:
|
|
6
|
+
|
|
7
|
+
- L3 e2e tests and L4 evals launch a real browser through `launch_chrome`.
|
|
8
|
+
- Agents then control that browser through CDP, including `Runtime.evaluate`,
|
|
9
|
+
`Debugger.*`, DOM interaction, console inspection, and network inspection.
|
|
10
|
+
|
|
11
|
+
That makes sandboxing a host setup and threat-model decision, not just a
|
|
12
|
+
Chrome flag. This document is the canonical reference for `--no-sandbox`,
|
|
13
|
+
AppArmor, unprivileged user namespaces, snap confinement, and Bubblewrap.
|
|
14
|
+
|
|
15
|
+
## Current default
|
|
16
|
+
|
|
17
|
+
`launch_chrome` defaults to `sandbox: false`, which adds `--no-sandbox`.
|
|
18
|
+
|
|
19
|
+
The default exists because Ubuntu 23.10+ and 24.04 commonly restrict
|
|
20
|
+
unprivileged user namespaces through AppArmor. Playwright-bundled Chromium
|
|
21
|
+
does not ship with a SUID `chrome_sandbox` helper. On those hosts, launching
|
|
22
|
+
Chromium without `--no-sandbox` can fail before the DevTools port opens:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
zygote_host_impl_linux.cc: No usable sandbox!
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
From `chrome-launcher`, that often surfaces as a startup port poll timeout or
|
|
29
|
+
`ECONNREFUSED`.
|
|
30
|
+
|
|
31
|
+
Other Linux distributions may allow Chromium's unprivileged user namespace
|
|
32
|
+
sandbox path by default. Validate the actual host before assuming the Ubuntu
|
|
33
|
+
automation default is necessary there.
|
|
34
|
+
|
|
35
|
+
Use `sandbox: true` only when the host has a working Chromium sandbox path:
|
|
36
|
+
|
|
37
|
+
- An AppArmor policy that permits the specific Chromium binary to create the
|
|
38
|
+
unprivileged user namespace it needs.
|
|
39
|
+
- Or a working SUID `chrome_sandbox` helper installed alongside that binary.
|
|
40
|
+
|
|
41
|
+
## Why the Chromium sandbox still matters
|
|
42
|
+
|
|
43
|
+
The MCP caller is already highly privileged relative to the page. It can ask
|
|
44
|
+
the server to evaluate JavaScript, drive the DOM, set breakpoints, inspect
|
|
45
|
+
scopes, and read browser-observed network activity. For that caller, the
|
|
46
|
+
Chromium renderer sandbox is not the primary trust boundary.
|
|
47
|
+
|
|
48
|
+
The Chromium sandbox still matters for hostile page content and browser
|
|
49
|
+
exploitation risk. With the sandbox enabled, Chromium isolates renderer, GPU,
|
|
50
|
+
and utility child processes from the browser process using mechanisms such as
|
|
51
|
+
namespaces, seccomp filters, brokered filesystem access, and per-process
|
|
52
|
+
capability reduction. With `--no-sandbox`, a compromised renderer has a much
|
|
53
|
+
larger blast radius inside the browser process tree.
|
|
54
|
+
|
|
55
|
+
So the project default is a pragmatic automation default, not a claim that
|
|
56
|
+
`--no-sandbox` is equally safe.
|
|
57
|
+
|
|
58
|
+
## How the mechanisms relate
|
|
59
|
+
|
|
60
|
+
These mechanisms solve different problems:
|
|
61
|
+
|
|
62
|
+
- Chromium sandbox: Chromium's internal process sandbox. It is the boundary
|
|
63
|
+
between web content renderer processes and the rest of the browser.
|
|
64
|
+
- AppArmor: host-enforced mandatory access control. It can confine Chromium,
|
|
65
|
+
and on recent Ubuntu systems it can also restrict whether an unprivileged
|
|
66
|
+
process may create user namespaces.
|
|
67
|
+
- Snap confinement: the packaging sandbox used by snap-installed Chromium.
|
|
68
|
+
It can hide or remap filesystem locations and has caused DevTools port and
|
|
69
|
+
`userDataDir` friction in local runs.
|
|
70
|
+
- Bubblewrap (`bwrap`): a small Linux sandboxing tool that starts a process in
|
|
71
|
+
new namespaces with a controlled filesystem, process, and optional network
|
|
72
|
+
view.
|
|
73
|
+
|
|
74
|
+
Bubblewrap is useful defense-in-depth around a browser or eval job. For
|
|
75
|
+
example, it can run the whole MCP server plus browser process tree with only
|
|
76
|
+
the repository and selected temp/profile directories writable. That helps
|
|
77
|
+
prevent accidental or malicious access to unrelated files such as SSH keys,
|
|
78
|
+
cloud credentials, or the rest of the home directory.
|
|
79
|
+
|
|
80
|
+
Bubblewrap is not a clean substitute for Chromium's own sandbox. If Chromium
|
|
81
|
+
runs with `--no-sandbox` inside Bubblewrap, the entire browser process tree is
|
|
82
|
+
inside an outer container-like boundary, but Chromium's internal renderer
|
|
83
|
+
isolation is still disabled. A renderer compromise may be contained by the
|
|
84
|
+
outer Bubblewrap filesystem or network policy, but it is not contained in the
|
|
85
|
+
same way as Chromium's per-renderer sandbox.
|
|
86
|
+
|
|
87
|
+
## Recommended posture
|
|
88
|
+
|
|
89
|
+
Local development:
|
|
90
|
+
|
|
91
|
+
- Treat the current default, `sandbox: false`, as a host-capability fallback:
|
|
92
|
+
it keeps work moving when Chromium sandbox setup is the blocker.
|
|
93
|
+
- Prefer `sandbox: true` once the host has a known-good Chromium sandbox path.
|
|
94
|
+
|
|
95
|
+
CI and L4 eval hosts:
|
|
96
|
+
|
|
97
|
+
- Keep the default working and deterministic. If `--no-sandbox` is needed for
|
|
98
|
+
Playwright-bundled Chromium on Ubuntu, document that in the host setup.
|
|
99
|
+
- Consider running the whole job inside an outer sandbox, VM, container, or
|
|
100
|
+
Bubblewrap profile with a throwaway browser profile and limited writable
|
|
101
|
+
paths.
|
|
102
|
+
|
|
103
|
+
Untrusted browsing:
|
|
104
|
+
|
|
105
|
+
- Prefer Chromium's sandbox on.
|
|
106
|
+
- Use a throwaway `userDataDir`.
|
|
107
|
+
- Add host-level confinement such as AppArmor, a container, VM, or Bubblewrap.
|
|
108
|
+
- Do not treat `bwrap + --no-sandbox` as equivalent to Chromium sandboxing.
|
|
109
|
+
|
|
110
|
+
## Validated hosts
|
|
111
|
+
|
|
112
|
+
Hosts where `sandbox: true` has been verified working against this project, with the supporting posture:
|
|
113
|
+
|
|
114
|
+
| Host | OS | Arch | `sandbox: true` | AppArmor profile | Notes |
|
|
115
|
+
|---|---|---|---|---|---|
|
|
116
|
+
| Ubuntu 24.04 arm64 (Parallels VM) | Ubuntu 24.04 | arm64 | ✓ | `/etc/apparmor.d/cdp-mcp-chromium` (named-unconfined, mirrors Ubuntu's stock `chrome` / `msedge` / `brave`) | `kernel.apparmor_restrict_unprivileged_userns = 0` was set as a side effect of enabling Bubblewrap, so the kernel-level userns restriction is already off system-wide. The AppArmor profile gives Playwright Chromium a stable named label (instead of `unconfined`) and grants `userns,` explicitly, so `sandbox: true` keeps working even if a future kernel/package update flips the global knob back to `1`. |
|
|
117
|
+
|
|
118
|
+
When adding a new host to this table:
|
|
119
|
+
|
|
120
|
+
1. Run the smoke tests below and capture the values.
|
|
121
|
+
2. If `kernel.apparmor_restrict_unprivileged_userns` is `1` (Ubuntu's stock default) and `sandbox: true` is desired, install a profile under `/etc/apparmor.d/` mirroring Ubuntu's stock `chrome` / `msedge` / `brave` shape:
|
|
122
|
+
```apparmor
|
|
123
|
+
abi <abi/4.0>,
|
|
124
|
+
include <tunables/global>
|
|
125
|
+
|
|
126
|
+
profile cdp-mcp-chromium /path/to/chromium flags=(unconfined) {
|
|
127
|
+
userns,
|
|
128
|
+
include if exists <local/cdp-mcp-chromium>
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
Load with `sudo apparmor_parser -r /etc/apparmor.d/cdp-mcp-chromium`. The profile auto-loads at boot from `/etc/apparmor.d/`.
|
|
132
|
+
3. Verify the running browser process is labelled correctly:
|
|
133
|
+
```sh
|
|
134
|
+
cat /proc/<chromium-pid>/attr/current
|
|
135
|
+
```
|
|
136
|
+
Expect the profile name (e.g. `cdp-mcp-chromium (unconfined)`), not `unconfined` alone.
|
|
137
|
+
4. Add a row to the table above.
|
|
138
|
+
|
|
139
|
+
Other hosts to characterize as they come online: Fedora — Fedora uses SELinux rather than AppArmor and ships userns enabled by default, so `sandbox: true` is expected to work without host-side profile work. The `dnf install bubblewrap` path is also first-class on Fedora.
|
|
140
|
+
|
|
141
|
+
## Smoke tests
|
|
142
|
+
|
|
143
|
+
Check whether unprivileged user namespaces are enabled:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
cat /proc/sys/kernel/unprivileged_userns_clone
|
|
147
|
+
cat /proc/sys/user/max_user_namespaces
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Expected working values are usually `1` for
|
|
151
|
+
`kernel.unprivileged_userns_clone` and a nonzero number for
|
|
152
|
+
`user.max_user_namespaces`.
|
|
153
|
+
|
|
154
|
+
On Ubuntu, AppArmor may still restrict unprivileged user namespaces:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
sysctl kernel.apparmor_restrict_unprivileged_userns
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Minimal Bubblewrap smoke tests:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / /usr/bin/true
|
|
164
|
+
bwrap --unshare-user --uid 0 --gid 0 --unshare-net --ro-bind / / /usr/bin/true
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If the first command fails with `setting up uid map: Permission denied`, the
|
|
168
|
+
host still blocks the user namespace setup Bubblewrap needs.
|
|
169
|
+
|
|
170
|
+
If the second command fails with `loopback: Failed RTM_NEWADDR: Operation not
|
|
171
|
+
permitted`, the network namespace setup is still blocked.
|
|
172
|
+
|
|
173
|
+
Project-level browser check:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
npm run test:e2e
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
For a direct MCP check, call `launch_chrome` with `sandbox: true` on the host
|
|
180
|
+
you want to validate. If Chromium cannot create a usable sandbox, it will fail
|
|
181
|
+
before exposing its DevTools target.
|
|
182
|
+
|
|
183
|
+
## Decision summary
|
|
184
|
+
|
|
185
|
+
- `--no-sandbox` remains the automation default because it keeps Ubuntu
|
|
186
|
+
Playwright-Chromium runs working.
|
|
187
|
+
- `sandbox: true` is the preferred security posture when the host supports it.
|
|
188
|
+
- AppArmor is the long-term host policy path for allowing Chromium's needed
|
|
189
|
+
user namespace behavior narrowly.
|
|
190
|
+
- Bubblewrap is an outer containment layer and is valuable defense-in-depth.
|
|
191
|
+
- Bubblewrap does not replace Chromium's internal renderer sandbox.
|
|
@@ -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,233 @@
|
|
|
1
|
+
# Linux: Run as a Persistent Service (systemd)
|
|
2
|
+
|
|
3
|
+
Register `cdp-mcp` as a systemd user service so it starts automatically on login
|
|
4
|
+
and exposes the MCP SSE endpoint on `127.0.0.1:9719`. If you enable lingering,
|
|
5
|
+
the service can also start at boot before an interactive login.
|
|
6
|
+
|
|
7
|
+
Persistent service mode is useful for MCP clients that support SSE because the
|
|
8
|
+
`cdp-mcp` process and its browser/CDP session can survive MCP client restarts or
|
|
9
|
+
reconnects. It does **not** persist state across service-process restarts.
|
|
10
|
+
|
|
11
|
+
> Security note: the local SSE endpoint has no authentication. MCP tools include
|
|
12
|
+
> in-page JavaScript evaluation and filesystem writes via screenshot paths. Only
|
|
13
|
+
> run a persistent service on trusted single-user machines. Be especially careful
|
|
14
|
+
> with `loginctl enable-linger` on shared hosts because it widens the service's
|
|
15
|
+
> exposure window beyond your interactive login session.
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
- [1. Install the server](#1-install-the-server)
|
|
20
|
+
- [2. Optional: enable lingering](#2-optional-enable-lingering)
|
|
21
|
+
- [3. Create the unit file](#3-create-the-unit-file)
|
|
22
|
+
- [4. Enable and start the service](#4-enable-and-start-the-service)
|
|
23
|
+
- [5. Verify](#5-verify)
|
|
24
|
+
- [6. Configure an MCP client](#6-configure-an-mcp-client)
|
|
25
|
+
- [7. Logs](#7-logs)
|
|
26
|
+
- [8. Stop / uninstall](#8-stop--uninstall)
|
|
27
|
+
- [9. Upgrade](#9-upgrade)
|
|
28
|
+
- [Linux ARM64 / Chromium](#linux-arm64--chromium)
|
|
29
|
+
- [Troubleshooting](#troubleshooting)
|
|
30
|
+
|
|
31
|
+
## 1. Install the server
|
|
32
|
+
|
|
33
|
+
Requires Node.js 20+ and a local Chrome/Chromium browser.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g cdp-mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Verify with `cdp-mcp --help`. The package ships prebuilt `dist/`, so there is no
|
|
40
|
+
build step and no repo checkout needed.
|
|
41
|
+
|
|
42
|
+
If `launch_chrome` cannot find Chrome/Chromium automatically, set `CHROME_PATH`
|
|
43
|
+
when generating the unit file below.
|
|
44
|
+
|
|
45
|
+
## 2. Optional: enable lingering
|
|
46
|
+
|
|
47
|
+
Enable lingering only if you want the user service to start at boot even before
|
|
48
|
+
you log in:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sudo loginctl enable-linger "$USER"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You only need to run this once per machine. Check with:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
loginctl show-user "$USER" --property=Linger
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Skip this step if starting the service during your login session is enough.
|
|
61
|
+
|
|
62
|
+
## 3. Create the unit file
|
|
63
|
+
|
|
64
|
+
Run this from any directory:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# If you use fnm, nvm, or another Node version manager, set these variables to
|
|
68
|
+
# stable paths before running this snippet. Example:
|
|
69
|
+
# NODE_BIN="$HOME/.local/share/fnm/aliases/default/bin/node"
|
|
70
|
+
# CDP_SCRIPT="$HOME/.local/share/fnm/aliases/default/bin/cdp-mcp"
|
|
71
|
+
NODE_BIN="${NODE_BIN:-$(command -v node)}"
|
|
72
|
+
CDP_SCRIPT="${CDP_SCRIPT:-$(command -v cdp-mcp)}"
|
|
73
|
+
CHROME_PATH="${CHROME_PATH:-}"
|
|
74
|
+
|
|
75
|
+
if [ -z "$NODE_BIN" ]; then
|
|
76
|
+
echo "Error: node not found in PATH. Install Node 20+ first." >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
if [ -z "$CDP_SCRIPT" ]; then
|
|
80
|
+
echo "Error: cdp-mcp not found. Run 'npm install -g cdp-mcp' first." >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
NODE_DIR="$(dirname "$NODE_BIN")"
|
|
85
|
+
mkdir -p ~/.config/systemd/user
|
|
86
|
+
cat > ~/.config/systemd/user/cdp-mcp.service << EOF
|
|
87
|
+
[Unit]
|
|
88
|
+
Description=cdp-mcp browser MCP server (SSE on port 9719)
|
|
89
|
+
After=network.target
|
|
90
|
+
|
|
91
|
+
[Service]
|
|
92
|
+
Type=simple
|
|
93
|
+
ExecStart="${NODE_BIN}" "${CDP_SCRIPT}" --port 9719
|
|
94
|
+
Restart=on-failure
|
|
95
|
+
RestartSec=5
|
|
96
|
+
Environment="PATH=${NODE_DIR}:/usr/local/bin:/usr/bin:/bin"
|
|
97
|
+
$(if [ -n "$CHROME_PATH" ]; then printf 'Environment="CHROME_PATH=%s"\n' "$CHROME_PATH"; else printf '# Optional: set CHROME_PATH if launch_chrome cannot find Chrome/Chromium.\n# Environment="CHROME_PATH=/path/to/chrome"\n'; fi)
|
|
98
|
+
|
|
99
|
+
[Install]
|
|
100
|
+
WantedBy=default.target
|
|
101
|
+
EOF
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The unit invokes `node` directly with the `cdp-mcp` script path. That makes the
|
|
105
|
+
`NODE_BIN` override authoritative even when your shell uses a Node version
|
|
106
|
+
manager. The `ExecStart` and `Environment` values are double-quoted so systemd
|
|
107
|
+
treats a path containing spaces as a single token rather than splitting it.
|
|
108
|
+
|
|
109
|
+
## 4. Enable and start the service
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
systemctl --user daemon-reload
|
|
113
|
+
systemctl --user enable --now cdp-mcp.service
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 5. Verify
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
systemctl --user status cdp-mcp.service
|
|
120
|
+
ss -tlnp | grep 9719
|
|
121
|
+
curl -s --max-time 2 http://127.0.0.1:9719/sse | head -1
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The `curl` command should print an SSE `event:` line. The stream stays open by
|
|
125
|
+
design. The server also sends periodic SSE keepalive comments by default; tune
|
|
126
|
+
with `CDP_MCP_SSE_KEEPALIVE_MS` only if your MCP client needs a different idle
|
|
127
|
+
interval.
|
|
128
|
+
|
|
129
|
+
## 6. Configure an MCP client
|
|
130
|
+
|
|
131
|
+
Point an SSE-capable MCP client at:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
http://127.0.0.1:9719/sse
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For example, clients that use JSON MCP server config commonly use:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"mcpServers": {
|
|
142
|
+
"cdp-mcp": {
|
|
143
|
+
"type": "sse",
|
|
144
|
+
"url": "http://127.0.0.1:9719/sse"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
SSE mode is single-client today. Multiple MCP clients connected to the same
|
|
151
|
+
service share one process-global browser/CDP session and can interfere with each
|
|
152
|
+
other. Use one active debugging client per service, or run separate services on
|
|
153
|
+
separate ports.
|
|
154
|
+
|
|
155
|
+
A reconnecting client resumes the prior session. If you want a clean browser
|
|
156
|
+
session after reconnecting, call `close_session` before launching or attaching
|
|
157
|
+
again.
|
|
158
|
+
|
|
159
|
+
## 7. Logs
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
journalctl --user -u cdp-mcp.service -f
|
|
163
|
+
journalctl --user -u cdp-mcp.service -n 100
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 8. Stop / uninstall
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
systemctl --user stop cdp-mcp.service
|
|
170
|
+
systemctl --user disable cdp-mcp.service
|
|
171
|
+
rm ~/.config/systemd/user/cdp-mcp.service
|
|
172
|
+
systemctl --user daemon-reload
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 9. Upgrade
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm install -g cdp-mcp@latest
|
|
179
|
+
systemctl --user restart cdp-mcp.service
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Restart or reconnect your MCP client after a server upgrade so it reloads tool
|
|
183
|
+
schemas.
|
|
184
|
+
|
|
185
|
+
## Linux ARM64 / Chromium
|
|
186
|
+
|
|
187
|
+
Google does not publish official Chrome builds for Linux ARM64. If your distro's
|
|
188
|
+
Chromium package is unreliable for DevTools Protocol launches, use a
|
|
189
|
+
Playwright-cached Chromium binary and set `CHROME_PATH` when generating the
|
|
190
|
+
unit:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Install Playwright's Chromium (one-time):
|
|
194
|
+
npx playwright install chromium
|
|
195
|
+
|
|
196
|
+
# Set CHROME_PATH to the latest revision before running the unit-file script:
|
|
197
|
+
export CHROME_PATH="$HOME/.cache/ms-playwright/chromium-1223/chrome-linux/chrome"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Snap Chromium (`/snap/bin/chromium`) can be unreliable for persistent services
|
|
201
|
+
because snap confinement may interfere with `--remote-debugging-port`, headless
|
|
202
|
+
flags, and process lifecycle management. A Playwright-cached Chromium is often
|
|
203
|
+
more predictable for CDP-based debugging sessions. The generated unit's `PATH`
|
|
204
|
+
does not include `/snap/bin`, so if you do use snap Chromium you must set
|
|
205
|
+
`CHROME_PATH=/snap/bin/chromium` explicitly — `launch_chrome` will not
|
|
206
|
+
auto-detect it under the service environment.
|
|
207
|
+
|
|
208
|
+
Playwright upgrades may relocate the binary. After running
|
|
209
|
+
`npx playwright install chromium`, check the new revision directory name (for
|
|
210
|
+
example, `chromium-1223` to `chromium-1250`), update `CHROME_PATH` in the unit
|
|
211
|
+
file, and run:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
systemctl --user daemon-reload
|
|
215
|
+
systemctl --user restart cdp-mcp.service
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
For Chromium sandbox flags (`--no-sandbox`, AppArmor, snap confinement) and known
|
|
219
|
+
host-OS launch gaps, see [chromium-sandboxing.md](./chromium-sandboxing.md) and
|
|
220
|
+
[known-chromium-gaps.md](./known-chromium-gaps.md).
|
|
221
|
+
|
|
222
|
+
## Troubleshooting
|
|
223
|
+
|
|
224
|
+
| Symptom | Fix |
|
|
225
|
+
|---|---|
|
|
226
|
+
| Service exits immediately | Check `journalctl --user -u cdp-mcp.service -n 100`; usually `cdp-mcp` is not installed, Node is too old, or a version-manager path moved |
|
|
227
|
+
| Port 9719 is already in use | Compare `systemctl --user show -p MainPID --value cdp-mcp.service` with `ss -tlnp \| grep 9719`, then stop the other process or change the port |
|
|
228
|
+
| 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 |
|
|
229
|
+
| `launch_chrome` cannot find Chrome | Set `CHROME_PATH` in the unit file and restart the service; on Linux ARM64, try Playwright-cached Chromium (`~/.cache/ms-playwright/chromium-*/chrome-linux/chrome`) |
|
|
230
|
+
| Service not starting after reboot | Enable lingering with `sudo loginctl enable-linger "$USER"` |
|
|
231
|
+
| Node not found after reboot with fnm/nvm | Version-manager shell paths can be ephemeral. Recreate the unit with stable `NODE_BIN` and `CDP_SCRIPT` paths, or install with a system Node |
|
|
232
|
+
| `Failed to connect to bus` over SSH | Run `export XDG_RUNTIME_DIR=/run/user/$(id -u)` before using `systemctl --user` |
|
|
233
|
+
| `already_session` after reconnecting | The prior browser/CDP session is still alive. Resume it, or call `close_session` before starting fresh |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Chrome DevTools Protocol MCP server — a TypeScript-aware frontend debugger for AI agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Leonard Janke",
|
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"main": "dist/index.js",
|
|
31
31
|
"files": [
|
|
32
|
-
"dist"
|
|
32
|
+
"dist",
|
|
33
|
+
"docs/launchd-service.md",
|
|
34
|
+
"docs/systemd-service.md",
|
|
35
|
+
"docs/chromium-sandboxing.md",
|
|
36
|
+
"docs/known-chromium-gaps.md"
|
|
33
37
|
],
|
|
34
38
|
"scripts": {
|
|
35
39
|
"build": "tsc -p tsconfig.json",
|