DEV Community

ereb-blade
ereb-blade

Posted on

My MCP token-cost auditor caught a live npx/PyPI namesquat

The tool I was actually building

I've been building mcp-tollbooth, a small CLI that scans your local MCP config (Claude Desktop, Claude Code, Cursor, VS Code) and answers a boring but real question: how many tokens are your MCP servers silently costing you before your agent does anything useful?

A handful of servers can eat 50,000+ tokens of context budget before you've typed a single message, and most people have no idea which of their installed servers are the expensive ones — or that they've installed the same capability twice under two different names.

That's it. That's the pitch. Not a security scanner — there's already tooling for that. Just the token-budget problem, clearly labeled as an estimate, not a measurement.

While building the "known package" lookup table, though, I ran into something I wasn't looking for.

The lookup table

To estimate token cost, tollbooth keeps a small table of known MCP servers and their approximate tool-schema cost. One of the reference servers from the official modelcontextprotocol/servers repo is fetch — a Python tool distributed on PyPI, normally invoked like this:

{
  "mcpServers": {
    "fetch": {
      "command": "uvx",
      "args": ["mcp-server-fetch"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

uvx pulls mcp-server-fetch from PyPI. Straightforward.

But MCP configs are loose about this. Some clients, some copy-pasted setup guides, and some just-plain-typos use npx for everything, regardless of which registry a given server actually lives in:

{
  "mcpServers": {
    "fetch": {
      "command": "npx",
      "args": ["-y", "mcp-server-fetch"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That second block looks almost identical. It is not the same package.

What's actually on npm

Out of curiosity, I checked what mcp-server-fetch resolves to on the npm registry — a completely different namespace from PyPI, but the exact same string.

Someone already owns it. It's published as a "security research canary":

maintainer: node-canaries
repository: theinfosecguy/npx-canary
Enter fullscreen mode Exit fullscreen mode

Its own README is upfront about what it is:

This package is part of an authorized bug bounty research project investigating npx confusion — a supply chain attack vector where unclaimed npm package names matching common binary references can be squatted.

On install/execution it sends minimal telemetry (timestamp, hostname, working directory, platform) to a logging endpoint. By its own description, nothing sensitive — no env vars, no file contents.

This particular package appears to be benign, run by a researcher demonstrating the technique responsibly. But the mechanism it's demonstrating is not benign in general. Nothing about npm's namespace stops someone else from squatting the same kind of name with an actual malicious payload — credential exfiltration, a reverse shell, whatever. The attack surface is: a config that says npx where it meant uvx, whether from a typo, a bad copy-paste, a stale tutorial, or someone deliberately steering a victim toward the wrong command.

Why this mattered for the tool I was building

Here's the part that made me stop and rewrite some code: my own known-package table was, at that point, keying entries purely by package-name string, with no notion of which registry that name was resolved against.

Which means: if I'd shipped it as-is, and someone's config had the npx-confusion version (npx mcp-server-fetch instead of uvx mcp-server-fetch), tollbooth would have looked up mcp-server-fetch in its known-good table, found a match, and confidently told the user this was a known, trusted, official server — because the string matched. It would have actively vouched for a namesquatted package, purely because I'd conflated two unrelated registries under one lookup key.

That's the opposite of what a tool like this should do.

The fix

I split uvx/pipx-invoked servers into their own internal kind, separate from npx-invoked ones, so the same literal package-name string can never cross-pollinate trust or cost data between the two registries:

export type ServerKind =
  | "npx-package"
  | "uvx-package"   // now tracked separately from npx
  | "remote-http"
  | "local-binary"
  | "unknown";
Enter fullscreen mode Exit fullscreen mode

And the known-cost/known-repo tables got split the same way — a PyPI-only table for uvx-package, an npm-only table for npx-package, never merged:

// npm registry lookups only ever consult this:
const KNOWN_SERVER_TOKEN_COSTS: Record<string, number> = { /* npm packages */ };

// uvx/pipx (PyPI) lookups only ever consult this — deliberately never merged
// with the table above, even though some strings could collide:
const KNOWN_UVX_TOKEN_COSTS: Record<string, number> = {
  "mcp-server-fetch": 350,
  "mcp-server-sqlite": 700,
};
Enter fullscreen mode Exit fullscreen mode

With that split, running the earlier two configs through the tool now gives you genuinely different answers for genuinely different packages:

  • uvx mcp-server-fetch → matched against the known PyPI table → recognized, trusted, correctly attributed to the real modelcontextprotocol/servers repo.
  • npx mcp-server-fetchnot matched against that table at all → falls back to an unverified heuristic, and if you check its actual npm metadata, you get pointed at theinfosecguy/npx-canary — not the MCP servers repo. No inherited trust, no inherited "known" label.

Same string. Two different verdicts, because they're two different packages.

The actual takeaway, if you're not building this tool

If your MCP config was written or edited by hand, by an AI assistant, or copy-pasted from a doc you skimmed — it's worth checking whether every npx-invoked entry is actually meant to be npx. A server documented for PyPI/uvx that ended up under npx in your config isn't just "wrong," it's potentially running an entirely different, unaudited package that happens to share a name.

This is a small, narrow instance of a broader problem: MCP configs currently give you very little signal about which registry a given command/args pair actually resolves against, and tooling (including mine, until I caught this) can inherit that ambiguity without realizing it.

The tool

If you want to check your own MCP setup:

npx mcp-tollbooth
Enter fullscreen mode Exit fullscreen mode

It scans the standard config locations (Claude Desktop, Claude Code, Cursor, VS Code) and reports estimated token cost per server, flags servers doing duplicate jobs, and gives a lightweight trust signal — clearly labeled as a heuristic, not a security scan.

Repo: github.com/Ereb-Blade/mcp-tollbooth

Feedback, issues, and PRs (especially more known-server entries) welcome.

Top comments (0)