<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ereb-blade</title>
    <description>The latest articles on DEV Community by ereb-blade (@ereb-blade).</description>
    <link>https://dev.to/ereb-blade</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4020423%2F9d781e79-b729-4222-8a6e-3ef58b5e88f3.jpg</url>
      <title>DEV Community: ereb-blade</title>
      <link>https://dev.to/ereb-blade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ereb-blade"/>
    <language>en</language>
    <item>
      <title>My MCP token-cost auditor caught a live npx/PyPI namesquat</title>
      <dc:creator>ereb-blade</dc:creator>
      <pubDate>Wed, 08 Jul 2026 03:09:33 +0000</pubDate>
      <link>https://dev.to/ereb-blade/my-mcp-token-cost-auditor-caught-a-live-npxpypi-namesquat-4jkk</link>
      <guid>https://dev.to/ereb-blade/my-mcp-token-cost-auditor-caught-a-live-npxpypi-namesquat-4jkk</guid>
      <description>&lt;h2&gt;
  
  
  The tool I was actually building
&lt;/h2&gt;

&lt;p&gt;I've been building &lt;a href="https://github.com/Ereb-Blade/mcp-tollbooth" rel="noopener noreferrer"&gt;mcp-tollbooth&lt;/a&gt;, a small CLI that scans your local MCP config (Claude Desktop, Claude Code, Cursor, VS Code) and answers a boring but real question: &lt;strong&gt;how many tokens are your MCP servers silently costing you before your agent does anything useful?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;While building the "known package" lookup table, though, I ran into something I wasn't looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lookup table
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://github.com/modelcontextprotocol/servers" rel="noopener noreferrer"&gt;modelcontextprotocol/servers&lt;/a&gt; repo is &lt;code&gt;fetch&lt;/code&gt; — a Python tool distributed on &lt;strong&gt;PyPI&lt;/strong&gt;, normally invoked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp-server-fetch"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;uvx&lt;/code&gt; pulls &lt;code&gt;mcp-server-fetch&lt;/code&gt; from PyPI. Straightforward.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-server-fetch"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second block looks almost identical. It is not the same package.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually on npm
&lt;/h2&gt;

&lt;p&gt;Out of curiosity, I checked what &lt;code&gt;mcp-server-fetch&lt;/code&gt; resolves to on the &lt;strong&gt;npm&lt;/strong&gt; registry — a completely different namespace from PyPI, but the exact same string.&lt;/p&gt;

&lt;p&gt;Someone already owns it. It's published as a "security research canary":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;maintainer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-canaries&lt;/span&gt;
&lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;theinfosecguy/npx-canary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its own README is upfront about what it is:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;a config that says &lt;code&gt;npx&lt;/code&gt; where it meant &lt;code&gt;uvx&lt;/code&gt;&lt;/strong&gt;, whether from a typo, a bad copy-paste, a stale tutorial, or someone deliberately steering a victim toward the wrong command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this mattered for the tool I was building
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;which registry&lt;/em&gt; that name was resolved against.&lt;/p&gt;

&lt;p&gt;Which means: if I'd shipped it as-is, and someone's config had the npx-confusion version (&lt;code&gt;npx mcp-server-fetch&lt;/code&gt; instead of &lt;code&gt;uvx mcp-server-fetch&lt;/code&gt;), tollbooth would have looked up &lt;code&gt;mcp-server-fetch&lt;/code&gt; in its known-good table, found a match, and confidently told the user this was a &lt;strong&gt;known, trusted, official server&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;That's the opposite of what a tool like this should do.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ServerKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npx-package&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uvx-package&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// now tracked separately from npx&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remote-http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local-binary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the known-cost/known-repo tables got split the same way — a PyPI-only table for &lt;code&gt;uvx-package&lt;/code&gt;, an npm-only table for &lt;code&gt;npx-package&lt;/code&gt;, never merged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm registry lookups only ever consult this:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_SERVER_TOKEN_COSTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* npm packages */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// uvx/pipx (PyPI) lookups only ever consult this — deliberately never merged&lt;/span&gt;
&lt;span class="c1"&gt;// with the table above, even though some strings could collide:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_UVX_TOKEN_COSTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp-server-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp-server-sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that split, running the earlier two configs through the tool now gives you genuinely different answers for genuinely different packages:&lt;/p&gt;

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

&lt;p&gt;Same string. Two different verdicts, because they're two different packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway, if you're not building this tool
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;npx&lt;/code&gt;-invoked entry is actually meant to be &lt;code&gt;npx&lt;/code&gt;. A server documented for PyPI/&lt;code&gt;uvx&lt;/code&gt; that ended up under &lt;code&gt;npx&lt;/code&gt; in your config isn't just "wrong," it's potentially running an entirely different, unaudited package that happens to share a name.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;If you want to check your own MCP setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-tollbooth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Ereb-Blade/mcp-tollbooth" rel="noopener noreferrer"&gt;github.com/Ereb-Blade/mcp-tollbooth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, issues, and PRs (especially more known-server entries) welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>security</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
