Last week someone asked their coding agent to "add a GitHub MCP server."
The agent skipped GitHub's own maintained server and installed @modelcontextprotocol/server-github — a package whose last release was April 8, 2025. The repository is archived. The README says no security updates are coming. npm has it flagged deprecated.
The agent didn't lie. It didn't hallucinate. It remembered correctly. That was the right package, in the world it was trained on.
Glow Labs named this pattern MCPhantom on November 2, and Help Net Security covered it the next day. If you only read the headlines, you filed it under "supply chain thing, someone else's problem."
I did too. Then I actually pulled the packages apart.
The security story is the less interesting half.
What I measured
I queried the npm registry and downloads API directly on November 5, 2026, for every archived reference server. Downloads are for the fixed window Oct 27 – Nov 2, 2026 so you can reproduce it.
| Package | Last release | Age | Downloads that week |
|---|---|---|---|
server-github | 2025-04-08 | 575 d | 200,397 |
server-slack | 2025-04-25 | 559 d | 176,607 |
server-postgres | 2024-12-04 | 700 d | 158,147 |
server-brave-search | 2024-12-04 | 700 d | 31,665 |
server-puppeteer | 2025-05-12 | 542 d | 30,673 |
server-google-maps | 2024-12-04 | 700 d | 7,442 |
server-gdrive | 2025-01-14 | 660 d | 6,049 |
server-gitlab | 2025-04-25 | 559 d | 5,398 |
server-redis | 2025-04-25 | 559 d | 3,964 |
server-aws-kb-retrieval | 2025-01-14 | 660 d | 189 |
create-server | 2024-11-25 | 710 d | 173 |
server-everart | 2024-12-04 | 700 d | 134 |
| Total | 620,838 |
All twelve carry npm's deprecated flag. All twelve still install. npx -y does not stop for a deprecation warning.
For contrast, the three reference servers that are still maintained — filesystem, memory, everything — were all republished on October 28, 2026. Eight days before I looked. The split is clean and it is not subtle.
So: 620k weekly installs of code that is, in the worst case, 710 days past its last commit. That's the number that made the rounds.
Here's the part that didn't.
Finding 1: these servers are protocol-frozen
Nine of the twelve pin the MCP SDK at an exact version. Not a caret range — an exact pin:
"dependencies": { "@modelcontextprotocol/sdk": "1.0.1" } I pulled both tarballs and read the constants.
@modelcontextprotocol/sdk@1.0.1:
export const LATEST_PROTOCOL_VERSION = "2024-11-05"; export const SUPPORTED_PROTOCOL_VERSIONS = [ LATEST_PROTOCOL_VERSION, "2024-10-07", ]; @modelcontextprotocol/sdk@1.32.2 (current, published 2026-10-23):
exports.LATEST_PROTOCOL_VERSION = '2025-11-25'; exports.SUPPORTED_PROTOCOL_VERSIONS = [ '2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07' ]; Read that as a capability statement. An archived server can negotiate 2024-11-05 or 2024-10-07. Full stop. It cannot reach:
- 2025-03-26 — the OAuth resource-indicator work and structured tool output
- 2025-06-18 — elicitation, tool annotations, structured content
- 2025-11-25 — the current revision, including long-running tasks
Your client is backward compatible, so the handshake succeeds and everything looks fine. You get a green checkmark and a server that silently cannot do three revisions' worth of things. When you wonder why outputSchema never seems to work, or why elicitation never fires, or why your annotations are ignored — this is why.
The pin is the mechanism. A caret range would have floated forward. An exact pin means the protocol ceiling is welded shut at build time, and no amount of npx -y freshness will move it. You are always fetching the newest version of a package that stopped moving in 2024.
That reframes the whole thing for me. This isn't only "you might get popped." It's your agent's tool surface is quietly 3 protocol revisions behind, and the failure mode is silence, not an error.
Finding 2: six names that are live on PyPI and unclaimed on npm
This one I stumbled into. I went looking for the npm packages for the maintained Python reference servers, assuming they'd be under the same scope.
@modelcontextprotocol/server-git HTTP 404 @modelcontextprotocol/server-fetch HTTP 404 @modelcontextprotocol/server-time HTTP 404 @modelcontextprotocol/server-sequentialthinking HTTP 404 @modelcontextprotocol/server-sqlite HTTP 404 @modelcontextprotocol/server-sentry HTTP 404 All six 404. Now PyPI:
mcp-server-git EXISTS latest 2026.10.28 mcp-server-fetch EXISTS latest 2026.10.28 mcp-server-time EXISTS latest 2026.10.28 mcp-server-sqlite EXISTS latest 2025.4.25 The servers are real, current, and maintained — in the other registry. The npm names are unclaimed.
Now put yourself in the model's position. It has seen mcp-server-git in a thousand READMEs. It has seen npx -y @modelcontextprotocol/server-* in a thousand configs. Composing those two patterns is the single most natural completion in the world, and it produces a name that resolves to nothing.
A 404 on a package name an agent will reliably generate is not a hygiene issue. It's an unregistered command-execution endpoint with a predictable trigger. This is slopsquatting, except the hallucination isn't random — it's a systematic cross-registry confusion, which makes it far more predictable than the usual made-up-package-name lottery.
To be clear about what I did and didn't do: I checked name availability. I did not register anything, and I'm not going to publish a how-to. The point is that the check is one HTTP request and the result is currently "unclaimed."
Why nothing in your stack noticed
Walk your controls:
- SCA / Dependabot — reads a manifest. These servers arrive via
npxand never enterpackage.json. There is nothing to scan. - CVE feeds — Glow's four high-severity flaws were closed as out-of-scope because the repo is archived. No CVEs were issued. A feed with no entry is a feed that says "clean."
- EDR — sees a trusted Node process installing a public npm package because the developer asked it to. That is the definition of normal.
-
npm audit— audits your dependency tree. This isn't in it. - Asking the agent to check first — Glow documented a session where the agent queried npm, received "This package is deprecated," installed it anyway, and never mentioned it to the user.
Every layer is working as designed. The thing that changed is that dependency selection moved into the model's weights, and weights don't subscribe to a deprecation feed.
Ophir Arie from Glow put it better than I can: "The model's memory is now a package manager with no deprecation feed."
Check your own machine in 30 seconds
I wrote a zero-dependency scanner. It reads your actual configs, extracts package names from command/args, and asks the live registry — so the verdict is never a hardcoded list that goes stale, which would be ironic.
It handles Claude Code, Cursor, VS Code, Codex (TOML), Gemini CLI and Claude Desktop, and it distinguishes four states: deprecated, stale, phantom, ok.
# one-liner sanity check, no install grep -HnoE '@modelcontextprotocol/(server-(github|gitlab|postgres|slack|puppeteer|brave-search|google-maps|gdrive|redis|aws-kb-retrieval|everart)|create-server)' \ .mcp.json .cursor/mcp.json .vscode/mcp.json \ ~/.claude.json ~/.cursor/mcp.json ~/.codex/config.toml \ "$HOME/Library/Application Support/Claude/claude_desktop_config.json" 2>/dev/null If that prints anything, you have at least one server that will never be patched.
The full scanner is one zero-dependency file (mcp-debt.mjs). Run it with:
node mcp-debt.mjs # scans known config locations node mcp-debt.mjs --json # for CI; exit 1 on findings Real output from a config I built to test it:
mcp-debt: 6 server entries scanned [!!] @modelcontextprotocol/server-git (phantom-bait) config: .mcp.json server: "git" name does not exist on npm — anyone can register it [XX] @modelcontextprotocol/server-postgres (deprecated) config: .mcp.json server: "db" last release: 2024-12-04 (700 days ago) pins SDK: 1.0.1 npm says: Package no longer supported. -> crystaldba/postgres-mcp or bytebase/dbhub [ok] @playwright/mcp (ok) last release: 2026-10-31 (4 days ago) Then stop it happening again
Detecting is cleanup. The fix that works today, without waiting for Anthropic, OpenAI or npm, is a pre-execution hook. Most harnesses already support them.
{ "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "node ~/bin/mcp-debt.mjs --hook" }] }] } } Exit code 2 blocks the call and hands the reason back to the agent:
[mcp-debt] BLOCKED @modelcontextprotocol/server-github (deprecated) use instead: github/github-mcp-server (official) The agent then picks the maintained server, because now something in the loop actually knows what year it is.
Migration targets worth knowing:
| Dead | Use instead |
|---|---|
server-github | github/github-mcp-server |
server-puppeteer | microsoft/playwright-mcp |
server-slack | https://mcp.slack.com/mcp |
server-gitlab | https://gitlab.com/api/v4/mcp |
server-redis | redis/mcp-redis |
server-postgres | crystaldba/postgres-mcp or bytebase/dbhub |
server-aws-kb-retrieval | awslabs/mcp |
One trap: server-filesystem is fine. It's maintained, republished October 28. Don't rip it out because it shares a scope with the dead ones. And git exists as a maintained Python server — the archived npm-shaped copy is the problem, not git integration.
The actual lesson
We spent fifteen years building infrastructure to answer "is this dependency safe?" — lockfiles, CVE feeds, SCA, SBOMs, audit logs. Every one of them assumes a human, or at least a deterministic resolver, chose the package.
That assumption is now false, and we didn't deprecate it. We just started letting a stochastic process trained on a frozen snapshot of the ecosystem make supply-chain decisions, and pointed all our tooling at the artifacts that process no longer produces.
The model isn't broken. It's answering the question it was trained to answer. The bug is that we wired its memory into exec() and called it a package manager.
If your agent can install things, "which package" is now a security control, and it needs the same treatment as any other: an allowlist, a runtime check, and a hook that can say no.
And the argument I keep going back and forth on: should a harness refuse to launch a deprecated MCP server by default? Glow's researcher notes it only works if every harness does it — one holdout keeps the hole open. Is that a reason to do it anyway, or a reason it's doomed?
Measurements taken 2026-11-05 against registry.npmjs.org and api.npmjs.org. Download window fixed at 2026-10-27..2026-11-02. Protocol constants read from the published @modelcontextprotocol/sdk 1.0.1 and 1.32.2 tarballs. MCPhantom was discovered and named by Glow Labs; the protocol-ceiling and cross-registry-phantom measurements above are my own.

