Skip to content

Staging-ground chrome-devtools-mcp Nix flake for cross-project reuse#650

Draft
srid wants to merge 6 commits intomasterfrom
livid-phase
Draft

Staging-ground chrome-devtools-mcp Nix flake for cross-project reuse#650
srid wants to merge 6 commits intomasterfrom
livid-phase

Conversation

@srid
Copy link
Copy Markdown
Member

@srid srid commented Apr 21, 2026

A reusable Nix flake wrapping chrome-devtools-mcp, staged inside the
Kolu repo at examples/chrome-devtools-mcp-nix/ so the shape can be
iterated on before it's extracted to its own repo (e.g.
github:srid/chrome-devtools-mcp-nix). Consumers point their Claude
Code .mcp.json at nix run ...; the flake provides a pinned npx
invocation with nodejs in the closure.

The volatility boundary is the interesting part. The wrapper owns one
concern — launching chrome-devtools-mcp at a pinned version — and
nothing else.
Chrome binary provisioning stays with the consumer
(--executable-path passes through via "$@"), because different
projects 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.0 is pinned in flake.nix instead of tracking
@latest — consumers inherit a tested combination rather than whatever
drifted to the npm registry between runs. The writeShellScriptBin
rather than writeShellApplication choice is deliberate: the latter
pulls 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).

Kolu's own .mcp.json and the ai::mcp-chrome-devtools just recipe
are untouched by this PR. This is a staging demo, not a migration
— it's on the author to extract and wire up the consumer side later.

Try it locally

nix run 'github:juspay/kolu/livid-phase?dir=examples/chrome-devtools-mcp-nix' -- --help

srid added 6 commits April 21, 2026 00:04
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.
@srid
Copy link
Copy Markdown
Member Author

srid commented Apr 21, 2026

Hickey/Lowy Analysis

# Lens Finding Disposition
1 Hickey mkMcp evaluated twice per system — complects derivation identity with call count Fixed in this PR (7b752b7, later superseded by 8674657 which actually satisfied the constraint)
2 Hickey CI smoke-test on the pinned version is documented as a norm, not mechanized Deferred to extracted repo — noted in README
3 Hickey No error context when npx fails (raw npx error, no flake/version breadcrumb) No-op — marginal for a staging demo
4 Lowy Offline/hermetic fetch as a third volatility axis (npm-fetch strategy) — npx -y hits npm at runtime Deferred to extracted repo — noted in README

Hickey rationale

The flake's concern separation is structurally sound — version pinning in one place, binary provisioning deferred, shell wrapper kept thin. The original perSystem function was called twice per system (once for packages, once for apps), which re-invoked mkMcp each time and silently regressed the "bind once per system" goal the let-refactor was supposed to achieve. The elegance pass in /code-police caught this and inverted the iteration so perSystem is a single eachSystem result, projected to packages and apps via mapAttrs — now mkMcp pkgs genuinely runs once per system. The rec { ... } that leaked mcp as an output attribute is also gone (now a let-local).

Lowy rationale

The implementation honors its design. MCP version volatility is owned by the flake (mcpVersion = "0.21.0", pinned and bumped deliberately). Chrome provisioning volatility is correctly deferred to the consumer via "$@" passthrough — the README names this boundary and shows three consumer patterns (PATH, custom binary, Playwright resolver) as first-class, with none elevated to "the way." Node.js runtime is encapsulated by nixpkgs (pinned via flake.lock). No Kolu-specific references leak into the flake body — the extraction seam is clean. One future-volatility axis is not yet encapsulated: npx -y assumes network reachability, which won't hold for air-gapped or strict-hermetic consumers; that's a real axis but belongs in the extracted repo, captured as a deferred item in the README.

@srid srid marked this pull request as ready for review April 21, 2026 04:15
@srid
Copy link
Copy Markdown
Member Author

srid commented Apr 21, 2026

/do results

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 inverted perSystem / mapAttrs refactor). Running /simplify earlier — e.g. alongside hickey+lowy rather than only in police — 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=haiku would 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 ci into flake-scope-aware subsets — not worth it for a one-off.

Workflow completed at 2026-04-21T15:17:00Z.

@srid
Copy link
Copy Markdown
Member Author

srid commented Apr 21, 2026

srid added a commit to srid/emanote that referenced this pull request Apr 21, 2026
…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.
@srid srid marked this pull request as draft April 23, 2026 12: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