Your Coding Agent Installs MCP Servers From Its Memory

You ask your agent to set up an MCP server, it picks a package name from its training data, and the name points at a reference server that was archived over a year ago with an open remote-code-execution hole. No dependency scanner sees it, because the server never lands in a manifest, and there is no CVE to match.

Viral RuparelNo. 85 of 8511 min read

A team I was helping wanted their coding agent to drive a browser, so someone typed a one-line request: add an MCP server for Puppeteer. The agent did not look anything up. It wrote an npx command from memory, the package installed, and the server was live inside a minute. The name it chose pointed at a reference server that had been archived more than a year earlier, carrying an open remote-code-execution hole that nobody was going to fix. No one on the team had picked that package. The model had, out of training data, and the install looked exactly like every other install the agent had ever run.

This is not a one-off. A review of real coding sessions by Glow Labs, reported in the MCPhantom writeup on Help Net Security, found that when an agent sets up an MCP server, the package name comes from the model's training data in 98 percent of cases, and the names it reaches for are often deprecated reference servers with high-severity bugs and no patches. The deprecated packages they tracked have been pulled from npm more than seventeen million times since mid-2025, and five of them sit in npm's top hundred MCP downloads. The most serious, a Puppeteer server, lets an attacker use prompt injection to launch arbitrary processes, even though the developer only ever approved web browsing. That is the same trust-boundary failure behind prompt injection in tool-using agents, except here the vulnerable surface arrived because your agent chose it for you.

Before, the agent writes an npx command from training memory and a deprecated unpatched MCP server installs and runs with no check in the path; after, an install-time gate matches the request against a pinned allowlist and a deprecation lookup, admitting the official server and refusing the archived one with a reason.
Figure. A hallucinated package install, then the same install behind a pinned allowlist gate

The model's memory is a package manager with no deprecation feed

Start with why this happens at all. A model's knowledge of which package to install is frozen at training time. The registry is not. Between the training cutoff and the day your agent runs, packages get deprecated, repositories get archived, maintainers hand a service over to its official vendor server, and the old reference implementation stops receiving fixes. None of that reaches the weights. The model keeps recommending the name it learned, with the confidence of something it has seen a thousand times, long after that name became a liability. Glow's Ophir Arie put the shape of it well: "The model's memory is now a package manager with no deprecation feed."

The failure rides on top of a measured tendency called package hallucination, catalogued in Socket's writeup on slopsquatting. The 2025 USENIX study it cites found that roughly a fifth of packages recommended by code models did not exist, and 43 percent of the invented names came back in every one of ten repeated runs of the same prompt. Newer models have narrowed that rate to the single digits, but repeatability is the part that bites: a wrong name the model emits consistently is a wrong name an attacker can register, or an abandoned package the agent will keep choosing. Slopsquatting is the variant where the name does not exist yet. The MCP case is the variant where the name does exist and points at code nobody maintains. Same root cause in the model's memory, different payload on the disk.

What makes MCP servers worse than an ordinary hallucinated import is that they are not libraries you call. They are processes your agent talks to, often with filesystem or network reach, and the install and the execution are frequently the same step. An npx @some/server does not land a dependency you can review later; it fetches and runs. The window between the model's choice and a live, reachable server is measured in seconds, which is why the check cannot wait until code review.

Why your scanners and CVE feeds miss it

If you are relying on the controls you already have, this slips straight through all of them, and it is worth being precise about why.

Software composition analysis reads manifests and lockfiles. A server launched with npx or uvx never enters either, so there is no line for the scanner to flag and no integrity hash to diff. The server exists only as a process on the endpoint. Your vulnerability feed is no help either, because these archived reference servers have no CVEs; the maintainers closed the reports as out of scope once the repository was archived, so there is nothing for a matcher to match. Endpoint detection sees a trusted Node process installing a public npm package at the user's request, which is indistinguishable from every legitimate install, and software inventory does not register an MCP server as an application at all. Every layer you would expect to catch this is looking at the wrong artifact.

The tempting fix is to make the agent check before it installs. That helps, and it is not enough on its own. In the sessions Glow reviewed, an agent saw npm's own deprecation notice, installed the package anyway, and never told the user. This is the recurring lesson from giving a coding agent authority over its own guardrails: the model is the component making the risky choice, so you cannot also make it the component that vetoes the choice. The veto has to live outside the agent, on the install path, where it runs deterministically and the agent cannot talk it out of firing.

Gate one: an allowlist of the servers your agent may install

The durable control is an allowlist of MCP servers your team has actually reviewed, with the exact package coordinates pinned, so the agent's freedom is narrowed to a set someone vetted. This is the same move as pinning the bytes behind a tool so that an approved MCP tool cannot quietly become a different one: you decide what is allowed once, in a file under review, instead of trusting a name at runtime.

Keep it boring and declarative. One entry per service, the official package, a pinned version, and the egress the server is allowed to reach.

# mcp-allowlist.yaml  --  the only MCP servers this agent may install
servers:
  github:
    package: "@github/mcp-server"      # the vendor's official server
    version: "4.2.1"                    # pinned, bumped by review
    runner: npx
    egress: ["api.github.com"]
  filesystem:
    package: "@modelcontextprotocol/server-filesystem"
    version: "2026.10.0"
    runner: npx
    egress: []                         # local only, no network
  playwright:
    package: "@playwright/mcp"
    version: "0.9.3"
    runner: npx
    egress: ["localhost"]
# Anything not listed here is denied by default, including older
# reference servers the model may still name from training data.

The list is short on purpose. The point is not to catalogue every server in existence; it is to make the set of installable servers a decision your team owns, approved one service at a time, rather than a decision the model makes from stale memory. A new server is a pull request that names the service, the official package, and the reason, reviewed the same way you review any dependency you take on. Default-deny is what gives the gate its teeth: an archived server the model loves is simply absent from the file, so it never has a path to run.

Gate two: check it at the install, not after

The allowlist is a document until something enforces it. The enforcement point is the moment of install, and the check is small: resolve the requested server against the pinned list, and as a second line, confirm the package is not deprecated on the registry before it is allowed through. The deprecation lookup catches the case you did not anticipate, a server you have not yet added that the agent tries to pull in.

# install_gate.py  --  the deterministic veto that runs outside the agent
import subprocess
import yaml

ALLOW = yaml.safe_load(open("mcp-allowlist.yaml"))["servers"]

def npm_is_deprecated(package: str, version: str) -> bool:
    # Ask the registry directly; a non-empty deprecated field is the signal.
    out = subprocess.run(
        ["npm", "view", f"{package}@{version}", "deprecated", "--json"],
        capture_output=True, text=True, timeout=20,
    )
    return out.returncode == 0 and out.stdout.strip() not in ("", "undefined")

def admit(service: str, package: str, version: str) -> dict:
    entry = ALLOW.get(service)
    if entry is None:
        raise PermissionError(f"no approved MCP server for '{service}'")
    if (package, version) != (entry["package"], entry["version"]):
        raise PermissionError(
            f"'{package}@{version}' is not the pinned server for '{service}'; "
            f"approved is '{entry['package']}@{entry['version']}'"
        )
    if npm_is_deprecated(package, version):
        raise PermissionError(f"'{package}@{version}' is deprecated on npm; refusing")
    return entry

Two refusals, each with a reason a human can act on. The allowlist mismatch catches the archived reference server the model reached for, because it is not the pinned coordinate for that service. The deprecation check is defense in depth for the gap in your own list. Neither asks the model whether the install is safe, which is the entire point. The guard is a function of the registry and your reviewed file, not of the agent's judgment. If you also want to bind the exact bytes rather than trust the version tag, carry an integrity hash in the allowlist and verify it after download, the same discipline a supply-chain gate on a skill applies to its contents.

Gate three: a pre-execution hook the agent cannot route around

A gate helps only if every install goes through it. Most agent harnesses expose a pre-execution hook, a point where you see the command the agent is about to run and can refuse it. That is where this belongs, because it closes the path around the gate: the agent cannot install a server except by issuing a command, and the hook sees every command. Glow's own recommendation lands here, and the spec is moving the same way, with the MCP feature lifecycle and deprecation policy adding a formal registry of deprecated features and a minimum twelve-month removal window, which gives a hook something authoritative to read.

#!/usr/bin/env bash
# pre-exec-hook.sh  --  the harness runs this before any shell command.
# Exit non-zero to block. Only install commands are inspected; the rest pass.
cmd="$1"

case "$cmd" in
  *npx*|*uvx*|*"npm install"*|*"pip install"*)
    # Pull the package coordinate out of the command the agent wrote.
    pkg=$(printf '%s\n' "$cmd" | grep -oE '(@[a-z0-9-]+/)?[a-z0-9-]+@[0-9.]+' | head -1)
    svc="${MCP_TARGET_SERVICE:-unknown}"   # set by the install step's context
    if ! python3 install_gate.py "$svc" "${pkg%@*}" "${pkg##*@}"; then
      echo "BLOCKED: '$pkg' is not an approved MCP server. See mcp-allowlist.yaml." >&2
      exit 1
    fi
    ;;
esac
exit 0

The hook runs as the user, not as the model, so it is a control plane the agent cannot edit out of its own way, the same separation that keeps a coding agent from switching off the monitor that watches it. Because the hook already pays for pre-execution inspection, this is also the right place to enforce the server's declared egress once it is running: a browsing server has no business opening a socket to anything but the hosts in its allowlist entry, and a default-deny egress boundary like the one behind sandboxed code execution with least privilege is what contains the Puppeteer-style bug when, not if, one slips past the install check.

The parts that bite

  • A lookup the model performs is not a gate. If the deprecation check runs as a tool the agent calls and interprets, the agent can install anyway, exactly as it did in the field. The check has to run in the hook, outside the agent, with a hard exit code.
  • Version tags move, hashes do not. Pinning a version stops the archived server, but a maintainer can republish a tag with new contents. If an install is high-value, carry a content hash in the allowlist and verify after download, so a silent swap becomes a loud mismatch instead of a quiet update.
  • The allowlist rots if nobody owns it. An out-of-date list is as dangerous as no list, because people route around a gate that blocks legitimate work. Put the file under the same review and update cadence as your lockfiles, and make adding a server a fast, normal pull request so the gate does not become the thing people disable.
  • Deprecated is not the same as removed. npm rarely removes a package, and a deprecated one still installs through npx. Do not assume the registry will take the dangerous server down for you; the download count on these servers is proof it will not.
  • Transitive MCP installs. An approved server can itself spawn or recommend another server. The hook sees shell commands, so a server that reaches outside that path needs the same egress containment as any other untrusted process, not just an install check.

The takeaway

The agent supply chain inherited the old software supply chain's problem and added a new twist: the component choosing your dependencies is a frozen snapshot of the world as it was at training time, and it will keep naming packages that have since been archived, deprecated, or replaced, with no feed telling it they went bad. Your scanners cannot see the result because it never lands in a manifest, your CVE feed has nothing to match, and the model that made the choice is the last thing you should ask to review it. The fix is not a smarter model and it is not a one-time audit. It is an allowlist of the servers you reviewed, an install-time check that refuses anything deprecated or unpinned, and a pre-execution hook that gives the check a path it cannot be routed around. Three small, deterministic pieces turn "the agent installed a server from memory and we hoped" into "nothing runs unless we approved it."

If your coding agents are installing MCP servers on their own and nobody is checking what actually lands on the machine, the gap is an install gate, not a weaker model. Book a consultation call and we will put a real one in front of them.

ShareXLinkedInviralruparel.com/blog/mcp-server-package-hallucination-install-gate

Questions this essay answers

01Why can't software composition analysis catch a deprecated MCP server?

Because the server never enters a dependency manifest. Your agent runs an npx or uvx command that fetches and executes the package in one step, so there is no package.json or requirements entry for a scanner to read, and no lockfile line to diff. The flaws in these archived reference servers also have no CVEs assigned, so even a scanner that could see them would have nothing to match against. The server shows up only as a running process on the endpoint, which is why the install path, not the manifest, is where the check has to live.

02Isn't this the same thing as slopsquatting?

It shares a root cause and differs in the payload. Slopsquatting exploits a name the model invents that does not exist yet, so an attacker registers it and waits. Package hallucination for MCP servers is the model naming a package that does exist but has been abandoned, archived, or superseded by the service owner's official server. One is a name waiting to be weaponized, the other is real code nobody patches. The defense overlaps: both come from the model's frozen memory choosing a package, so both are stopped by gating the agent's package choice rather than trusting it.

03Won't a newer model just stop recommending the old packages?

Not reliably, and not soon. The package name lives in the model's weights, frozen at training time, while the registry keeps moving: packages get deprecated, repositories get archived, and official servers replace reference ones. Retraining to update that knowledge is expensive and happens on its own schedule, not yours. Measured hallucination rates have fallen across newer models, but they have not reached zero, and a single stable wrong name is enough to put an unpatched server on a machine. The control you own is the gate, not the model's memory.

04Can I just tell the agent to check npm before installing?

A lookup before install is necessary but not sufficient on its own. In the sessions Glow reviewed, an agent saw npm's deprecation notice, installed the package anyway, and did not mention it to the user. The model is the thing making the risky choice, so asking it to also be the thing that vetoes the choice puts the guard inside the system it is meant to guard. The veto has to run outside the agent, as a deterministic hook on the install command that cannot be reasoned past.

Working on this in production?I help teams make agents like this reliable. Thirty minutes, no pitch.
Viral Ruparel

Generative AI consultant and full-stack architect. Ex-IBM, former Director of Engineering at a YC W20 startup. Writes about what breaks when agents meet reality.