november 2026 · 4 min

A Lockfile Only Counts if the Install Reads It

A lock file protects exactly one thing: the install command that reads it. Ship it in your repo, pin every hash, and it still does nothing for the person who installs your package from a registry, because the registry doesn't carry it. The lock lives in the source tree. The install runs from the package's declared ranges.

That gap is what Corelight measured this week, and MCP servers sit right in it.

What Happened

Corelight's post (Nov 10) looked at the launcher cache on a machine running a community MCP server for a network gateway. The cache held 19 builds of the same release, same commit, made over five weeks. Those 19 builds came out as 15 distinct dependency sets, and 23 of the server's 69 packages changed version along the way. The maintainer had done the right thing and published a hash-pinned lock file. In Corelight's words, "the launcher was installed from the package's declared dependencies, not from the lock file."

Across the MCP servers it watched, Corelight counted over 1,600 connections in one week to 11 management systems, the firewall and the SIEM among them. Of the 35 servers it reviewed, 23 recommend an install that resolves fresh from a public registry. No malware turned up. My point is that nobody would have noticed the day a dependency changed.

Why the Lock Never Arrives

Look inside any published wheel. Here is mcp-server-fetch 2026.11.11, the reference fetch server:

unzip -p mcp_server_fetch-*.whl '*/METADATA' | grep Requires-Dist
# Requires-Dist: httpx[socks]>=0.27
# Requires-Dist: mcp<3,>=2.3
# Requires-Dist: pydantic>=2.0.0
# ...

Ranges, and no uv.lock in the archive. The repo's uv.lock pins and hashes every package; the wheel has eight ranges. uvx mcp-server-fetch resolves those ranges against PyPI as it stands at that moment, and uv's own docs describe the result as an environment "treated as disposable" and "only cached to reduce the overhead of repeated invocations". Clear the cache, or let it go stale, and you get a new environment. npm is the same: it never applies a dependency's package-lock.json, only an npm-shrinkwrap.json, which almost nobody ships.

This is fine for a tool you run once to see what it does. It is not fine for a long-running process holding credentials to your management systems, started by an agent whenever it needs one. An MCP config line that says uvx is a deploy step with no build.

The Check

Find the servers your agents launch through a resolver (JSON configs; adjust the paths for your tools):

grep -h -E '"command": *"[^"]*(uvx|npx|bunx|pipx)"' ~/.claude.json .mcp.json ~/.cursor/mcp.json 2>/dev/null

Every hit is a server whose dependency set is decided on your machine, at start time, by whatever the registry says.

Install From the Lock

Pin the source to a commit, export its lock with hashes, and install from that:

git clone https://github.com/modelcontextprotocol/servers && cd servers/src/fetch
git checkout <commit>
uv export --frozen --no-dev --no-emit-project --format requirements.txt -o requirements.txt
uv venv .venv
VIRTUAL_ENV=.venv uv pip install --require-hashes -r requirements.txt
VIRTUAL_ENV=.venv uv pip install --no-deps .

uv export writes every package with its hashes, and --require-hashes refuses anything that doesn't match. Then point the MCP config's command at .venv/bin/mcp-server-fetch instead of uvx. Now an upgrade is a change you make, with a diff you can read.

If the server doesn't ship a lock, uvx --exclude-newer <date> at least stops the drift: uv treats anything uploaded after that date "as if they do not exist" (uv docs). That freezes the environment in time. It doesn't mean anyone reviewed it.

The Tradeoff

You now own the updates. A security fix in httpx doesn't reach you until you re-export and reinstall, and a frozen environment rots quietly if nobody re-locks it. The build backend (hatchling, for this server) still comes from PyPI unpinned when the last line builds the project; build the wheel once in CI if you want that closed too. All of it is more work than one line in a JSON file, and for the servers you only try out, uvx is the right tool.

It rhymes with agents installing from memory. The convenience path built for humans trying things became the default path for agents running things, and nobody decided that. Supply chain failures are a lack of agency. The maintainer here did their part. The lock just never made it onto the install path.

← all posts