From 83e2c191d2e94675b07e6e2290e44edeffd10432 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 21 Apr 2026 00:04:27 -0400 Subject: [PATCH 1/6] feat(examples): reusable chrome-devtools-mcp Nix flake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage a demo of an externalized MCP server wrapper at examples/chrome-devtools-mcp-nix/ — pinned chrome-devtools-mcp@0.21.0 via nix-run, CLI args passed through to let consumers supply --executable-path themselves. Browser provisioning stays out of the wrapper (per Parnas/Lowy volatility boundary). To be extracted to its own repo once the shape settles; kolu's own .mcp.json wiring is left untouched for now. --- examples/chrome-devtools-mcp-nix/README.md | 107 ++++++++++++++++++++ examples/chrome-devtools-mcp-nix/flake.lock | 27 +++++ examples/chrome-devtools-mcp-nix/flake.nix | 27 +++++ 3 files changed, 161 insertions(+) create mode 100644 examples/chrome-devtools-mcp-nix/README.md create mode 100644 examples/chrome-devtools-mcp-nix/flake.lock create mode 100644 examples/chrome-devtools-mcp-nix/flake.nix diff --git a/examples/chrome-devtools-mcp-nix/README.md b/examples/chrome-devtools-mcp-nix/README.md new file mode 100644 index 00000000..428f5948 --- /dev/null +++ b/examples/chrome-devtools-mcp-nix/README.md @@ -0,0 +1,107 @@ +# chrome-devtools-mcp-nix (example) + +Staging-ground example of a **reusable Nix flake** that runs +[`chrome-devtools-mcp`](https://www.npmjs.com/package/chrome-devtools-mcp) +as a stdio MCP server. Consumers point their Claude Code `.mcp.json` at +`nix run`; the flake provides a pinned `npx` invocation with `nodejs` in +the closure. + +This lives inside the kolu repo so the shape can be iterated on before +being extracted to its own repo (e.g. `github:srid/chrome-devtools-mcp-nix`). + +## Design + +One concern per boundary (volatility-based decomposition, Parnas/Lowy): + +| Concern | Owner | +| -------------------------------------------------- | ---------------- | +| Launch `chrome-devtools-mcp` with a pinned version | **this flake** | +| Provide a Chrome binary at `--executable-path` | **the consumer** | + +Chrome provisioning is deliberately _not_ bundled — different consumers +have different answers (Playwright browsers, system Chrome, a container, +a devshell-provided binary). Bundling any one of them would leak a +project-specific choice into a shared interface. The flake forwards all +CLI arguments to `chrome-devtools-mcp`, so the consumer passes +`--executable-path=...` directly. + +`@latest` is also avoided on purpose. The version is pinned in +`flake.nix` (`mcpVersion`) so consumers inherit a tested combination; +bump deliberately rather than drifting on every invocation. + +## Consumer usage + +### `.mcp.json` + +Simplest form — consumer already has Chrome on `$PATH`: + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "nix", + "args": [ + "run", + "github:srid/chrome-devtools-mcp-nix", + "--", + "--headless=true", + "--isolated=true" + ] + } + } +} +``` + +With a custom Chrome binary: + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "nix", + "args": [ + "run", + "github:srid/chrome-devtools-mcp-nix", + "--", + "--headless=true", + "--isolated=true", + "--executable-path=/opt/chrome/chrome" + ] + } + } +} +``` + +### With a resolver (Playwright-style, like kolu) + +When the Chrome path isn't fixed (e.g. Playwright's Chrome-for-Testing, +whose path varies per platform and nixpkgs revision), wrap the `nix run` +in a shell that resolves the binary first. Kolu does this in +`agents/ai.just:83-89`: finds `chrome-linux64/chrome` or +`chrome-mac-*/Google Chrome for Testing.app/...` under +`$PLAYWRIGHT_BROWSERS_PATH`, then execs the MCP with +`--executable-path="$chrome"`. Same pattern, just substitute +`nix run github:srid/chrome-devtools-mcp-nix --` for the `npx` call. + +## Running locally from this checkout + +```sh +nix run ./examples/chrome-devtools-mcp-nix -- --help +``` + +## Bumping the pinned version + +Edit `mcpVersion` in `flake.nix`. CI should run smoke tests against the +new version before merging. + +## When this moves to its own repo + +Once extracted, the consumer's `.mcp.json` stanza becomes the final +form shown above. Nothing changes about the interface — which is the +point of putting the boundary here. + +APM ([microsoft/apm#655](https://github.com/microsoft/apm/pull/655)) +will eventually let an APM package declare the MCP stanza and have +`apm install` render it into the consumer's `.mcp.json`. That solves +config distribution; this flake solves binary provisioning. Both layers +are needed and orthogonal. diff --git a/examples/chrome-devtools-mcp-nix/flake.lock b/examples/chrome-devtools-mcp-nix/flake.lock new file mode 100644 index 00000000..dfdfdf98 --- /dev/null +++ b/examples/chrome-devtools-mcp-nix/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776548001, + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/chrome-devtools-mcp-nix/flake.nix b/examples/chrome-devtools-mcp-nix/flake.nix new file mode 100644 index 00000000..cbfd0212 --- /dev/null +++ b/examples/chrome-devtools-mcp-nix/flake.nix @@ -0,0 +1,27 @@ +{ + description = "chrome-devtools-mcp packaged for cross-project reuse in Claude Code .mcp.json"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + mcpVersion = "0.21.0"; + + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + eachSystem = f: nixpkgs.lib.genAttrs systems (system: + f nixpkgs.legacyPackages.${system}); + + mkMcp = pkgs: pkgs.writeShellScriptBin "chrome-devtools-mcp" '' + exec ${pkgs.nodejs}/bin/npx -y chrome-devtools-mcp@${mcpVersion} "$@" + ''; + in + { + packages = eachSystem (pkgs: { default = mkMcp pkgs; }); + apps = eachSystem (pkgs: { + default = { + type = "app"; + program = "${mkMcp pkgs}/bin/chrome-devtools-mcp"; + }; + }); + }; +} From 7b752b7c0715dba1f5dc648312c8f9566e81a79f Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 21 Apr 2026 00:06:41 -0400 Subject: [PATCH 2/6] refactor(hickey): bind mkMcp once per system in example flake packages.default and apps.default each called mkMcp pkgs independently, complecting derivation identity with call count. Bind the result once via a perSystem let so both outputs share one derivation by construction. --- examples/chrome-devtools-mcp-nix/flake.nix | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/chrome-devtools-mcp-nix/flake.nix b/examples/chrome-devtools-mcp-nix/flake.nix index cbfd0212..38d67e2c 100644 --- a/examples/chrome-devtools-mcp-nix/flake.nix +++ b/examples/chrome-devtools-mcp-nix/flake.nix @@ -14,14 +14,20 @@ mkMcp = pkgs: pkgs.writeShellScriptBin "chrome-devtools-mcp" '' exec ${pkgs.nodejs}/bin/npx -y chrome-devtools-mcp@${mcpVersion} "$@" ''; + + perSystem = pkgs: rec { + mcp = mkMcp pkgs; + package = { default = mcp; }; + app = { + default = { + type = "app"; + program = "${mcp}/bin/chrome-devtools-mcp"; + }; + }; + }; in { - packages = eachSystem (pkgs: { default = mkMcp pkgs; }); - apps = eachSystem (pkgs: { - default = { - type = "app"; - program = "${mkMcp pkgs}/bin/chrome-devtools-mcp"; - }; - }); + packages = eachSystem (pkgs: (perSystem pkgs).package); + apps = eachSystem (pkgs: (perSystem pkgs).app); }; } From 6ff409beb80cedef26c495e4df4aa17bdf0772bc Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 21 Apr 2026 00:07:03 -0400 Subject: [PATCH 3/6] docs(examples): note deferred axes for chrome-devtools-mcp extraction Capture two items surfaced by the hickey/lowy review that belong in the extracted repo, not this staging demo: a CI smoke-test on the pinned mcpVersion, and an offline/hermetic npm-fetch strategy as a third volatility axis (separable from version pinning and binary provisioning). --- examples/chrome-devtools-mcp-nix/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/chrome-devtools-mcp-nix/README.md b/examples/chrome-devtools-mcp-nix/README.md index 428f5948..6ed456ea 100644 --- a/examples/chrome-devtools-mcp-nix/README.md +++ b/examples/chrome-devtools-mcp-nix/README.md @@ -100,6 +100,17 @@ Once extracted, the consumer's `.mcp.json` stanza becomes the final form shown above. Nothing changes about the interface — which is the point of putting the boundary here. +Deferred items to wire up in the extracted repo: + +- **CI smoke-test** on the pinned version — `nix run . -- --help` (or a + non-interactive equivalent) so a bad `mcpVersion` bump fails CI + instead of a consumer's `nix run`. +- **Offline / hermetic fetch strategy** — the wrapper currently relies + on `npx -y` hitting npm at runtime. For air-gapped or strict-hermetic + Nix consumers, a `buildNpmPackage`/`fetchurl`-based offline bundle + would be a third volatility axis (npm-fetch strategy) separable from + version pinning and binary provisioning. + APM ([microsoft/apm#655](https://github.com/microsoft/apm/pull/655)) will eventually let an APM package declare the MCP stanza and have `apm install` render it into the consumer's `.mcp.json`. That solves From 867465794464d7a8c653825662247fef33f3f572 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 21 Apr 2026 00:09:56 -0400 Subject: [PATCH 4/6] =?UTF-8?q?refactor(police):=20elegance=20=E2=80=94=20?= =?UTF-8?q?invert=20perSystem=20so=20mkMcp=20runs=20once=20per=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior perSystem was a function called twice (once for packages, once for apps), re-invoking mkMcp each time and regressing the hickey fix. Move perSystem inside eachSystem so each system's derivation is built once, then project packages/apps via mapAttrs. Also drops the stray rec, hides mcp as a let-local, and collapses the package/app wrapper attrs. Surfaced by the elegance pass. --- examples/chrome-devtools-mcp-nix/flake.nix | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/chrome-devtools-mcp-nix/flake.nix b/examples/chrome-devtools-mcp-nix/flake.nix index 38d67e2c..639c5d8b 100644 --- a/examples/chrome-devtools-mcp-nix/flake.nix +++ b/examples/chrome-devtools-mcp-nix/flake.nix @@ -15,19 +15,17 @@ exec ${pkgs.nodejs}/bin/npx -y chrome-devtools-mcp@${mcpVersion} "$@" ''; - perSystem = pkgs: rec { - mcp = mkMcp pkgs; - package = { default = mcp; }; - app = { - default = { + perSystem = eachSystem (pkgs: + let mcp = mkMcp pkgs; in { + packages.default = mcp; + apps.default = { type = "app"; program = "${mcp}/bin/chrome-devtools-mcp"; }; - }; - }; + }); in { - packages = eachSystem (pkgs: (perSystem pkgs).package); - apps = eachSystem (pkgs: (perSystem pkgs).app); + packages = nixpkgs.lib.mapAttrs (_: s: s.packages) perSystem; + apps = nixpkgs.lib.mapAttrs (_: s: s.apps) perSystem; }; } From cd094ae3a10fed067f8e4c1151a1d43aabfa2327 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 21 Apr 2026 00:10:14 -0400 Subject: [PATCH 5/6] =?UTF-8?q?refactor(police):=20elegance=20=E2=80=94=20?= =?UTF-8?q?use=20lib.systems.flakeExposed=20for=20systems?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the hand-maintained 4-platform array in favor of nixpkgs' canonical list. Picks up tier-2 targets (riscv64-linux, armv7l-linux, etc.) at no cost — genAttrs is lazy and only the active system is forced by nix run. --- examples/chrome-devtools-mcp-nix/flake.nix | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/chrome-devtools-mcp-nix/flake.nix b/examples/chrome-devtools-mcp-nix/flake.nix index 639c5d8b..c7d996ed 100644 --- a/examples/chrome-devtools-mcp-nix/flake.nix +++ b/examples/chrome-devtools-mcp-nix/flake.nix @@ -7,9 +7,8 @@ let mcpVersion = "0.21.0"; - systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - eachSystem = f: nixpkgs.lib.genAttrs systems (system: - f nixpkgs.legacyPackages.${system}); + eachSystem = f: nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed + (system: f nixpkgs.legacyPackages.${system}); mkMcp = pkgs: pkgs.writeShellScriptBin "chrome-devtools-mcp" '' exec ${pkgs.nodejs}/bin/npx -y chrome-devtools-mcp@${mcpVersion} "$@" From fa26c9fcab44a67597b5e40808f7ab70797a378a Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 21 Apr 2026 00:10:26 -0400 Subject: [PATCH 6/6] =?UTF-8?q?refactor(police):=20quality=20=E2=80=94=20d?= =?UTF-8?q?rop=20unused=20self=20from=20outputs=20destructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit self was destructured but never referenced. Using { nixpkgs, ... } keeps the signature honest. --- examples/chrome-devtools-mcp-nix/flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/chrome-devtools-mcp-nix/flake.nix b/examples/chrome-devtools-mcp-nix/flake.nix index c7d996ed..c574a523 100644 --- a/examples/chrome-devtools-mcp-nix/flake.nix +++ b/examples/chrome-devtools-mcp-nix/flake.nix @@ -3,7 +3,7 @@ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - outputs = { self, nixpkgs }: + outputs = { nixpkgs, ... }: let mcpVersion = "0.21.0";