Repository navigation
Description
Relocated from dezren39/darwin-nix#107 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: #175
Cache: deferred to #187 (#181 collision 12). #187 owns the cache mechanism
(layout, keys, eviction, leases, and the nix GC roots that keep a resolved store
path alive); this issue owns what it puts in it — resolved program store paths
(part 1) and captured devshell environments (part 2). The caching text below
stays as input to #187.
Summary
Two related asks. First, accept a flake reference anywhere mcpx currently takes
a command to run — MCP server launchers, CLI→MCP adapters, script interpreters,
anything with a command/args pair — so that github:owner/repo#tool is a
valid thing to configure. Second, accept a flake reference as the environment
a script runs in: resolve a devshell (or an ad-hoc flake, or the shell.nix in
the working directory), enter it, and run the script inside.
Original notes
- accept a flake url#path anywhere a binary/script/remote/api/etc can be used
- accept a flake url as an alternative exec env (devshell/shell.nix in default pwd or tempdir or ad-hoc flake that isfully isolated, path executed before script, script executed inside
Current state
mcpx has several places that name a program to run, and all of them take a
literal command:
| place | shape |
|---|---|
| MCP server config | command, args, env, cwd, or a url for HTTP servers |
| registry add | package type → launcher: npm→npx -y, pypi→uvx, oci→docker (internal/registry/registry.go:248-255, also nuget→dnx) |
paths.adapters |
a CLI wrapped as an MCP server |
runner.Detect |
deno/bun/node looked up with exec.LookPath |
mcpx api --spec |
an OpenAPI document turned into tools |
There is no environment concept at all: runner.Run spawns the runtime with
os.Environ() plus MCPX_* plus script.env, in opts.Dir. Whatever is on
PATH is what the script gets. The Nix derivation compensates by suffixing
git, deno, bun-bin and nodejs onto PATH in the wrapper
(pkgs/mcpx/package.nix:96-103, docs/dependencies.md) — a fixed set, decided
at build time, identical for every script. The container (#182) is a second
fixed set: bun and git only, with MCPX_SCRIPT_RUNTIME=bun
(pkgs/mcpx/Containerfile:66-80; the git half may change with the in-flight
native git resolution).
mcpx is packaged with Nix and developed in a Nix flake repo, so the audience for
this is the audience that already exists. Nix 2.26.2 is installed here.
Relevant Nix surface (all stable today):
| need | command |
|---|---|
| run an app/package from a flake | nix run <ref>#<attr> -- args… |
| put packages on PATH for one command | nix shell <ref>#<attr> -c cmd args… |
| enter a devshell for one command | nix develop <ref> -c cmd args… |
a legacy shell.nix |
nix develop -f shell.nix -c cmd, or nix-shell shell.nix --run |
| resolve to a store path once | nix build --no-link --print-out-paths <ref>#<attr> |
<ref> covers github:o/r/rev, path:./dir, ., git+ssh://…, and a
registry name — all with an optional #attrpath, which is exactly the
"flake url#path" the note asks for.
Approaches
Approach A — least recommended: document that command: "nix" already works
It does: {"command":"nix","args":["run","github:o/r#tool","--"]} configures a
flake-provided MCP server today, and nothing in mcpx needs to change.
Why not. It works and it is bad. Every server start pays a flake evaluation —
seconds on a warm cache, much worse cold, and unbounded on a github: ref that
has to fetch; the pool starts and restarts servers, so that cost is paid
repeatedly rather than once. There is no way to express "pin this", "do not go
to the network", or "fail fast if it is not already built", because all of that
is buried in an argv the user typed. mcpx cannot report which store path a
server actually came from, so a configuration that silently follows main looks
identical to one that is pinned — and docs/story.md:1672 already commits to
the opposite ("A configuration that silently upgrades is one that…"). And it
does nothing at all for the second bullet: there is no seam where an
environment could wrap the script.
Approach B — middle: a flake field wherever a command is configured
Add an alternative spelling to the places that take command/args:
{ "flake": "github:owner/repo#tool", "args": ["--port", "0"] }mcpx expands it to nix run <ref> -- args…, or better to nix shell <ref> -c,
and keeps the rest of the launcher machinery unchanged. Same for adapters, and
script.interpreter (see the shebang sibling issue) gains the same form.
Small, obvious, and it makes the configuration say what it means instead of
hiding it in argv. It also gives mcpx somewhere to hang options — --offline,
--refresh, a pinned rev — as fields rather than flags the user must know.
What it does not do: it is still an evaluation per start, and it still has no
environment story. It also risks a second, parallel expansion mechanism
alongside the @placeholder launcher templates from #7, which already exist to
turn a declaration into a command line.
Approach C — recommended: one resolver that produces a store path, cached; plus exec.env as a first-class execution environment
Part 1 — resolve once, execute many. A single internal resolver turns a
program spec into an argv, and a flake spec resolves via nix build --no-link --print-out-paths <ref>#<attr> to a store path, which is then executed
directly. The resolution is cached, keyed by the reference plus its locked
input (nix flake metadata --json gives the locked URL and a lastModified);
the resulting store path is immutable by construction, so the cache can never be
stale in the dangerous direction. It can be dangling, though:
nix-collect-garbage removes a store path nothing roots, so each cached entry
needs a GC root held for as long as the entry lives — one of the reasons the
mechanism is #187's rather than a private map here.
This is the difference that matters: the hot path spawns
/nix/store/…/bin/tool with no nix process in it at all. A launcher that
evaluates on every server start is an unbounded latency source in a project that
measures its exec path in tens of milliseconds (#79); a launcher that resolves
once and then spawns a store path costs the same as any other binary.
It also makes mcpx able to say what it ran — the store path is the identity,
so mcpx status and the logs can report which derivation a server actually is,
and a pinned configuration is visibly different from a floating one.
The spec is one string with a scheme, so it works in every place that takes a
command without a schema change per place:
| spec | meaning |
|---|---|
tool / /usr/bin/tool |
today's behaviour, exec.LookPath |
nix:github:owner/repo#tool |
resolve and run that flake output |
nix:.#tool |
the flake in the working directory |
nix:nixpkgs#ripgrep |
the registry |
Applied uniformly: MCP server command, adapters, script.interpreter, the
registry launcher table (so a nix package type joins npm/pypi/oci), and
mcpx api --spec's fetch. "Anywhere a binary/script/remote/api/etc can be used"
is then one resolver, not N features.
Part 2 — exec.env, the execution environment. A separate setting from the
interpreter, because they are separate questions: the interpreter is what runs
the script, the environment is what the script can see.
Naming: exec.env is a working name and should change before it ships.
script.env already exists and means environment variables for the script
(internal/settings/registry.go:254, --env KEY=VALUE); two settings whose
names differ only in exec. vs script. and mean different things is a trap.
Something like script.devshell or exec.environment says what it is.
| value | behaviour |
|---|---|
host (default) |
today — inherit PATH and the environment, unchanged |
flake:<ref> |
nix develop <ref> -c <runtime> <script> |
shell:<file> |
nix develop -f shell.nix -c … for a legacy file |
auto |
use flake.nix/shell.nix in the working directory if one exists, else host |
isolated |
an ad-hoc generated flake in a temp dir with only the declared packages — no host PATH leakage |
isolated is the interesting one and is what the note's "fully isolated" asks
for: mcpx writes a minimal flake naming exactly the runtime plus whatever the
dependency set declares, and the script runs with nothing else reachable. That
is a genuine capability nothing else in mcpx has — runner.Permissions is
honest that only Deno has a sandbox and that bun and node "run with the user's
own authority whatever is asked". A Nix-provided environment is not a sandbox,
but it is a reproducible and minimal one, and it is the first answer mcpx
could give to "what did this script actually have access to".
The note's "path executed before script, script executed inside" is the
devshell's own hook — shellHook runs on entry, before the command — which
nix develop -c already honours. Nothing new is needed for it; it should be
documented rather than reimplemented as another launcher phase.
Same caching argument applies: nix develop re-evaluates every time, so the
resolved environment should be captured once (nix print-dev-env --json, or a
built profile store path) and replayed as an env diff on subsequent runs, with
the flake lock as the cache key.
Part 3 — the honest cost table, which belongs in the comparison doc:
| environment | first run | subsequent | isolation | reproducible |
|---|---|---|---|---|
host |
0 | 0 | none | no |
flake: resolved+cached |
eval + possible build | ~0 | PATH-level | yes, pinned |
flake: naive (nix run each time) |
eval | eval | PATH-level | only if pinned |
isolated |
eval + build | ~0 | strongest available | yes |
Open questions
- Is Nix a dependency or an option?
docs/dependencies.md's rule is that
anything listed breaks something by its absence.nix:specs should degrade
with a clear message on a machine without Nix, not fail obscurely — and mcpx
must still build and run for people who do not use Nix. - Where does resolution happen for a remote exec — on the daemon, always? A
flake ref is machine-independent, which is an argument for the daemon doing it
and an argument that the answer may legitimately differ by platform
(x86_64-linuxvsaarch64-darwin). - Eval cost bounds:
nix buildon a cold cache can take minutes. A timeout, and
an--offline/nix.offlinesetting, are probably required before this can
sit in an exec path at all. - Impure vs pure:
nix developon a flake with--impureinputs, and
shell.nixfiles that read the ambient environment, cannot be cached by lock
alone. Refuse, or cache by something weaker and say so? - Does
exec.envcompose withscript.interpreterand with a shebang? The
natural rule is environment-then-interpreter —nix develop … -c bash script.sh— but the shebang case needs the interpreter to exist inside the
environment, which it may not. - Should a flake spec be allowed for the JavaScript runtime itself
(script.runtime = "nix:nixpkgs#deno"), replacing thepackage.nixPATH
wrapper with something configurable? nix-shell/shell.nixsupport means supporting the non-flake path too. Worth
it, or flakes only with a documentednix develop -fescape hatch?
Ease · Impact · Related
Ease L · Impact medium overall, high for this repository's own users —
mcpx is packaged and developed in Nix, and "the environment a script runs in" is
currently a fixed list chosen at build time.
Depends on / related: #187 (owns the cache mechanism, #181 collision 12),
#7 (launcher placeholders — the existing declaration-to-command-line machinery
this must not duplicate), #79 (startup cost, which naive nix run would
wreck), #78 (daemon cache correctness — the resolved-store-path cache is new
daemon state), #49 (CI), and siblings #106 (shebang/interpreter dispatch, where
exec.env composes), #103 (dependency configuration; "baked into binary"
overlaps with a Nix-vendored dependency set) and #101 (the comparison doc).
Security/auth deferred; see tracker.
Activity
developing-today commentedon Oct 28, 2026
CODE-847
dezren39 commentedon Oct 28, 2026
History carried over from dezren39/darwin-nix#107 (verbatim, oldest first):
@dezren39 · 2026-09-30T07:21:45Z
Integration pass. Placed in the DAG rooted at #181.
d956689to9afedba#100 Bump build/buck/preludes/buck2-prelude fromd956689to8740ce0#101 Bump build/buck/preludes/buck2-prelude fromd956689to062245a#103 Bump tracing from 0.1.38 to 0.1.40 in /sources/pavex/examples/skeleton #104 Bump h2 from 0.3.18 to 0.3.24 in /sources/pavex/examples/skeleton #105 Bump h2 from 0.3.19 to 0.3.24 in /sources/pavex/examples/realworld #106@dezren39 · 2026-09-30T07:28:25Z
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 new content-addressed caches, one cache design needed
#103, #104 and #107 each introduce a content-addressed cache, sized for
node_modulestrees and Nix closures -- orders of magnitude larger than whatpruneWorkDirs/defaults.ExecWorkDirswas sized for. Without one design across the three, #78 gets a fourth instance of the same problem.Also on: #103 #104 #78. Nobody currently owns this.
@dezren39 · 2026-09-30T11:06:01Z
Collision ownership (issues pass, 2026-09-30, at 05c78b2). #181 listed these cross-theme collisions with no owner. Each now has one:
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:04:36Z
Issues pass (2026-09-30, at 05c78b2).
Parent: #175and "Cache: deferred to Phase 4: joiner with missing pre-join control messages stalls silently #187" (Phase 3: sync, blob gate, and relays #181 collision 12). Part 1's resolved store paths and part 2's captured devshells become entries in Phase 4: joiner with missing pre-join control messages stalls silently #187's mechanism, and the caching text stays as input. Added the one fact Phase 4: joiner with missing pre-join control messages stalls silently #187 must own: a cached store path is immutable but can be garbage-collected, so each entry needs a nix GC root for as long as it lives.exec.envis too close to the shippedscript.env(environment variables,--env KEY=VALUE,internal/settings/registry.go:254). Suggestedscript.devshellorexec.environment.internal/registry/registry.go:248-255) and the wrapper (package.nix:96-103). Added the container as a second fixed runtime set (Containerfile:66-80, bun + git; the git half may change with GIT's in-flight PR). Siblings are now listed by number.@dezren39 · 2026-10-28T21:49:56Z
Triage verdict (task 2140, repo-health-nix-pilot): RELOCATED — confirmed unimplemented feature; program-acceptance code moved to developing-today/code, cache facet stays with #187. No fix packet in this repo.
Verified against current main at
224c7b738e88c7a8e5d406987a7d88257e229db2:enhancement,design— no hard-stop labels. Parent tracker fix(id): make Nix E2E check reproducible #175 is OPEN; the cache mechanism (including the nix GC roots that keep a resolved store path alive) is deferred to Phase 4: joiner with missing pre-join control messages stalls silently #187, also OPEN.mcpxaccepting a flake ref as a program and as an execution environment — is implemented, if anywhere, in mcpx's CLI/runner program-resolution code. That code no longer lives in dezren39/nix: nopkgs/mcpxat HEAD (404); this repo'smcpx-nixbuild.ymlstates the source "lives only in developing-today/code now" (migration commita03494f3090bthere).pkgs/mcpxforflakeRefand for flake support in its environment docs return nothing.Route: no
small_fixpacket in dezren39/nix — this repo's only stake is the flake pin it builds, not program acceptance. The feature routes with the source in developing-today/code, referencing #107 and #187 (which owns what the resolution cache stores and its GC roots); per the cross-repo rule, do not re-file it there. #107 stays open on the nix side until implemented there or transferred.dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 29, 2026
🤖 Agent note · triaged
Verdict: scope CONFIRMED against current pkgs/mcpx main; not a duplicate; Part 1 follow-up enqueued, Part 2 remainder stays tracked here.
Dedupe (searched all code issues: "mcpx flake ref", "mcpx devshell", "mcpx flake program environment", "nix resolver store path mcpx"): only #1921 itself plus distinct-scope items — #1908 (one content-addressed cache; owns the mechanism and the nix GC roots this feature must use, not the feature), #1983 (testing: environments as CI matrix entries — test infrastructure, not program acceptance), #1895 (the nix derivation's own man page/completions), and trackers #1889 / #1900. Nothing else accepts a flake ref as a program or an execution environment. No duplicate closure.
Scope confirmation at main
c58507a3630e1a5a4935b3279a61c7fb59e3c65b(2026-10-29):pkgs/mcpxreturns 0 hits forflakeRef,print-out-paths,nix develop, anddevshell— no flake acceptance and no environment concept exists, exactly as the issue states;runner.Runstill spawns with the inherited environment.internal/registry/registry.gocontains the package-type launcher map (uvx / dnx present) — the table anixpackage type joins.internal/settings/registry.go:333definesscript.envas environment variables — the naming collision the issue warns about is live: the Part 2 setting must bescript.devshellorexec.environment, neverexec.env.Enqueued follow-up: task 3199 (small_fix, dedupe
repo-health:v1:fix:developing-today/code:issue:1921) — Part 1 only: one program-spec resolver (plain spec = today's LookPath;nix:<ref>#<attr>resolves once vianix build --no-link --print-out-pathsto a store path executed directly), wired into MCP servercommandand the registry launcher table, with explicit degradation (no nix / bad ref / timeout → error naming the spec, never a silent PATH fallback). Verification:go test ./...in pkgs/mcpx via CI, resolver tested against a stubbed nix binary.Remainder (stays on this issue): Part 2 execution environments (
flake:/shell:/auto/isolated, captured-env replay), the persistent resolution cache + GC roots (mechanism is #1908's), and adapter /script.interpreterwiring. The open questions (nix as dependency vs option, eval timeout/offline bounds, impure flakes, remote-exec resolution site) gate Part 2 and the cache, not the Part 1 resolver.145 remaining items
dezren39 commentedon Nov 4, 2026
🤖 Agent note · triaged
ghcode reads, 2026-11-04). Verdict: still live — for part 2 only.pkgs/mcpx/internal/progspec/progspec.goimplements program specs: anix:<ref>#<attr>(ornix:<ref>) spec names a flake package, resolved once at launch withnix build --no-link --print-out-paths, memoised in-process, with explicit errors (never a silent PATH fallback). Wired into the launch path:internal/mcpclient/stdio.goandinternal/mcpclient/supervised.gocallprogspec.Resolve. The durable cache / GC roots the issue deferred are owned by mcpx: one content-addressed cache for prepared dependencies, import graphs and flake resolutions #1908 per the progspec package doc.pkgs/mcpxenters a devshell or runs a script inside a flake environment — code search fornix develop/devshellunderpkgs/mcpxhits only NixOS/home-manager modules and docs, no launcher path.status/waiting-dependencystays accurate for the cache piece (mcpx: one content-addressed cache for prepared dependencies, import graphs and flake resolutions #1908).dezren39 commentedon Nov 4, 2026
agent-note:v1
state: picked_up
task: 9168 · queue repo-health-code-pilot · worker repo-worker-code · lane 1a180ae5-384f-4e71-8d77-08f7b1652c75
found_via: queue claim
Task: Scope #1921 Part 2 only: entry semantics for a flake/devshell execution environment (resolution, isolation, cwd, failure modes), its dependency on #1908's cache, and a sized implementation route. Part 1 (flake ref as program) is explicitly excluded.
Expectation: Revalidate current GitHub/main state, record the remaining design decisions and dependency state on #1921, and identify whether the implementation is one fix task or needs a design issue. #1921 stays open for Part 2.
Links: #1921 · #1908
dezren39 commentedon Nov 4, 2026
agent-note:v1
state: blocked
reason_class: packet_defect
task: 9168 · queue repo-health-code-pilot · worker repo-worker-code · lane 1a180ae5-384f-4e71-8d77-08f7b1652c75
Blocker: The packet scopes #1921 Part 2 without accounting for open overlapping tracker #1889 and its claimed triage task 8118. #1889 owns the broader question of what environment scripts run inside; its current labels are status/in-progress, queue/claimed, work/assigned. A previous #1921 triage (task 7792) already recorded #1889 as the same design surface and recommended no implementation packet until that tracker is triaged. Proceeding here risks duplicate/conflicting design work.
Required before retry: reconcile task 9168's ownership/scope with #1889/task 8118, then update or replace the packet. Task 9168 is blocked as packet_defect; no code/worktree changes were made.
Evidence: #1889 · #1889 (comment) · #1921 (comment)