Repository navigation
Description
Relocated from dezren39/darwin-nix#86 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).
Parent: #174
Summary
A browser view of a running daemon. The daemon is already a web server — HTTP over a unix socket and
loopback TCP, /v1/*, SSE at /v1/events, an OpenAPI document at /v1/openapi.json, MCP at /mcp — so
"web server" is done and "web frontend" is the whole of the work. The decision is not whether to serve
a UI but how much build system to accept for it, because mcpx is one buildGoModule derivation today and
a bundled SPA makes it two.
Original notes
- web frontend
- web server
Current state (what mcpx does today, with file refs)
- The listener exists. The daemon serves HTTP on a unix socket and, with
daemon.port, on TCP;
daemon.address(default127.0.0.1) chooses the interface
(internal/settings/registry.go:343-375). Nothing serves static content — every route is registered
explicitly ininternal/daemon/ops.go,routes_proto.go,routes_settings.go,routes_consumer.go,
routes_resolve.go,routes_exec.go; there is nohttp.FileServerand no/handler. - The data a UI needs is all there. 60 operations across
internal/api/ops*.go, one table that
already generates routes, MCP tools and the OpenAPI document. Every view the TUI has —
browse/log/stats/servers/sessions/storage (internal/tui/tui.go:41-51) — is a/v1call, and the
read surface is captured as an eight-method interface (internal/tui/data.go:19-28). - Live updates exist.
GET /v1/eventsis SSE (internal/api/ops.go:156), honoursLast-Event-IDso a
reconnectingEventSourceresumes by itself, and saysevent: gapin-band when it cannot
(internal/daemon/server.go:797-842). - An open stream does not keep the daemon alive today. The idle clock is stamped when a request starts
(server.go:344-353), so an open tab holding/v1/eventsis not activity after it connects; see chore(deps): bump actions/setup-go from 4 to 5 #87. go:embedis already in use (internal/defaults/defaults.go:24), so embedding assets costs no new
mechanism.- The nix package is deliberately minimal:
subPackages = [ "cmd/mcpx" ], onevendorHash, and a
comment block that enumerates and justifies each of five direct Go dependencies
(pkgs/mcpx/package.nix:36-53).
.lootbox/ui — assess before reusing
36 tracked files under .lootbox/ui: React 18, Vite 6, Tailwind 3, a shadcn/radix component set
(components/ui/*), @tanstack/react-query, zustand, monaco, and exactly one page,
src/pages/Dashboard.tsx (193 lines) behind a 34-line App.tsx. A prebuilt bundle is committed
(assets/index-B2p0eWoj.css, assets/index-BkA-9JbN.js); dist/ is empty; node_modules/ is 279 MB and
untracked. #49 already records that this copy is not built by the flake — the lootbox that actually
runs is built from upstream (flake.nix:388) — so it has no build check and nothing depends on it.
Verdict for this issue: it is lootbox's dashboard, not mcpx's, and its one page is not a starting point
for an mcpx UI. The reusable part is the shadcn component set, which is a copy of an upstream generator's
output and can be regenerated at any time. It should not be adopted as mcpx's frontend, and #49's other
option — delete it — is not blocked by anything here.
Approaches
Approach A — vendor a React SPA into the repo and bundle it at build time, least recommended
ui/ with Vite + React + Tailwind, npm run build into internal/web/dist, go:embed the result, serve
it at /.
Why not: it turns one derivation into two with two fixed-output hashes. buildGoModule grows a
buildNpmPackage stage and an npmDepsHash that Renovate will break on a cadence — #49 is already about
the vendorHash/npmDepsHash repair step never having run on a real bot PR, and this doubles the surface it
must handle. It also means the committed tree contains generated bundles (as .lootbox/ui does) or that
nix build .#mcpx requires network-free npm vendoring for a UI most users will never open. A 280 MB
node_modules in a repo whose Go side is proud of five dependencies is a values mismatch, not just a size
one. And the maintenance is not the initial build: it is React majors, Tailwind majors and Vite majors
arriving on a project whose owner is writing Go.
Approach B — publish the contract and let the UI be someone else's repo
Do no frontend work. Make /v1/openapi.json good enough that openapi-typescript or oapi-codegen
produces a usable client, document the SSE stream in the same document, and point at it from the README.
Anyone who wants a dashboard generates a client and builds one outside this repo.
This is genuinely defensible and is most of the value at nearly no cost. The OpenAPI document is generated
from the same ops table as the routes (internal/api/openapi.go), so it cannot drift, which is exactly the
property a generated client needs. docs/proposals.md:440 already gestures at this — "a dashboard, a test
harness, another language's client generator — use the same…".
Where it falls short: a contract is not a UI. Nobody gets a browser view by default, "open the dashboard"
stays a multi-step setup, and the honest outcome of "someone will build it" is that nobody does. It is also
not mutually exclusive with shipping something — the work below assumes the OpenAPI document is good, and
any gaps found while building a UI against it are bugs in the contract worth fixing anyway.
Worth checking cheaply as part of this: does /v1/openapi.json currently pass openapi-typescript and
spectral lint clean? If it does not, that is a small, high-leverage fix regardless of which approach wins.
Approach C — recommended: a zero-build frontend embedded in the binary, plus a directory escape hatch
Recommended. Ship a small, dependency-free frontend — plain HTML, CSS and ES modules, no bundler, no npm,
no transpile — go:embeded into the binary and served by the existing listener. Modern browsers run ES
modules and import directly; a five-file frontend that calls fetch('/v1/...') and subscribes to
EventSource('/v1/events') needs no build step at all. nix build .#mcpx stays one derivation with one
hash, and mcpx daemon --ui is a dashboard on a machine that has nothing installed but a browser.
Shape:
| decision | choice | why |
|---|---|---|
| where | the daemon's existing listener, at /ui/ |
a second listener is a second lifetime, a second address and a second thing to leak; the daemon's idle-exit and reload already work |
| enabling | one setting, daemon.ui (bool, scope daemon, default off) |
matches how every other capability is exposed (internal/settings/); off by default so a daemon on daemon.address=0.0.0.0 does not silently gain a UI |
| overriding | daemon.uiDir (string, default empty) serves a directory instead of the embedded assets |
the escape hatch that makes A unnecessary: anyone who wants a React SPA builds it wherever they like and points the daemon at dist/. Also the dev loop — edit, reload, no rebuild |
| data | /v1 + /v1/events only, no UI-specific endpoints |
the parity rule (docs/parity.md) is CLI = /v1 = MCP = plugin; a /ui/api prefix would be a fifth surface that drifts |
| scope v1 | the TUI's six views, read-only, plus answering elicitations | elicitation is the one thing a browser is better at than the terminal — a form with a schema beats a prompt — and /v1/elicit/{id}/{action} already exists |
| idle | apply #87's lifetime rule (#181 collision 9): activity on any surface renews the daemon's idle deadline, and nothing else does | today an open /v1/events stream counts only when it connects, so daemon.idleExit can kill the daemon under an open tab. Whether a delivering stream counts as activity is decided once, in #87, for this surface and ssh (#85) together -- not here |
Why this beats A: the cost that kills A is the toolchain, not the UI, and a no-build frontend has no
toolchain. Why it beats B: B's contract work is a prerequisite of C, not an alternative — building the
embedded UI against /v1 is the thing that proves the OpenAPI document is usable, and C ends with B's
deliverable plus a working dashboard.
The real risk of C is scope creep: without a framework, view number seven is where hand-written DOM code
starts hurting. Mitigate by fixing the scope at "the TUI's views, plus elicitation forms" and treating
anything beyond that as a reason to reach for daemon.uiDir and an external project. If the UI outgrows
no-build, daemon.uiDir means moving it out is a configuration change rather than a rewrite.
One more reason to prefer the embedded route: it makes the remote story work end to end. With the ssh
endpoint scheme in the sibling issue, ssh -L 7777:... plus http://127.0.0.1:7777/ui/ is a remote
dashboard with nothing installed locally — which is most of what the "ssh tui" want is actually asking for.
Open questions
/ui/or/? Serving at/risks a future/v1-adjacent path colliding and makes a 404 ambiguous
between "no such route" and "no such asset"./ui/with a redirect from/is probably right.- Does the UI need write operations in v1, or is read-only + elicitation the correct first cut? Write
operations (restart,shutdown,servers_remove) are markedAdmin/Destructivein the ops table
(internal/api/ops.go:451-466,internal/api/ops_settings.go:111), and a browser is exactly where a
misclick happens. - Is
.lootbox/uideleted, kept as a reference, or given a CI build? build(deps): bump build/buck/preludes/buck2-prelude fromd956689to3cfc766#49 owns the decision; this issue's
input is that it is not a useful base for mcpx. - Does
/v1/openapi.jsoncurrently generate clean withopenapi-typescript? Cheap to check, informs both
B and C. - Browser support floor: ES modules and
EventSourceare universal, but import maps and<template>-based
rendering vary. Writing the floor down avoids a debate later.
Ease · Impact · Related
Ease: S to serve a directory and add the settings; M for the embedded read-only frontend covering
the TUI's six views; L if a bundled SPA (A) is chosen. Impact: med — high for demos, remote
machines and anyone who will not open a terminal; low for the existing CLI-first user.
Depends on / related: #49 (.lootbox/ui has no build check and may be deleted; any new build stage
needs CI), #76 (CLI parity — a UI makes /v1-only operations visible to more people), #77 (protocol
follow-ups: inline answers and task status are what a live dashboard would want), #87 (owns the lifetime
rule this issue applies, collision 9; also touch-on-read for the elicitations the UI answers), #85 (a
forwarded port plus /ui/ is the remote dashboard), #88 (the forwarded port needs a knowable socket path),
#52 (closed, fixed in #182).
Security/auth deferred; see tracker.
Activity
developing-today commentedon Oct 28, 2026
CODE-809
dezren39 commentedon Oct 28, 2026
History carried over from dezren39/darwin-nix#86 (verbatim, oldest first):
@dezren39 · 2026-09-30T07:21:34Z
Integration pass. Placed in the DAG rooted at #181.
d956689tofd4ce63#88@dezren39 · 2026-09-30T07:28:15Z
Integration pass -- cross-theme collision. Surfaced only when the themes were put side by side; the authors of these issues could not see each other's work. Recorded at the root in #181.
Three surfaces, one lifetime question
A browser tab versus
daemon.idleExit(#86), leftover sshControlMasterprocesses (#85), and leaked e2e daemons (#52) are the same class of problem: who ends a session nobody explicitly closed. #86 says "decide deliberately" about its own case. Decide it once for all three.Also on: #85 #52. Nobody currently owns this.
@dezren39 · 2026-09-30T11:05:52Z
Collision ownership (issues pass, 2026-09-30, at 05c78b2). #181 listed these cross-theme collisions with no owner. Each now has one:
daemon.idleExit, sshControlMaster, leaked e2e daemons)stop --allvia info files,MCPX_AUTOSTART_IDLE_EXIT=60s).Where this issue is the owner, the decision is made here; where it is not, it adopts the owner's decision rather than making its own.
@dezren39 · 2026-09-30T12:12:16Z
Issues pass (2026-09-30, at 05c78b2).
Parent: #174. The "idle" row of the Approach C table now applies chore(deps): bump actions/setup-go from 4 to 5 #87's lifetime rule (Phase 3: sync, blob gate, and relays #181 collision 9) instead of deciding it here. Added the fact that an open/v1/eventsstream counts only when it connects (internal/daemon/server.go:344-353)./v1/eventshonoursLast-Event-IDand sends an in-bandevent: gap(server.go:797-842).d956689toe7457f6#52 as the reason against a second listener, since chore(deps): bump build/buck/preludes/buck2-prelude fromd956689toe7457f6#52 is closed (fixed in Phase 5: tag event bodies must travel with the event #182). The lifetime argument now points at chore(deps): bump actions/setup-go from 4 to 5 #87. Related adds chore(deps): bump openssl from 0.10.54 to 0.10.60 in /sources/pavex/examples/realworld #85, chore(deps): bump actions/setup-go from 4 to 5 #87 and chore(deps): bump build/buck/preludes/buck2-prelude fromd956689tofd4ce63#88.registry.go:343-375,tui.go:41-51,data.go:19-28,package.nix:36-53,ops.go:451-466+ops_settings.go:111. Added thedesignlabel.@dezren39 · 2026-10-28T21:09:48Z
Triage verdict — partially implemented in the new home; remainder confirmed, not closeable as done.
Verified against current main (2026-10-28):
dezren39/nix@8c42c2c, origin/main) has no mcpx Go source —pkgs/mcpx/deleted 2026-10-19 (lab-ops-hub container crawler accepts inet_aton IPv4 spellings (127.1, 2130706433, 0x7f.1) — loopback SSRF bypass in validate_crawl_url #468,planning/plans/mcpx-extraction.mdsteps 4–5); mcpx lives solely indeveloping-today/codeatpkgs/mcpx/.developing-today/code@528886e): the core of Approach C. A zero-build, dependency-free single-page frontend:pkgs/mcpx/internal/dashboard/dashboard.goembeds one 20 KBdashboard.htmlvia//go:embedand the daemon serves it —mux.HandleFunc("GET /dashboard", …)andGET /v1/dashboardatpkgs/mcpx/internal/daemon/server.go:642-643, handler inroutes_dashboard.go, an ops-table entry ("Live web dashboard HTML",internal/api/ops.go:594), amcpx dashboardCLI command, and a handler test (routes_dashboard_test.go). Its data is/v1-only (/v1/status,/v1/metrics/tokens,/v1/metrics/tools,/v1/search,/v1/feedback,/v1/interactions), as Approach C requires. No second build system was added.git grep "daemon.ui|uiDir" pkgs/mcpxat code HEAD returns nothing — there is nodaemon.uidefault-off setting and nodaemon.uiDirdirectory escape hatch (both are explicit Approach C decisions); the route is/dashboard, not the proposed/ui/; and the page's scope is pools + token/tool metrics telemetry, not the TUI's six views (browse/log/stats/servers/sessions/storage) read-only plus elicitation forms.Route: no
small_fixpacket in the nix queue, and do not close this issue as fixed — the shipped dashboard satisfies the "serve a zero-build embedded UI" headline but not the setting/gating, escape-hatch, or six-view scope decisions. Keep open here until code#309 re-files the remainder indeveloping-today/code, then close here with that link (crediting the dashboard that already shipped).agent-note:v1 · agent: repo-queue-worker-shift-nix (lane A) · task 2084 · kind: triage · outcome: partially implemented in code (embedded dashboard at /dashboard), remainder (daemon.ui/uiDir, six views + elicitation) confirmed open; routed to #309; no nix follow-up packet
dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 29, 2026
🤖 Agent note · done
Triage verdict — core already built at main
42796be5; rescope the restThe issue's Current State (written against the nix-era tree) says "nothing serves static content — there is no
http.FileServerand no/handler", and Approach C recommends a zero-build frontendgo:embeded into the binary. That mechanism now exists at HEAD:pkgs/mcpx/internal/dashboard/dashboard.go—//go:embed dashboard.html, a "web dashboard UI" handler; the page is a ~20 KB single-file app titled "mcpx Dashboard" (metrics-centric: Tokens / Tools / Metrics, some servers/sessions).pkgs/mcpx/internal/daemon/routes_dashboard.go— serves it at/dashboard, alongside metrics handlers.pkgs/mcpx/internal/cli/dashboard.go—mcpx dashboard [--open]ensures the daemon and prints/opens the/dashboardURL.http.FileServerexists underpkgs/mcpxoutside e2e tests — the embedded route is the only static serving, in the exact no-second-build-system shape the issue prescribes.What remains unbuilt (the rescoped scope):
internal/settings/registry.godaemon.*entries are reapInterval / saveInterval / port / endpoint / address / inline / autostart —daemon.uihas zero hits in repo code. The dashboard is always served; the issue's "off by default so a daemon bound beyond loopback does not silently gain a UI" decision was never taken.daemon.uiDirescape hatch — an external directory cannot replace the embedded assets (the dev loop / external-SPA path)./ui/vs/question is settled by the existing/dashboardroute — keep it.Dedupe — no duplicate issue. Searches for "web frontend",
daemon.ui,uiDirreturn only #1882 itself and the surfaces tracker #1887 (ssh + browser surfaces parent). #1876 (docs runnable example) is a different surface — it consumes the daemon from the docs site and its stage-3 local mode will want the CORS/port decisions noted there, not this UI.Follow-up enqueued: queue task 3115 (
small_fix, deduperepo-health:v1:fix:developing-today/code:issue:1882), rescoped to the three gaps above —daemon.uigating (default to be settled deliberately on the PR, weighing the issue's loopback argument against today's always-onmcpx dashboardbehaviour),daemon.uiDiroverride, and extending the embedded zero-build page to the six TUI views + elicitation,/v1-only. It explicitly excludes rebuilding the frontend or any bundled SPA.24 remaining items
dezren39 commentedon Oct 30, 2026
🤖 Agent note · picked up
6450ea72); this shift resumes at the recorded checkpoint: extend the embeddeddashboard.htmlto the TUI's six views (browse/log/stats/servers/sessions/storage, read-only) plus elicitation answering via the existing/v1/elicitendpoints, data strictly via/v1+/v1/events.feat(mcpx): dashboard operator views + elicitation answering (#1882)
dezren39 commentedon Oct 30, 2026
🤖 Agent note · parked
0af8d077— embedded dashboard now carries the six TUI views (read-only, /v1 only) plus elicitation answering; laptopgo test ./internal/daemon/ ./internal/dashboard/PASS and the page script passesnode --check. Detail comment is on PR feat(mcpx): daemon.ui gating and daemon.uiDir override for /dashboard (#1882) #2521.0af8d077still QUEUED after ~5 minutes of polling (repo-wide runner backlog; the requiredbasic: proxy port fallbackscheck has not started).0af8d077(with the 2 pre-existing main reds ininternal/defaults/internal/settingsdiscounted, as on PR docs(mcpx): level-1 pages — overview, install, quick start, now-what (#1878) #2502), read any new Codex findings on the new commit and disposition them, then merge PR feat(mcpx): daemon.ui gating and daemon.uiDir override for /dashboard (#1882) #2521, complete task 3115 asfixed_merged, and close mcpx: a web frontend served by the daemon, without a second build system #1882.Merge pull request #2521 from developing-today/queue/3115-1882-daemon-ui
dezren39 commentedon Oct 30, 2026
🤖 Agent note · done — merged
awaiting_ci, completedfixed_mergedmainas7d49db36a7715fa3334f52ac7ed152663f246720at 2026-10-30T04:30:58Z (head0af8d077); verified as an ancestor oforigin/main(674c4c6a) at close time.internal/daemon/routes_dashboard.go(+97) serves the dashboard behind thedaemon.uisetting with adaemon.uiDiroverride;internal/dashboard/dashboard.html(+345) is the embedded page (no second build system);internal/settings/registry.go(+26) declares the two settings;server.gowires the route;routes_dashboard_test.go(+158) covers the gating, the override and the TUI-view/elicitation coverage;docs/configuration.mddocuments both keys.gh pr mergeput it through themainmerge queue and it landed; the PR-head rollup still showed the three basic jobs as QUEUED at the moment I read it, i.e. the queue accepted on its own merge-group validation rather than a completed PR-head run. No held /user/owned/do-not-mergelabels.