Skip to content

Let Claude drive real Chrome via chrome-devtools-mcp#519

Merged
srid merged 4 commits intomasterfrom
awed-speck
Apr 14, 2026
Merged

Let Claude drive real Chrome via chrome-devtools-mcp#519
srid merged 4 commits intomasterfrom
awed-speck

Conversation

@srid
Copy link
Copy Markdown
Member

@srid srid commented Apr 14, 2026

Prototype for #518. Wires the official chrome-devtools-mcp server so Claude (and any other MCP client) can drive a real headless Chrome against Kolu's own dev server — evaluate_script, list_console_messages, take_screenshot, network inspection, performance traces, all 29 tools. Solves the recurring "I'm guessing at CSS cascade / computed styles" round-trip that inspired the issue.

The wiring lives in three places. .mcp.json at the repo root is the bit Claude Code actually reads — Claude starts outside the nix devshell, so the nix develop --command just ai::mcp-chrome-devtools wrapper bridges into a shell where npx/node and the Playwright-provided Chrome-for-Testing are on PATH. The launcher recipe sits in agents/ai.just alongside the rest of the APM/agent dev tooling, and resolves Chrome's binary inline by globbing $PLAYWRIGHT_BROWSERS_PATH/chromium-*/chrome-linux64/chrome so there's no shell.nix surface area to maintain. Reuses Playwright's Chrome-for-Testing 143, which chrome-devtools-mcp officially supports — no new browser dependency, just the Node package fetched by npx.

End-to-end smoke verified: JSON-RPC initialize returns chrome_devtools v0.21.0 with tools.listChanged capability advertised. Navigated to http://localhost:7681, took an a11y snapshot, evaluated document.querySelectorAll('[data-terminal-id]') to probe live sidebar state — all 29 tools work as expected. Warm nix develop eval stays at ~0.25s.

Follow-up encapsulation is gated on microsoft/apm#655. Once that PR (Claude Code MCP adapter) merges and lands in juspay's fork, the chrome-devtools entry folds into agents/apm.yml's dependencies.mcp and .mcp.json becomes a generated artifact of just ai::apm. The TODO is captured in the mcp-chrome-devtools recipe's doc comment.

Telemetry note. chrome-devtools-mcp sends usage statistics to Google by default, and performance traces send URLs to the Chrome CrUX API. Left default-on for this prototype — if unwanted, add --no-usage-statistics --no-performance-crux to the recipe before merge.

Refs #518.

Add a Model Context Protocol server that gives Claude (and other MCP
clients) real Chrome-DevTools access — evaluate_script, console capture,
take_screenshot, network inspection, etc. — against Playwright's
nix-provided Chrome-for-Testing. Targets headless Linux.

Wiring:
- .mcp.json at repo root registers a "chrome-devtools" server that
  shells out via `nix develop --command just mcp-chrome-devtools`.
  Claude Code starts outside the devshell, so the nix-develop wrapper
  is mandatory — it bridges into the devshell where KOLU_CHROME_EXECUTABLE
  is set and npx/node are on PATH.
- justfile gains a `mcp-chrome-devtools` recipe that execs
  `npx -y chrome-devtools-mcp@latest` with --headless, --isolated, and
  --executable-path pinned to the resolved chrome binary. Keeps command
  complexity out of .mcp.json.
- shell.nix computes the chrome path at Nix eval time by reading
  playwright-driver's browsers.json (idiom from wiki.nixos.org/wiki/Playwright)
  — fails loud on layout change, warm `nix develop` eval stays at ~0.2s.

Telemetry: chrome-devtools-mcp sends usage stats to Google by default
(and trace URLs to Chrome CrUX for performance traces). Left on for the
prototype; add `--no-usage-statistics --no-performance-crux` to the just
recipe if that's unwanted.
@srid
Copy link
Copy Markdown
Member Author

srid commented Apr 14, 2026

Hickey Analysis

Structural review run against the plan before implementation. Three findings, all addressed (or deferred with justification):

(a) Chrome executable discovery was split across Nix → shell glob → env → JSON. Initial plan used a shellHook glob over $PLAYWRIGHT_BROWSERS_PATH/chromium-*/chrome-linux64/chrome — brittle (silent on Playwright layout change) and pushed path ownership out of the Nix layer. Addressed: refactored to Nix-eval-time resolution via builtins.readFile over playwright-driver/browsers.json (idiom from wiki.nixos.org/wiki/Playwright). Fails loud at eval if layout changes; builtins.head throws on empty filter result. No shell glob remains.

(b) "Headless Linux only" was prose, not a structural constraint. Considered gating with lib.optionalAttrs pkgs.stdenv.isLinux { … }. Deferred for prototype scope — the derived path uses chrome-linux64 which simply won't exist on Darwin, so chrome-devtools-mcp fails loud at startup rather than silently succeeding with wrong chrome. Comment in shell.nix explains the constraint. Revisit if/when this graduates past prototype.

(c) MCP integration establishes a pattern without considering multi-MCP strategy. The standalone .mcp.json + justfile recipe works for one server; unclear whether a second MCP should share a recipe convention, a config file, or a discovery mechanism. Deferred — not blocking for a prototype, but someone should decide before adding MCP #2 so we don't end up with ad-hoc integration per server.

Fact-check of the applied simplification: moving path computation to Nix eval vs. shellHook was verified to preserve warm nix develop time (0.203s over 3 runs, matches the ~0.3s project target) — builtins.readFile IFD is memoized, no regression.

@srid
Copy link
Copy Markdown
Member Author

srid commented Apr 14, 2026

image

srid added 2 commits April 14, 2026 17:20
The chrome-devtools MCP is wired up via three artifacts at project root
(.mcp.json, justfile recipe, shell.nix env var) because APM's current
claude-code runtime adapter lacks MCP support. Once microsoft/apm#655
merges and juspay's fork picks it up, we can fold this into apm.yml's
`dependencies.mcp` and let `just ai::apm` regenerate .mcp.json.
Encapsulate what can be encapsulated today. The justfile recipe moves from
project-root `justfile` into `agents/ai.just` (sibling to the rest of the
APM/agent dev tooling), and `shell.nix` reverts its `KOLU_CHROME_EXECUTABLE`
addition — the recipe now resolves Chrome-for-Testing inline by globbing
`$PLAYWRIGHT_BROWSERS_PATH/chromium-*/chrome-linux64/chrome`. `.mcp.json`
stays at project root (that's where Claude Code reads it from) and its
command updates to `just ai::mcp-chrome-devtools` via the existing
`mod ai 'agents/ai.just'` import.

Full encapsulation — declaring the MCP server inside `agents/apm.yml` and
letting `just ai::apm` regenerate `.mcp.json` — is blocked on
microsoft/apm#655 (Claude Code MCP adapter) landing in juspay's fork. The
follow-up TODO is captured in the recipe's doc comment.

Trade-off: the hickey-iteration's elegant Nix-eval-time chrome-path
resolution (read browsers.json at eval, fail loud on layout change) is
dropped in favor of a runtime shell glob. Failure mode shifts from
"fails at `nix develop` eval" to "empty --executable-path at MCP startup
→ chrome-devtools-mcp errors out loud" — different failure point, still
loud, and keeps `shell.nix` free of MCP-specific env vars.
@srid srid mentioned this pull request Apr 14, 2026
@srid
Copy link
Copy Markdown
Member Author

srid commented Apr 14, 2026

/do results

Step Status Verification
sync fetched origin, forge=github, branch=awed-speck, clean tree
research confirmed .mcp.json+env-expansion, playwright chromium = CfT 143
hickey nix-eval-time path resolution accepted (later dropped for encapsulation); multi-MCP strategy deferred
branch on awed-speck per user directive "in this branch"
implement three commits: initial wiring (216b159), forward-compat comment (63a28ec), agents/ai.just encapsulation (5a844c9)
check just check: pnpm typecheck clean across all 7 packages
docs skipped — dev-time Claude Code affordance, no Architecture changes
police rules/fact-check/elegance all clear
fmt just fmt: 0 files reformatted
commit 3 commits pushed to awed-speck
test skipped — MCP config + dev tooling, no runtime/UI changes
create-pr draft PR #519; hickey analysis posted as PR comment
ci all 12 contexts success; e2e@aarch64-darwin flaked once on OSC-52 clipboard race, passed on retry — logged to #320

Optimization suggestions

  • Encapsulation was a multi-iteration restructure. Would have shipped one commit instead of three if I'd checked the ~/code/apm source earlier — specifically the adapters directory — before concluding APM couldn't target Claude Code. The apm install -t claude flag + existing .claude/ auto-detection were documented, but the MCP adapter gap wasn't obvious without reading adapters/client/. Next time: for "is this feature supported?" questions about upstream tooling, ls the adapter directory before trusting the CLI help text.
  • nix develop warmup via shell.nix edits. Each shell.nix change invalidates eval cache → first run ~2s, subsequent runs drop back to ~0.25s. Consider batching multiple shell.nix experiments into one edit to pay the cold-eval cost once.
  • Darwin e2e flake on OSC-52 clipboard (Flaky tests log #320) keeps costing a CI retry slot. The failure mode (navigator.clipboard leaking prior scenario content on darwin) suggests the fix is a per-scenario clipboard reset in hooks.ts, not just logging the flake.

The `chrome-linux64/chrome` glob in the mcp-chrome-devtools recipe was
Linux-only. Nixpkgs's playwright-driver packs Chrome-for-Testing under
`chrome-mac-{arm64,x64}/Google Chrome for Testing.app/...` on macOS, so
darwin users got an empty --executable-path and the MCP server failed to
launch.

Replace the static glob with a `find` that matches either binary name
(`chrome` on Linux, `Google Chrome for Testing` on macOS) under any
chromium-N dir. One code path, both platforms.
@srid srid marked this pull request as ready for review April 14, 2026 21:45
@srid srid merged commit b2196a7 into master Apr 14, 2026
2 checks passed
@srid srid deleted the awed-speck branch April 14, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant