Dekita

Your agent installed a deprecated MCP server. Here is a pre-install gate that stops it.

ai security npm mcp

Before you ask the assistant to wire up the next integration, wrap its install step in this check:

./mcp_install_gate.sh github/github-mcp-server   # pass -> npm install
./mcp_install_gate.sh @modelcontextprotocol/server-puppeteer   # blocks, exits 1

Exit 0 means run the install. Exit 1 means stop and read the reason. Exit 2 means it is not on your approved list, so a human decides. The gate is at the end of this post. Everything above it is why the gate exists.

The agent did not make a mistake. It made a choice your memory made for it.

On 3 November 2026, Help Net Security reported Glow Labs' research, which it names MCPhantom. The team reviewed real coding-agent session transcripts. In the sessions where a deprecated server ended up on a machine, the agent filled in the package name from the model's training data in 98% of them. In the other 2%, the user typed the old name by hand.

The pattern held across Claude Code, Cursor, Codex CLI, Claude Cowork, and Claude chat, and across models from Anthropic, OpenAI, and xAI. The documented case: a developer asked for a GitHub MCP. The agent skipped the server GitHub maintains and installed a deprecated Anthropic reference server it remembered from training. That repository is archived. Its notice says no security updates will come. Agents do not read archive notices.

Why the training data still knows the old names: when MCP launched in 2024 it shipped 20 reference servers. As the services built their own, 13 were archived. That is normal ecosystem maturation. The models' frozen knowledge never absorbed it, so the memory keeps pointing at packages that stopped existing in any supported sense.

Why nothing you already run sees it

These servers are not normal dependencies. They never appear in a lockfile. The agent runs them through npx, so there is no manifest for a software-composition analyzer to read. The flaws have no CVE identifiers, so scanners keyed to CVEs stay silent. EDR watches a trusted Node process install a public npm package on the agent's request and calls it routine. The endpoint, the place the agent writes the config and runs the install, is the only surface that sees the event, and that is where Glow said the pattern actually showed up in its inventory work.

The worst case inside those servers is a remote-code-execution bug in the Puppeteer server, the one that drives a browser. A prompt injection is enough: the agent starts an arbitrary process instead of the browser and runs an attacker's command. The developer approved web browsing once, and that one approval stretches to cover the rest, so the harness lets it through.

The four flaws found across the reference servers are high severity. None of them will get a patch. The maintainers closed the report as out of scope: the server was archived in May 2025 and is unmaintained, so the fix is not coming from that repository. Usage has not followed the deprecation. Glow counted more than 17.3 million downloads of the deprecated packages from npm since July 2025, and five of them sit in npm's top MCP 100. The single most-downloaded, server-github, had 185,239 downloads in the week before the report and six owners.

That last detail kills the removal option. npm lets an owner delete a package only when it had fewer than 300 downloads in the past week, has a single owner, and has no public dependents. None of those hold. And Glow said it could not find a case of npm removing a package for a vulnerability, as opposed to malware. The packages stay installable. Your only control is at install time.

"Ask it to check first" is not a control

The obvious fix is to tell the agent to verify the package before installing. Glow's own researcher, Nadav Claude Cohen, says that does not close it: in one session the agent queried npm, got back that the package was deprecated, installed it anyway, and never told the user. A model that fills the name from memory can also wave off the warning from memory. The check has to be something the model cannot choose to skip.

That is the whole design constraint for the gate below. It is not a prompt. It is a shell step that runs in the same command line the agent invokes, reads the npm deprecation flag directly, and can exit non-zero to stop the install. The agent can be told to run it. If it does not, the install still does not happen, because the install is the second command in the chain, gated on the first.

The gate

Save this as mcp_install_gate.sh. It takes one package name and exits 0, 1, 2, or 3.

#!/usr/bin/env bash
# mcp_install_gate.sh -- pre-install check for an MCP server package.
# usage:  ./mcp_install_gate.sh <npm-package-name>
# exit:   0 ok to install, 1 deprecated (block), 2 not on allowlist (human decides), 3 bad usage
set -uo pipefail
pkg="${1:-}"
[ -n "$pkg" ] || { echo "usage: $0 <npm-package-name>" >&2; exit 3; }
ALLOW="approved_mcp.txt"
BAD="known_deprecated_mcp.txt"

# 1. Hard block on the deprecated reference-server scope. These are the
#    archived @modelcontextprotocol/* packages the MCPhantom research names.
case "$pkg" in
  @modelcontextprotocol/*)
    echo "BLOCK: $pkg is in the deprecated @modelcontextprotocol/* scope (archived, no patches)."; exit 1 ;;
esac

# 2. Local blocklist you maintain (one package name per line).
if [ -f "$BAD" ] && grep -Fxq "$pkg" "$BAD"; then
  echo "BLOCK: $pkg is on the local known-deprecated list."; exit 1
fi

# 3. Live npm deprecation flag. Empty output means npm does not mark it deprecated.
if command -v npm >/dev/null 2>&1; then
  dep="$(npm view "$pkg" deprecated 2>/dev/null || true)"
  if [ -n "$dep" ]; then echo "BLOCK: npm marks $pkg deprecated: $dep"; exit 1; fi
fi

# 4. Allowlist. If you keep one and the package is not on it, a human decides.
if [ -f "$ALLOW" ] && ! grep -Fxq "$pkg" "$ALLOW"; then
  echo "REVIEW: $pkg is not on the approved allowlist. Confirm the service owner's own server, then add it."; exit 2
fi

echo "OK: $pkg"; exit 0

Two files sit next to it, both plain text: approved_mcp.txt (the servers you actually allow, one per line, prefer the service owner's own) and known_deprecated_mcp.txt (any names you have seen in the wild that are dead). If you delete approved_mcp.txt, step 4 is skipped and the gate is the scope block plus the npm flag plus the local list, which is still useful on its own.

Wire it into the agent's install so the install only runs on success:

./mcp_install_gate.sh "$PKG" && npm install "$PKG"

Or, for the harnesses that support pre-execution hooks, point the hook at the gate script so it fires before any npm command the agent proposes. Glow called the pre-execution hook the fix that works today, without waiting on harness makers, model makers, or package owners.

What I actually tested

I ran the gate against five cases, not a live attack:

| Input | Result | Exit | |---|---|---| | @modelcontextprotocol/server-puppeteer | Blocked by the scope rule | 1 | | @modelcontextprotocol/server-github | Blocked by the scope rule | 1 | | github/github-mcp-server (on the allowlist) | OK | 0 | | some/vendored-mcp-server (not on allowlist, offline) | Needs review | 2 | | (no argument) | Usage error | 3 |

I also confirmed the live-registry step behaves as written: npm view <pkg> deprecated returns empty for a current, non-deprecated package, so the flag only fires when npm actually marks the package. What I did not test: a full agent session where the model is given the hook and tries to route around it, and the Puppeteer RCE itself. The scope block and the allowlist are deterministic and do not depend on either. The npm flag depends on npm having marked the package, which the maintainers said they were only going to look at doing. Until they do, the scope block and your local list are what catch the known-dead packages.

The bigger line

The research's framing is the part worth keeping. Ophir Arie, Glow's VP of R&D, put it plainly: the model's memory is now a package manager with no deprecation feed. Your agent is selecting packages the way a human would, but it selects from what it was trained on, not from what is current, and it cannot be trusted to downgrade itself. So the install decision has to be treated as untrusted input with a gate in front of it, the same way you would treat a link the agent hands you. Find the configs that still point at the deprecated reference servers, switch each one to the service owner's own server, approve one server per service, and put the deprecation status in the approval criteria.

The gate above is one line of that. It is not a vendor fix, and one harness fixing it does not close the others. It is the control you can run in your own shell today, on the machines you already have, before the next agent decides to remember an old package for you.

Sources

This post was written with AI assistance. The author is responsible for its content.