Staging-ground chrome-devtools-mcp Nix flake for cross-project reuse#650
Staging-ground chrome-devtools-mcp Nix flake for cross-project reuse#650
Conversation
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.
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.
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).
…system 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.
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.
self was destructured but never referenced. Using { nixpkgs, ... } keeps
the signature honest.
Hickey/Lowy Analysis
Hickey rationaleThe flake's concern separation is structurally sound — version pinning in one place, binary provisioning deferred, shell wrapper kept thin. The original Lowy rationaleThe implementation honors its design. MCP version volatility is owned by the flake ( |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 0s | git fetch ok; forge=github; noGit=false |
| research | ✓ | 1m 41s | Surveyed kolu Nix conventions + APM #655; planned externalization at examples/chrome-devtools-mcp-nix/ |
| branch | ✓ | 17s | Worktree-scoped livid-phase, fresh from master |
| implement | ✓ | 1m 19s | flake.nix + README + flake.lock; smoke-built via nix flake check |
| check | ✓ | 12s | just check passed |
| docs | ✓ | 10s | Example is self-documented |
| fmt | ✓ | 8s | 0/6 files needed reformatting |
| commit | ✓ | 13s | 83e2c19 pushed |
| hickey+lowy | ✓ | 2m 33s | 1 fixed in PR, 2 deferred to extracted repo, 1 no-op |
| police | ✓ | 3m 35s | Elegance pass caught a real regression (double-eval of mkMcp); 3 commits applied |
| test | — | 15s | Skipped: no relevant e2e scenarios for a standalone flake |
| create-pr | ✓ | 1m 16s | Draft → ready; hickey/lowy posted as comment |
| ci | ✓ | 2m 36s | All 10 contexts green on fa26c9f |
| Total | 14m 59s |
Slowest step: police (3m 35s)
Optimization suggestions
police(3m 35s) was the bottleneck, driven by/simplify's three parallel agents catching a real issue I thought hickey+lowy had already fixed (the invertedperSystem/mapAttrsrefactor). Running/simplifyearlier — e.g. alongsidehickey+lowyrather than only inpolice— would have surfaced the double-eval in one combined pass instead of discovering it post-commit and needing three follow-up commits.hickey+lowy(2m 33s) could have been shorter by passing the smaller review model. For a 30-line declarative flake,--review-model=haikuwould likely have returned the same findings at lower latency.- CI (2m 36s) is dominated by cross-system builds; no room to trim for a flake-only PR short of splitting
just ciinto flake-scope-aware subsets — not worth it for a one-off.
Workflow completed at 2026-04-21T15:17:00Z.
…f-hosted fonts, MCP shell (#636) ## Summary A top-to-bottom visual refresh of the default Emanote theme, arrived at through iterative feedback against the live dev server. The goal was to land somewhere *colourful and fun but not jarring* — evocative of [neuron.zettel.page](https://neuron.zettel.page/)'s character, suitable for Zettelkasten notes as much as tech docs or personal pages — without making the theme feel like a template everyone else already uses. ### What changed **Typography.** Swapped the old Source Serif stack for **Lora** (prose), **Space Grotesk** (UI chrome + headings), and **Space Mono** (code). All three are self-hosted under `_emanote-static/fonts/` as woff2 + a generated `fonts.css` with relative URLs, so generated static sites no longer phone home to `fonts.gstatic.com`. **Theme toggle.** A persistent dark/light mode toggle lives in the sidebar (and, critically, in the no-sidebar "neuron-style" layout too, via the top-right button row). State is kept in `localStorage`; an early-load script in `base.tpl` applies the `.dark` class before first paint to avoid a flash of wrong theme. Everything previously keyed off `prefers-color-scheme` — `skylighting.css`, the stork search dialog — was rewired to the manual toggle via `.dark` class selectors (and a tiny `MutationObserver` in the stork wrapper for live swap). **Note title** is back to the Emanote brand treatment: `bg-primary-600`, white, centered, `rounded-2xl`. That look *is* the Emanote brand; an earlier iteration stripped it and it immediately felt generic. **Backlinks.** Re-done as a 2-column card grid with a thin primary left-accent (`border-l-2`) and subtle shadow, no outer panel bg/border. The "Links to this page" block now picks up the sans font via `#backlinks` being added to the UI-chrome selector. **Links.** Wikilinks get a subtle primary-50/primary-950 **hover background** (no underline); external links switch to hover-only underline. The wikilink background uses `-mx-1 px-1 rounded` so text doesn't reflow on hover. **TOC.** Replaced the old `window.onscroll` scroll-spy with an `IntersectionObserver` (#520's long-standing TODO). On top of that: depth-based visual hierarchy — each nested `<ul>` level in the "On this page" list steps down font-size and colour, so a 30-heading page stays scannable at a glance. Active-item styling now uses `var(--color-primary-*)` so per-site theme overrides flow through cleanly. **Sidebar.** Removed the tag-index and expand-tree shortcut icons — search and theme toggle only. Folgezettel tree depth rails (`#sidebar .pl-2 .pl-2 { border-left: ... }`) keep the nested structure legible. **Smaller polishes.** Callouts use `color-mix(in srgb, ${color} 8%, transparent)` for tint backgrounds (no more naive `${color}10`). External-link glyphs are drawn with `mask-image` + `currentColor` so they inherit link colour in both themes. `kbd` restyled with theme variables. Task-list items use `:has(> svg.--ema-checkbox)` to swap the disc bullet for a flex-aligned checkbox. Footnotes re-done as a semantic `<aside> + <ol>` instead of the old `scale-x-90` squish. Nested-list vertical rhythm fixed. Mermaid diagrams re-render on theme toggle. **Infra/MCP.** Added `{{nix_shell}}` prefixing in `justfile` so `just run` works outside a Nix shell, plus a thin `just mcp-chrome-devtools` recipe invoked from `.mcp.json`. A new `nix/chrome-devtools/shell.nix` bundles Chromium + pins `chrome-devtools-mcp@0.21.0` (adapted from [juspay/kolu#650](juspay/kolu#650)) so the chrome-devtools MCP server has a headless browser to drive. **Docs.** `docs/guide/html-template/fonts.md` updated to reflect the new default stack. ### Notable tradeoffs - Self-hosted fonts add ~612 KB to the static layer (29 woff2 blobs across Lora + Space Grotesk + Space Mono subsets). The offline-capable generated site is worth the bytes; browsers cache the blobs per-origin anyway. - Dropping the tag/expand-tree shortcuts from the sidebar is a deliberate simplification — the pages are still reachable via breadcrumbs and the full URL. ## Test plan - [ ] Verify both layouts (with-sidebar and neuron-style no-sidebar) render correctly in light + dark mode. - [ ] Exercise the theme toggle from the sidebar and from the neuron-layout top-right button; confirm persistence across reloads. - [ ] Open the stork search dialog in both themes; confirm input, results list, and highlight contrast are correct. - [ ] View a code block in both themes; confirm syntax highlighting follows the manual toggle (not just OS preference). - [ ] Browse a long-outlined page (e.g. `/yaml-config`) and confirm the TOC shows depth-based hierarchy and the active-item tracker follows scroll. - [ ] Confirm `cabal build all` and `cabal test all` succeed. - [ ] Confirm generated static site produces no `fonts.googleapis.com` network requests.
A reusable Nix flake wrapping chrome-devtools-mcp, staged inside the
Kolu repo at
examples/chrome-devtools-mcp-nix/so the shape can beiterated on before it's extracted to its own repo (e.g.
github:srid/chrome-devtools-mcp-nix). Consumers point their ClaudeCode
.mcp.jsonatnix run ...; the flake provides a pinnednpxinvocation with
nodejsin the closure.The volatility boundary is the interesting part. The wrapper owns one
concern — launching
chrome-devtools-mcpat a pinned version — andnothing else. Chrome binary provisioning stays with the consumer
(
--executable-pathpasses through via"$@"), because differentprojects have different answers: Playwright's Chrome-for-Testing (like
Kolu does today), system Chrome, a container, a devshell-provided
binary. Bundling any one answer into a shared wrapper would leak a
project-specific choice into a cross-project interface. The README
calls this out explicitly and shows three consumer-side patterns.
chrome-devtools-mcp@0.21.0is pinned inflake.nixinstead of tracking@latest— consumers inherit a tested combination rather than whateverdrifted to the npm registry between runs. The
writeShellScriptBinrather than
writeShellApplicationchoice is deliberate: the latterpulls shellcheck at eval time (~600ms), which isn't free on a wrapper
that runs on every MCP server startup (see
docs/nix-eval-perf-report.md).Try it locally
nix run 'github:juspay/kolu/livid-phase?dir=examples/chrome-devtools-mcp-nix' -- --help