From 216b1594ee73b4ff88f048c49098157ffc836938 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 14 Apr 2026 16:53:57 -0400 Subject: [PATCH 1/4] feat(mcp): wire chrome-devtools-mcp for Claude Code via nix+just MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .mcp.json | 8 ++++++++ justfile | 7 +++++++ shell.nix | 12 ++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..31fadc4d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "chrome-devtools": { + "command": "nix", + "args": ["develop", "--command", "just", "mcp-chrome-devtools"] + } + } +} diff --git a/justfile b/justfile index 80d4cda9..328baa26 100644 --- a/justfile +++ b/justfile @@ -95,6 +95,13 @@ fmt: fmt-check: {{ nix_shell }} sh -c 'prettier --check --cache --ignore-unknown . && nixpkgs-fmt --check *.nix nix/**/*.nix' +# Launch chrome-devtools-mcp server over stdio — wired up via .mcp.json. +# Drives Playwright's nix-provided Chrome-for-Testing (headless, isolated +# profile). Claude Code starts outside the devshell, so .mcp.json wraps +# this with `nix develop --command just mcp-chrome-devtools`. +mcp-chrome-devtools: + {{ nix_shell }} sh -c 'exec npx -y chrome-devtools-mcp@latest --headless=true --isolated=true --executable-path="$KOLU_CHROME_EXECUTABLE"' + # Nix build (server + client) build: nix build diff --git a/shell.nix b/shell.nix index 0f373661..21114aec 100644 --- a/shell.nix +++ b/shell.nix @@ -2,6 +2,17 @@ { pkgs ? import ./nix/nixpkgs.nix { } }: let packages = import ./default.nix { inherit pkgs; }; + + # Chrome-for-Testing path for chrome-devtools-mcp (see .mcp.json). Pulled + # from playwright-driver's browsers.json at eval time so the chromium-N + # revision tracks whatever nixpkgs pins — fails loud here if layout changes + # instead of silently at MCP startup. Idiom from https://wiki.nixos.org/wiki/Playwright. + playwrightBrowsers = (builtins.fromJSON + (builtins.readFile "${pkgs.playwright-driver}/browsers.json")).browsers; + chromiumRev = (builtins.head + (builtins.filter (b: b.name == "chromium") playwrightBrowsers)).revision; + koluChromeExecutable = + "${pkgs.playwright-driver.browsers}/chromium-${chromiumRev}/chrome-linux64/chrome"; in pkgs.mkShell { name = "kolu-shell"; @@ -11,6 +22,7 @@ pkgs.mkShell { KOLU_COMMIT_HASH = "dev"; PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; + KOLU_CHROME_EXECUTABLE = koluChromeExecutable; }; shellHook = '' From 63a28ecacdbc0b86137bacbb39157738319bc85c Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 14 Apr 2026 17:20:13 -0400 Subject: [PATCH 2/4] chore(mcp): link to microsoft/apm#655 for future encapsulation 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. --- justfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/justfile b/justfile index 328baa26..a23eb975 100644 --- a/justfile +++ b/justfile @@ -99,6 +99,11 @@ fmt-check: # Drives Playwright's nix-provided Chrome-for-Testing (headless, isolated # profile). Claude Code starts outside the devshell, so .mcp.json wraps # this with `nix develop --command just mcp-chrome-devtools`. +# +# TODO: fold this + .mcp.json into agents/apm.yml's `dependencies.mcp` once +# microsoft/apm#655 (Claude Code MCP adapter) merges and lands in juspay's +# fork — then the recipe moves into agents/ai.just and .mcp.json becomes a +# generated artifact. mcp-chrome-devtools: {{ nix_shell }} sh -c 'exec npx -y chrome-devtools-mcp@latest --headless=true --isolated=true --executable-path="$KOLU_CHROME_EXECUTABLE"' From 5a844c9981dcf19fc91b7e89f1ce21cf9ed11ade Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 14 Apr 2026 17:22:55 -0400 Subject: [PATCH 3/4] refactor(mcp): move chrome-devtools recipe into agents/ai.just MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .mcp.json | 2 +- agents/ai.just | 13 +++++++++++++ justfile | 12 ------------ shell.nix | 12 ------------ 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/.mcp.json b/.mcp.json index 31fadc4d..93f5ef82 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "chrome-devtools": { "command": "nix", - "args": ["develop", "--command", "just", "mcp-chrome-devtools"] + "args": ["develop", "--command", "just", "ai::mcp-chrome-devtools"] } } } diff --git a/agents/ai.just b/agents/ai.just index c4e8e1fb..14602b62 100644 --- a/agents/ai.just +++ b/agents/ai.just @@ -61,3 +61,16 @@ apm-sync: apm-audit agent *args: apm just prepare {{ env('AI_AGENT', 'claude --dangerously-skip-permissions') }} {{ args }} + +# Launch chrome-devtools-mcp server over stdio — wired up via the root +# `.mcp.json`, which shells out via `nix develop --command just +# ai::mcp-chrome-devtools` (Claude Code starts outside the nix devshell). +# Chrome binary resolved from Playwright's nix-provided Chrome-for-Testing; +# glob misses fail loud downstream (empty --executable-path → MCP server +# errors out visibly). Headless Linux only. +# +# TODO: fold .mcp.json's entry into `dependencies.mcp` above once +# microsoft/apm#655 (Claude Code MCP adapter) merges and lands in juspay's +# fork — `.mcp.json` then becomes a generated artifact of `just ai::apm`. +mcp-chrome-devtools: + sh -c 'chrome="$(ls -d "$PLAYWRIGHT_BROWSERS_PATH"/chromium-*/chrome-linux64/chrome 2>/dev/null | head -1)"; exec npx -y chrome-devtools-mcp@latest --headless=true --isolated=true --executable-path="$chrome"' diff --git a/justfile b/justfile index a23eb975..80d4cda9 100644 --- a/justfile +++ b/justfile @@ -95,18 +95,6 @@ fmt: fmt-check: {{ nix_shell }} sh -c 'prettier --check --cache --ignore-unknown . && nixpkgs-fmt --check *.nix nix/**/*.nix' -# Launch chrome-devtools-mcp server over stdio — wired up via .mcp.json. -# Drives Playwright's nix-provided Chrome-for-Testing (headless, isolated -# profile). Claude Code starts outside the devshell, so .mcp.json wraps -# this with `nix develop --command just mcp-chrome-devtools`. -# -# TODO: fold this + .mcp.json into agents/apm.yml's `dependencies.mcp` once -# microsoft/apm#655 (Claude Code MCP adapter) merges and lands in juspay's -# fork — then the recipe moves into agents/ai.just and .mcp.json becomes a -# generated artifact. -mcp-chrome-devtools: - {{ nix_shell }} sh -c 'exec npx -y chrome-devtools-mcp@latest --headless=true --isolated=true --executable-path="$KOLU_CHROME_EXECUTABLE"' - # Nix build (server + client) build: nix build diff --git a/shell.nix b/shell.nix index 21114aec..0f373661 100644 --- a/shell.nix +++ b/shell.nix @@ -2,17 +2,6 @@ { pkgs ? import ./nix/nixpkgs.nix { } }: let packages = import ./default.nix { inherit pkgs; }; - - # Chrome-for-Testing path for chrome-devtools-mcp (see .mcp.json). Pulled - # from playwright-driver's browsers.json at eval time so the chromium-N - # revision tracks whatever nixpkgs pins — fails loud here if layout changes - # instead of silently at MCP startup. Idiom from https://wiki.nixos.org/wiki/Playwright. - playwrightBrowsers = (builtins.fromJSON - (builtins.readFile "${pkgs.playwright-driver}/browsers.json")).browsers; - chromiumRev = (builtins.head - (builtins.filter (b: b.name == "chromium") playwrightBrowsers)).revision; - koluChromeExecutable = - "${pkgs.playwright-driver.browsers}/chromium-${chromiumRev}/chrome-linux64/chrome"; in pkgs.mkShell { name = "kolu-shell"; @@ -22,7 +11,6 @@ pkgs.mkShell { KOLU_COMMIT_HASH = "dev"; PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; - KOLU_CHROME_EXECUTABLE = koluChromeExecutable; }; shellHook = '' From e8c4c920fc1277526aa62305289beb44de22064a Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 14 Apr 2026 17:39:51 -0400 Subject: [PATCH 4/4] fix(mcp): resolve Chrome across both Linux and macOS layouts 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. --- agents/ai.just | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agents/ai.just b/agents/ai.just index 14602b62..6ead047b 100644 --- a/agents/ai.just +++ b/agents/ai.just @@ -65,12 +65,14 @@ agent *args: apm # Launch chrome-devtools-mcp server over stdio — wired up via the root # `.mcp.json`, which shells out via `nix develop --command just # ai::mcp-chrome-devtools` (Claude Code starts outside the nix devshell). -# Chrome binary resolved from Playwright's nix-provided Chrome-for-Testing; -# glob misses fail loud downstream (empty --executable-path → MCP server -# errors out visibly). Headless Linux only. +# Chrome binary resolved from Playwright's nix-provided Chrome-for-Testing +# across platforms: `chrome-linux64/chrome` on Linux and +# `chrome-mac-*/Google Chrome for Testing.app/.../Google Chrome for Testing` +# on macOS. `find` picks either layout; a miss fails loud downstream +# (empty --executable-path → MCP server errors out visibly). # # TODO: fold .mcp.json's entry into `dependencies.mcp` above once # microsoft/apm#655 (Claude Code MCP adapter) merges and lands in juspay's # fork — `.mcp.json` then becomes a generated artifact of `just ai::apm`. mcp-chrome-devtools: - sh -c 'chrome="$(ls -d "$PLAYWRIGHT_BROWSERS_PATH"/chromium-*/chrome-linux64/chrome 2>/dev/null | head -1)"; exec npx -y chrome-devtools-mcp@latest --headless=true --isolated=true --executable-path="$chrome"' + sh -c 'chrome="$(find "$PLAYWRIGHT_BROWSERS_PATH"/chromium-* \( -name chrome -o -name "Google Chrome for Testing" \) -type f -perm -u+x 2>/dev/null | head -1)"; exec npx -y chrome-devtools-mcp@latest --headless=true --isolated=true --executable-path="$chrome"'