23. May 2026
MCP server configs across the ecosystem default to npx. That quietly forces a Node runtime onto every machine that wants to run a native binary tool. bx is the missing primitive for the other case.
I spent the weekend building bx — short for binary execute — a single static binary whose only job is to fetch, cache, and exec other single static binaries from GitHub releases. Think npx/uvx/pipx, except there is no Node and no Python anywhere in the picture.
bx grahambrooks/symgraph -- --version # latest release
bx grahambrooks/[email protected] serve # pinned tag
bx grahambrooks/symgraph#cli -- foo # named binary
bx --refresh grahambrooks/symgraph serve # ignore cache
That is the whole user-facing surface for milestone 0. The interesting part is what it removes.
If you’ve wired up an MCP client recently — Claude Desktop, Cursor, Zed, an agent SDK — you’ve probably typed something close to this:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
}
}
}
The convention works. It also means every MCP server in your config, including the ones that aren’t JavaScript, tends to be invoked through a Node process manager. The Python ecosystem reaches for uvx. Compiled tools have no equivalent — so when a Rust or Go MCP server ships a static binary, the surrounding tutorials still tell you to install something runtime-heavy to “manage” it.
The cost adds up:
~/.npm cache is hundreds of megabytes. A fresh npx -y invocation downloads and unpacks a package tree before it execs anything.PATH.For tools that are already compiled and statically linked, all of that is pure overhead.
It gets worse the moment nvm enters the picture, which it usually has — nvm is how most working JavaScript developers install Node in the first place.
nvm doesn’t install Node on the system PATH. It installs each Node version under ~/.nvm/versions/node/vX.Y.Z/bin/ and prepends the active one to PATH via a shell hook sourced from ~/.bashrc or ~/.zshrc. That hook only runs for interactive shells.
MCP clients don’t launch their servers from an interactive shell. Claude Desktop, Cursor, Zed, and most agent SDKs fork the configured command directly. The environment they hand the child is whatever the GUI app inherited from launchd or your desktop session — and launchd never sourced your shell rc files. The result is the failure mode every MCP user has hit at least once:
spawn npx ENOENT
The fixes people reach for all have sharp edges:
/Users/you/.nvm/versions/node/v22.3.0/bin/npx). Now the config breaks the next time you nvm install and switch.npx into /usr/local/bin. The symlink points at one Node version forever, silently diverging from the version your terminal uses.bash -lc so the login shell sources nvm first. Adds a shell startup to the hot path of every server launch, and on macOS launchd’s default shell isn’t necessarily what you think.corepack enable, volta, switch to fnm. Each is a different version manager with the same fundamental shape: Node lives somewhere only its hook knows about.Even when the launcher does find Node, there is still the version-drift problem. nvm use 20 in one terminal doesn’t change which Node a long-running Claude Desktop process is using; that one captured PATH at startup. So your MCP servers and your project work can be on different Node versions, and the only way to find out is when one of them stops working.
And npx -y caches the package tree per Node version. Switch from 20 to 22 and the next launch re-downloads everything, because the cache lives under the version’s own prefix.
bx sidesteps all of this by not having a runtime to locate. It’s a single binary on PATH, the asset it fetches is a single binary, and neither one cares which version of anything else is installed. The MCP config becomes:
{
"mcpServers": {
"symgraph": {
"command": "bx",
"args": ["grahambrooks/symgraph", "serve"]
}
}
}
Which keeps working across nvm install, nvm use, a fresh laptop, or a sandbox that has never heard of Node.
The runtime and PATH problems are operational. The supply-chain problem is the one that should genuinely worry you.
Every npx -y some-mcp-server is a decision to download and execute, with your user’s permissions, the latest published version of a package and every transitive dependency it pulls in. A typical MCP server published as an npm package will have a dependency tree in the dozens to low hundreds of packages, maintained by an equally large set of accounts whose security posture you have no visibility into.
The track record is not abstract:
event-stream (2018) — a popular package was handed off to a new maintainer who quietly added a payload that stole Bitcoin wallet credentials. Pulled in transitively by millions of installs before discovery.ua-parser-js, coa, rc (2021) — maintainer accounts compromised, malicious versions published that ran credential-stealing post-install scripts. All three were dependencies-of-dependencies for huge swaths of the ecosystem.node-ipc (2022) — the maintainer themselves shipped a version that wiped files on machines geolocated to specific countries. This wasn’t a compromise; it was the author.colors / faker self-sabotage (2022) — author pushed infinite-loop versions to protest unpaid work. Broke CI everywhere overnight.Three properties of the npm model make this structurally hard to fix:
npx -y resolves to the latest version by default. Most MCP config snippets you’ll find online don’t pin a version. You’re authorizing whatever the maintainer (or whoever has their token today) publishes next.node_modules or the accounts that publish them.The bx model narrows all three:
owner/repo. There is no transitive package graph. What you trust is the same thing you’d trust if you downloaded the binary yourself from the release page.bx does fetch → extract → exec. The binary doesn’t run until you actually invoke it, and even then it’s a normal process — nothing happens during “install.”bx grahambrooks/[email protected] resolves to a specific GitHub release, immutable once published. Milestone 1 adds checksum verification on top of that; milestone 4 adds Sigstore signatures and trust-on-first-use. Even today, “pin a tag” is a meaningfully stronger default than “whatever npx -y resolves to right now.”None of this makes static binaries inherently safe — a compromised release is still a compromised release. But the trust boundary is one author, one repo, one artifact, instead of one author plus the closure of every npm account whose package ended up in their dependency tree. For something that an AI agent is going to be invoking on your behalf, with access to your filesystem and your tokens, that is a very different security posture.
The pipeline is deliberately small:
owner/repo[@ref][#binary].darwin-arm64, x86_64-unknown-linux-gnu, windows-x86_64, the usual vocabulary.$XDG_CACHE_HOME/bx/<owner>/<repo>/<tag>/, extract tar.gz or zip, find the binary inside.exec it with full stdio passthrough and forward the exit code.That last point matters for MCP specifically. The stdio transport assumes the server’s stdin and stdout are the channel — anything wrapping the process has to be transparent to byte streams, signals, and exit status. bx is built around inheriting those rather than buffering them.
A pinned tag is the fast path: if the cache has the binary, bx doesn’t talk to the network at all. The first byte of an MCP request hits the actual server in low double-digit milliseconds.
For a single MCP server, the comparison looks like this:
| Stack | What ends up on disk to launch one tool |
|---|---|
npx -y some-server |
Node runtime + npm cache + the package tree + the tool |
uvx some-server |
Python + uv + a managed venv + the tool |
bx owner/repo |
bx (one static binary) + the tool (one static binary) |
Multiply by the number of MCP servers in a real config — a filesystem server, a search server, a code-intelligence server, a calendar server, a couple of project-specific ones — and the savings stop being theoretical. On a laptop it’s tens of seconds of cold-start time and a few hundred megabytes. In an agent sandbox that spins up per task, it’s the difference between “ready” and “still installing.”
MCP is the motivating case, but the same shape shows up everywhere a project depends on a CLI that happens to be a static binary:
ripgrep, fd, jq, a custom linter.In each of those, the thing you actually want is “give me this binary, pinned to this tag, cached, on PATH” — and you want it without standing up a package manager that assumes a language runtime.
Milestone 0 ships the fetch/cache/exec foundation. The roadmap from there:
.bx.toml manifest, checksum verification, and bx prune. The manifest is what makes a repo say “these are the binaries this project needs” the same way a package.json lists npm dependencies.bx mcp add/list/update/inspect, which reads and writes MCP client configs directly. The goal is a single command that registers a native MCP server in Claude Desktop or Cursor without hand-editing JSON.bx ensure --skill <dir> resolves them. This is the piece that closes the loop with the AI-agent direction the rest of my tooling has been pointed at.--offline mode. The point at which “fetch from GitHub” becomes something a security-conscious team can adopt by default.brew tap grahambrooks/bx https://github.com/grahambrooks/bx
brew install bx
bx grahambrooks/symgraph -- --help
Or build from source — cargo install --git https://github.com/grahambrooks/bx. The repo is grahambrooks/bx; issues and milestone-1 design feedback welcome.
The bet is straightforward: when the tool you’re running is a static binary, the launcher should be one too.