Skip to content

mcpx: accept a flake ref as a program and as an execution environment #1921

Open

Description

@dezren39

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-linux vs aarch64-darwin).
  • Eval cost bounds: nix build on a cold cache can take minutes. A timeout, and
    an --offline/nix.offline setting, are probably required before this can
    sit in an exec path at all.
  • Impure vs pure: nix develop on a flake with --impure inputs, and
    shell.nix files that read the ambient environment, cannot be cached by lock
    alone. Refuse, or cache by something weaker and say so?
  • Does exec.env compose with script.interpreter and 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 the package.nix PATH
    wrapper with something configurable?
  • nix-shell/shell.nix support means supporting the non-flake path too. Worth
    it, or flakes only with a documented nix develop -f escape 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

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    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.


    @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_modules trees and Nix closures -- orders of magnitude larger than what pruneWorkDirs/defaults.ExecWorkDirs was 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:

    #181 row Collision Owner What the owner decides
    12 three new content-addressed caches (#103, #104, #107) #187 (new) One cache mechanism (layout, keys, byte/age eviction, leases, nix GC roots) under #175; the three issues own only what they put in it.

    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).


    @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:

    • Bump checkmarx/ast-github-action from 2.0.19 to 2.0.22 #107 is OPEN with labels 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.
    • The feature — mcpx accepting 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: no pkgs/mcpx at HEAD (404); this repo's mcpx-nixbuild.yml states the source "lives only in developing-today/code now" (migration commit a03494f3090b there).
    • No evidence it already landed in the new home: searches of developing-today/code's pkgs/mcpx for flakeRef and for flake support in its environment docs return nothing.

    Route: no small_fix packet 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.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged

    • Agent: queue worker (repo-worker-code), Lane B
    • Session / ID: queue task 2816 (triage) → follow-up task 3199
    • Found via: queue claim

    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):

    • GitHub code search over pkgs/mcpx returns 0 hits for flakeRef, print-out-paths, nix develop, and devshell — no flake acceptance and no environment concept exists, exactly as the issue states; runner.Run still spawns with the inherited environment.
    • internal/registry/registry.go contains the package-type launcher map (uvx / dnx present) — the table a nix package type joins.
    • internal/settings/registry.go:333 defines script.env as environment variables — the naming collision the issue warns about is live: the Part 2 setting must be script.devshell or exec.environment, never exec.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 via nix build --no-link --print-out-paths to a store path executed directly), wired into MCP server command and 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.interpreter wiring. 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.

  6. 145 remaining items

  7. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged

    • Agent: queue worker (repo-worker-code), lane code-h
    • Session / ID: task 8726
    • Task: Triage at current main (verified fresh via gh code reads, 2026-11-04). Verdict: still live — for part 2 only.
    • Part 1 (flake ref as a program): DONE. pkgs/mcpx/internal/progspec/progspec.go implements program specs: a nix:<ref>#<attr> (or nix:<ref>) spec names a flake package, resolved once at launch with nix 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.go and internal/mcpclient/supervised.go call progspec.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.
    • Part 2 (flake ref as an execution environment): NOT implemented. No Go code under pkgs/mcpx enters a devshell or runs a script inside a flake environment — code search for nix develop / devshell under pkgs/mcpx hits only NixOS/home-manager modules and docs, no launcher path. status/waiting-dependency stays accurate for the cache piece (mcpx: one content-addressed cache for prepared dependencies, import graphs and flake resolutions #1908).
    • Route: follow-up task 9168 enqueued in repo-health-code-pilot, scoped to part 2 only (design + scope the environment-entry mechanism; part 1 must not be re-implemented). This issue stays open as the tracker for part 2.
  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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

  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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)

  11. added
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    and removed
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestkind/featureNew feature or capability requestqueue/blockedQueue or user escalation statesource/creatorFiled by the new-issue-creator jobstatus/blockedQueue or user escalation statestatus/triagedTriaged and routed (see agent note)status/waiting-dependencyWaiting on a dependencytask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions