Nearly half of sampled repos start an MCP server with no version. One script checks Claude Code, Cursor, Codex and OpenCode configs for it.

Most of the row is pinned. The loose ones are the ones that change under you.
TL;DR
- A line like
npx -y @scope/serverstarts whatever was published most recently, every time your agent launches it. In a sample of 346 public repos that configure a local MCP server, 46.0% start at least one this way and 18.2% ask forlatestoutright.- This month showed why it matters for more than security. A server image tagged
lateststopped loading tools in Claude Code. A popular server package moved up a major version eight days after its 1.0.0. Claude Code shipped two MCP protocol changes in three days.- Pin every local server to an exact version (by digest for containers), save its
tools/listwhen you pin it, and let a 96-line check fail CI when someone adds an unpinned line. Script and checklist below.
On 13 October someone opened issue #3431 on the Model Context Protocol repo. The complaint was small and specific. The quickstart pages that people copy MCP configs from start every server unpinned, with lines like npx -y @modelcontextprotocol/server-filesystem. Meanwhile the project's own Local Server Security guide says to "Install a specific version rather than latest."
The issue came with numbers. The author took a random sample of 2,999 public GitHub repositories that ship agent skills or MCP configs. Of the 346 that configure a local MCP server, 159 (46.0%) start at least one with no pinned version, and 63 (18.2%) ask for latest outright.
Two caveats, and the author gives both. It's a sample of what GitHub code search surfaces, not a census. And a hand check found 19 of 22 findings correct, so the scanner isn't perfect either. Still, the study's README has one more number worth knowing: popularity doesn't help. Among repos with 100 or more stars, 57.8% had an unpinned server, though that band is only 45 repos.
The quickstarts aren't the only docs that teach the habit. The config example on OpenAI's Codex MCP page is args = ["-y", "@upstash/context7-mcp"], also with no version.
What "unpinned" means on an ordinary Tuesday
An MCP server configured with npx -y some-package isn't installed. It's resolved. Each time your agent starts the server, npx asks the registry what the newest version is and runs that. uvx some-package does the same for Python. A docker run line with no tag pulls :latest.
Here's what that looked like for the package in the issue's own example. I checked the npm registry on 21 October:
-
@modelcontextprotocol/server-filesystem1.0.0 was published on 11 October and taggedlatest. - 2.0.0 was published on 19 October and took over
latest.
So an unpinned config that started 1.0.0 last week now starts a new major version, and nothing in the config file changed. The 2.0.0 package depends on the new @modelcontextprotocol/server 2.x package instead of @modelcontextprotocol/sdk 1.x, and its engines field now asks for Node 24 or later.
I ran both versions and saved their tools/list output with the MCP Inspector CLI. The 14 tools have the same names, descriptions and input schemas. The only difference is that 2.0.0 no longer sends an execution field on each tool. That's a harmless change this time. But you only know that because you compared, and an unpinned config never gives you the moment to compare.
It's a compatibility problem too
Most writing about MCP pinning is about supply-chain attacks, and those are real. This month, though, the more common failure was a server and a client upgrading out of step.

Twelve days in October. Client and server both moved, and only a pin decides when you move with them.
The clearest example is github/github-mcp-server#3453, opened on 7 October. The reporter ran the server as docker run … ghcr.io/github/github-mcp-server, with no tag, so they got latest, which was v2.0.1. From v2.0.0 the server negotiated the new MCP protocol revision 2026-07-28 but left out the resultType field that revision requires in tools/list. Claude Code connected and then loaded no tools. Codex CLI, which negotiated an earlier revision, was fine. The reporter's workaround was one line: pin ghcr.io/github/github-mcp-server:v1.14.0.
The client side kept moving too. Claude Code v2.1.299 (14 October) changed cloud-session and Remote Control MCP connectors to negotiate protocol 2026-07-28 by default, with MCP_PROTOCOL_NEGOTIATION=legacy as the opt-out. Two days later, v2.1.302 fixed "MCP servers showing no tools, or every tool call failing, when the server negotiates protocol version 2026-07-28 but omits resultType, ttlMs or cacheScope."
Neither change is wrong. Protocols evolve, and both sides are converging. The point is that an unpinned server picks its upgrade date for you, and that date won't match your client's.
Where your MCP servers live
Before you can pin anything you have to find it, and every tool keeps its MCP servers somewhere different. These are the files the check below reads:
| Tool | Project file | User file | Servers live under |
|---|---|---|---|
| Claude Code | .mcp.json |
~/.claude.json (user and local scope) |
mcpServers |
| Cursor | .cursor/mcp.json |
~/.cursor/mcp.json |
mcpServers |
| Codex |
.codex/config.toml (trusted projects) |
~/.codex/config.toml |
[mcp_servers.<name>] |
| OpenCode |
opencode.json or .jsonc
|
~/.config/opencode/opencode.json |
mcp, with command as an array |
| Gemini CLI | .gemini/settings.json |
~/.gemini/settings.json |
mcpServers |
Claude Code's local-scope servers are the easy ones to miss. They're stored in ~/.claude.json under each project's path, not in the repo, so a teammate's review never sees them. The check reads those too. It also reads .vscode/mcp.json, which uses a servers key.
The check
Save this as mcp-pin-check.py and run python3 mcp-pin-check.py from your project root. It needs Python 3.11 or later (for tomllib) and nothing else. It only reads files and prints one line per server. It never prints environment values or tokens from your configs.
What it treats as a failure:
-
No version (
npx -y pkg,uvx pkg, an image with no tag). -
latest, in any form:pkg@latest,uvx pkg@latest,image:latest. -
Ranges and tags:
@^2.1.0,@~0.9,@*,@next. A range isn't a pin. The resolver will happily pick a newer release inside it on a later day.
An image with a version tag gets a warning, not a failure, because a registry tag can be moved to new content and a digest can't. Remote servers and local scripts are listed as information, since there's no package to pin.

An excerpt from the sample configs I tested it on. Any FAIL line makes the script exit 1.
I tested it against sample configs for every location in the table, including JSONC with comments and trailing commas, npx --package=…, uvx --from pkg==…, pnpm dlx, bunx, cmd /c npx on Windows, Podman, digests and a broken JSON file. It's a heuristic, not a parser for every launcher. If your setup has a launcher it doesn't know, it says INFO rather than guessing.
Fixing what it finds
Look up the current version, read the changelog, then write that exact version into the config:
In Claude Code you can write the pinned form straight into the shared file with claude mcp add --scope project <name> -- npx -y <package>@<version>.
One gotcha from the issue thread. Some MCP packages switched from date-style versions to normal semver, and server-filesystem once published 2026.8.31, which sorts above 1.0.0 and 2.0.0 under semver. A commenter tested it with npm 11.16.0: a date-based range written before 11 October stays on the deprecated 2026.8.31. An exact pin doesn't have this problem, which is one more reason to use one.
Save the tool contract when you pin
A version pin tells you what you installed. It doesn't tell you what the server says to your model. Tool names and descriptions are instructions the model reads, and the Local Server Security guide warns that they "are not fixed at install time either."
So when you pin a server, save its tools/list next to the config. When you bump the pin, save it again and diff. This is the command I used for the comparison above:
npx --yes @modelcontextprotocol/inspector@2.11.0 --cli \
npx -y @modelcontextprotocol/server-filesystem@2.0.0 /path/to/project \
-- --method tools/list --format json > mcp-contracts/filesystem-2.0.0.json
The Inspector's own smoke-testing guide makes the same point about itself: "In CI, pin an exact version." Note that I pinned the Inspector too.
A raw byte comparison is noisy, so compare the fields that matter: tool names, descriptions and input schemas. Between filesystem 1.0.0 and 2.0.0 the raw JSON differed on all 14 tools, and every difference was the dropped execution field.
A pin is the floor, not the fence
Pinning stops your config from changing under you. It doesn't make a server safe, and the guide is clear about what it leaves out.

Each layer catches something the one above it misses.
Transitive dependencies. A version pin with npx or uvx "fixes only the top-level package; its transitive dependencies are typically resolved fresh at install time." Pinning a container image by digest freezes the whole tree. A commenter on #3431 who watches MCP servers for drift reported that the same mongodb-mcp-server@2.1.2 tarball, installed on different days, typed one parameter differently, depending on which zod minor version resolved.
Reach. A local server is an ordinary process with your user's access. A new arXiv paper catalogued 21,097 MCP servers and triaged 4,806 of them. It flagged 787 (16.4%) as having filesystem capabilities even though they weren't classified as filesystem servers. The authors call that a coarse screening signal, not a vulnerability count. Their dynamic tests then confirmed 150 vulnerable instances, and coordinated disclosure produced 13 advisories, including two CVEs in Anthropic's Git MCP server. The guide's fix is isolation: a container with only the directory the server needs, read-only where possible, and --network=none for servers that don't need the network.
Advisories. Pinning cuts both ways, and the guide says so:
"…when a security advisory lands for something you pinned, the pin is what keeps you vulnerable."
The week's example was GHSA-84m7-p3x7-pcfv (CVE-2026-59951), published on 15 October. In the MCP Python SDK, a Streamable HTTP server in stateful mode, which is the default, never reclaimed session state. Sessions didn't expire, and a client could grow server memory without bound "using ordinary short requests." It affects mcp 1.8.0 through 1.29.1 and 2.0.0 through 2.1.1. It matters if you build or host a Python MCP server that clients you don't fully trust can reach. The fix is 1.30.0 or 2.2.0, which add an idle timeout (1,800 seconds by default) and a session cap (10,000 by default). If you can't upgrade yet, the advisory suggests stateless_http=True where you don't need per-session state.
So pin, and upgrade on purpose. Pinning only works as a habit if bumping a pin is cheap and someone actually watches the advisories.
Make it a rule the agent can't skip
Your agents add MCP servers too, usually by copying the line from a README. I'd put one line in AGENTS.md, so every tool that reads it gets the rule:
## MCP servers
- Pin every local MCP server to an exact version (image digest for containers).
- Save its tools/list to mcp-contracts/ when you add or bump one.
- `python3 scripts/mcp-pin-check.py` must exit 0 before you call the change done.
Then don't rely on the line. Run the script in CI, where the runner's home directory is empty, so it checks only the files committed to the repo. That's the same pattern as the dependency allowlist in Define Done as a Command, and the same reason I gave in How Much Should You Invest in Your AGENTS.md? for moving rules out of prose and into checks.
The checklist
Run through this once per machine and once per repo:
- Delete servers you don't use. Each one is code that runs as you, and its tool definitions cost context on every turn. I measured that cost in Where Did My Claude Usage Go?
-
Run the check. Every local server pinned to an exact version. No
latest, no ranges. - Pin containers by digest, not by tag.
-
Save each server's
tools/listwhen you pin it, and diff on every bump. - Isolate the ones with reach. Use a container, one mount (read-only if you can), and no network unless the server needs it.
-
Give each server its own narrow token through its
envblock, not through variables exported in your shell profile. -
Watch advisories for what you pinned. If you ship a Python MCP server over Streamable HTTP, that means
mcp1.30.0 or later on 1.x, or 2.2.0 or later on 2.x. -
If a server shows zero tools in Claude Code, update Claude Code (v2.1.302 fixed the missing-field case) before you blame the server. Set
MCP_PROTOCOL_NEGOTIATION=legacyonly as a temporary workaround. - Add the check to CI and the one-line rule to AGENTS.md.
Threads that prompted this
A few Reddit threads circled the same problem from different directions:
- In r/mcp, someone scanned the official MCP registry with a config scanner and found 372 servers with unpinned packages. In most of them, the registry metadata looked pinned, but the launch arguments (such as
npx -poruvx --from) named the package again with no version. So the unpinned one is what gets installed. - Also in r/mcp: a heads-up about stdio servers that suddenly show zero tools in Claude Code, the same kind of mismatch as the GitHub server issue.
- In r/ClaudeCode this week: "Do you guys share env secrets with Claude Code?" It's a fair question, and a local MCP server is part of the answer. It inherits whatever environment launched it.
Sources
- MCP issue #3431 and comments (13–21 Oct 2026); study data: agent-skills-state-2026
- MCP docs: Local Server Security (read 21 Oct 2026)
- GitHub Advisory GHSA-84m7-p3x7-pcfv / CVE-2026-59951 (15 Oct 2026)
- Claude Code release notes v2.1.299 (14 Oct) and v2.1.302 (16 Oct 2026)
- github/github-mcp-server#3453 (7 Oct 2026)
- Huang et al., "My Credentials, Please": Detecting Filesystem Capability Laundering in MCP Servers at Scale, arXiv 2610.22301
- npm registry metadata for
@modelcontextprotocol/server-filesystem(read 21 Oct 2026); MCP Inspector CLI smoke-testing guide - Config locations: Claude Code MCP docs, Cursor MCP docs, Codex MCP docs, OpenCode config docs
- Previous articles: Define Done as a Command, How Much Should You Invest in Your AGENTS.md?, Where Did My Claude Usage Go?
If this was useful, subscribe to Agents in Practice for one practical note on AI coding agents a week. Helping your team adopt Claude Code or Codex safely and without the surprise bill? I consult on exactly that: andre-burgstahler-9n5.pages.dev.
This article was written with AI assistance and reviewed by the author.
Top comments (0)