On November 2 the Glow Labs research team at Glow Security published a finding it named MCPhantom: coding agents asked to set up an MCP server keep installing deprecated ones, and the package name comes from the model's training data, not from anything the agent looked up. The servers in question are archived. Four of them carry high-severity vulnerabilities, one of those is an unpatched remote code execution bug, and the maintainers have said in writing that no fix is coming. Help Net Security covered it the next day.
I read the whole report twice, because the pattern hits the exact layer I spend my days in: the config files that decide what your agent is allowed to run. Here is what it says and what I am changing in my own setup because of it.
What the sessions actually showed
Glow Labs reviewed real coding-session transcripts where a deprecated MCP server got installed or suggested. In 98 percent of those sessions, the agent filled in the package name from memory. In the other 2 percent, the user asked for the old package by name and the agent obliged. In the GitHub example, the agent passed over the MCP server GitHub maintains itself and picked @modelcontextprotocol/server-github, an Anthropic reference server whose repository was archived in May 2025.
The pattern showed up in every harness in the data: Claude Code, Cursor, Codex CLI, Claude Cowork and Claude chat, on models from Anthropic, OpenAI and xAI. So no single vendor ships a fix for this. One caveat the report is honest about, and I want to keep it that way: the study only looked at sessions where a deprecated install happened. It says nothing about how often agents choose correctly overall. The number that matters is that when the wrong pick happened, it was almost never the user's idea.
Why the packages are still downloadable
When Anthropic launched MCP in 2024 it published 20 reference servers as examples. The services built servers of their own, and 13 of the 20 got archived. Archived means the repository says no security updates or bug fixes will be provided. Agents do not read archive notices.
Usage did not follow the deprecation. Glow Labs counted 17,352,267 npm downloads across the deprecated set since July 10, 2025, and five of the packages sit in npm's top MCP 100. server-github alone accounts for 5,266,085. The packages also cannot realistically be pulled: npm only lets owners delete a package with fewer than 300 downloads a week, a single owner and no public dependents, and Glow's Nadav Claude Cohen notes server-github had 185,239 downloads in a week and six owners. Deprecation marks do not stop npx either, it prints a warning and installs anyway.
The Puppeteer flaw and the permission that stretched
The most serious of the four high-severity findings is an RCE in the Puppeteer server. One prompt injection is enough: instead of opening a browser, the server starts an arbitrary process and runs a command the attacker chose. The reason your harness does not catch it is the part I keep re-reading. The developer approved web browsing. The Puppeteer server is the thing that browses, so the commands it runs inherit that approval. One yes covers all of it.
Glow Labs reported the flaw. The maintainers closed it as out of scope, on the grounds that the repository was archived in May 2025 and unmaintained, so no fix will come from it. They said they would look at marking the npm package deprecated. No exploitation has been observed so far. With no patch coming, the door stays open.
Why the tools you already run stay quiet
Glow Labs calls these servers dependencies that never appear in a dependency file, and its breakdown of why every standard control misses them is the most useful part of the report:
- EDR sees a trusted Node process install a public npm package an agent requested. That looks routine.
- SCA tools read manifests. An
npx-launched server sits in no manifest, and these flaws have no CVE numbers. - Software inventory registers installed applications. A server inside a Node process is not one.
- The agent harness gates what the agent may run. It has no way to know the package is deprecated.
The only place the whole thing is visible is the endpoint where the config got written and the install ran.
Asking the agent to check first does not work
My first instinct was "just tell the agent to verify the package before installing." The reporting already contains the counterexample. Cohen told Help Net Security: "In one session the agent checked the package on npm and got back 'This package is deprecated.' It installed the package anyway and never told the user." A polite instruction is not a control. A control is something that fails closed when the model does not cooperate.
Take the choice away in the config
The fix that works today is boring: the MCP config block names the exact server, and the agent never gets to pick a package. Glow's own recommendation for security teams is the same idea at fleet scale, approve one MCP server per service, make it the standard, and add npm deprecation status to the approval criteria.
The shape to avoid, where the agent freehands a remembered package name:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
}
}
}
The shape to prefer: the service owner's own server, named explicitly, reviewed by a human, with a comment in the repo pointing at where the decision is recorded. GitHub maintains its own MCP server, and that is the one the config should name. When I approve a server now, I check it the way I would check any other dependency:
npm view <package> deprecated
npm view <package> maintainers
If the first command prints a deprecation notice, the server is out, whatever the agent remembers about it.
A hook catches it before the install
Cohen sees four possible sources of a fix: harness makers refusing deprecated servers, model makers updating what the models know, package owners patching, or pre-execution hooks, and his view is that only the hooks "work today, without waiting for the harness makers, the model makers or the package owners." Claude Code already supports PreToolUse hooks, so a deny rule for the archived namespace is a few lines in settings:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' | grep -q '@modelcontextprotocol/server-' && exit 2 || exit 0"
}
]
}
]
}
}
Exit 2 blocks the call and shows the reason to the agent, so the failed install is at least visible in the transcript instead of silent.
What I am checking tonight
- Grep every MCP config on my machines for
@modelcontextprotocol/server-:~/.claude.json, project.mcp.json,~/.cursor/mcp.json. - Run
npm view <package> deprecatedon every hit. - Replace each one with the server the service itself maintains, where one exists.
- Remove any Puppeteer server first, that one carries the unpatched RCE.
- Write the approved server per service into the config, so the next "set up a GitHub MCP" request has exactly one right answer.
None of this is new discipline, it is the same rule I already apply to lockfile dependencies, applied to the one dependency tree that never had a lockfile. I wrote about the official MCP servers shipping vulnerability fixes in v2.1.0, and the marketplace malware that posed as Claude Code in Cursor back in October. MCPhantom is the third leg of the same stool: the supply chain your agent builds for you, one remembered package name at a time, with nobody left maintaining it.
Top comments (1)
The honest caveat — the study only looked at sessions where the wrong pick already happened — is what makes the 98% number usable instead of scary. Thanks for keeping it.
On the hook: the @modelcontextprotocol/server-* grep is a good tourniquet, but I'd generalize it into the registry check you already run by hand. A deny-list for one archived namespace is out of date the moment the next deprecated-but-dangerous server appears under a different name, and it says nothing about a typosquatted lookalike. The durable version is a pre-install hook that runs npm view deprecated on whatever package the agent proposes, and fails closed when the registry lookup itself fails — a polite instruction isn't a control, and a skipped lookup isn't one either. The nice property of the deprecation signal is that it works without maintainer cooperation, which your writeup shows is exactly the thing you can't count on: archived repo, six owners, an RCE that stays.
One gap to name honestly: the registry check only holds when the package resolves through npm. A tarball URL or a git+https dependency walks around it, which is why the hook has to gate the install action, not scan for known-bad names. Are you planning to keep the namespace rule as the stopgap and wire the registry check into the PreToolUse hook permanently?