DEV Community

Cover image for The median MCP server installs 94 packages, and 88% pull an HTTP framework into a stdio process
Jiangw2718i
Jiangw2718i

Posted on • Originally published at tryfrisk.dev

The median MCP server installs 94 packages, and 88% pull an HTTP framework into a stdio process

I built an MCP server this week. Before publishing it I did the thing I would want someone else to do: I looked at what installing it actually puts on your machine.

Installing it pulled down 95 packages. My server is two files and 145 lines. It exposes one read-only tool. It speaks JSON-RPC over its own stdin and stdout, and it never opens a socket.

Inside those 95 packages were express, hono, and @hono/node-server.

That bothered me enough to ask whether it was just me. It was not. I measured every npm-packaged stdio server in the official MCP registry — 6,139 of them, 6,030 resolvable — and the median install is 94 packages. For the 88% built on the v1 SDK the median is 95, which is exactly where mine landed, without trying.

The interesting part turned out not to be the number. It is that almost nobody chose it, and that you cannot see it in anything they wrote down.

What I measured

The official MCP registry held 18,990 servers when I pulled it, 18,788 marked active. Filtering to entries that ship an npm package and declare stdio transport gives 6,139 distinct npm packages. That is the population.

For each one I asked npm what a production install resolves to:

npm install <package>@<version> \
  --omit=dev --package-lock-only --no-audit --ignore-scripts
Enter fullscreen mode Exit fullscreen mode

--package-lock-only resolves the full tree without downloading a single tarball, which is the only reason 6,139 packages is feasible at all.

The scripts and the measured data are in the repo, at research/mcp-dependency-census. This post makes claims about its own instrument, so the instrument ships with it. Running article_numbers.py reprints the population, distribution, framework and SDK figures below, each labelled as it appears here. The figures that need the network — the sampled dependency declarations, the SDK-version table, and the sizes — are not in it.

6,030 resolved. 109 did not, and it is worth saying why: 58 declare a version that is not published on npm, 29 name a package that is not on npm at all, 19 have an identifier npm cannot parse as a spec, and 3 failed otherwise — one refused this platform, one timed out, one exited non-zero. Those 109 are excluded from every number below.

One wording note I will use consistently: each row is an npm package, not a server. 166 packages appear in the registry under two different server names, and I count those once.

Checking the ruler, twice, because the first check was worthless

I got a result that could not be true. A server package occupies one slot plus whatever the SDK brings, so a server depending on the SDK had to be larger than the SDK alone. My data said the median server was smaller.

I had two counting rules and had not noticed. Install locations counts every node_modules/… entry — content-type appears three times in the SDK's tree, at two versions, because of a conflict deeper down. Distinct package names collapses those three into one. My validation measured install locations and agreed with a real npm install. My measurement counted distinct names. I validated one ruler and then measured with a different one.

So I fixed it, re-ran, and validated six packages against real installs. Six out of six agreed, and I nearly shipped that sentence as my proof of rigour.

It was not proof of anything. All six of those packages have zero platform-specific optional dependencies — and that is the one failure mode this instrument has. --package-lock-only writes a lockfile entry for every platform variant of an optionalDependency; a real install unpacks only the ones matching your OS and CPU. For kubernetes-mcp-server@0.0.65 the lockfile lists 7 and a real install creates 2. My validation set could not have detected that if it tried.

Two instruments later — npm install --dry-run --json looked authoritative and then undercounted appium-mcp by 43% against its own real install — I settled on gating optional entries by os/cpu, and validated it the way it should have been validated the first time: real installs on 150 randomly chosen packages from the actual population.

packages exact match
no optional entries 128 128 (100%)
has optional entries 21 18 (86%)
all 149 146 (98.0%)

Three overcounts, by 1.5% to 2.0%. Not one undercount. Sample median: 94 measured, 94 real. So the numbers below were exact for 98% of a random sample, never low in it, and at worst 2% high on trees carrying platform-specific binaries. The median is untouched, because the trees sitting at the median contain no optional entries at all.

I am spelling this out because the first version of this post contained a section congratulating itself for checking its instrument, using a check that was structurally incapable of finding the bug it still had.

The distribution

packages installed packages share
1 443 7.3%
2–5 118 2.0%
6–20 72 1.2%
21–50 60 1.0%
51–100 3,281 54.4%
101–250 1,846 30.6%
251+ 210 3.5%

Median 94. First quartile also 94 — 2,356 packages resolve to exactly 94, and 52.6% of the population sits within five packages of it. The heaviest resolves 2,052.

5,280 of 6,030 — 87.6% — install an HTTP server framework:

framework packages
express 5,276
hono 5,203
@hono/node-server 5,202
koa 22
fastify 19
connect 7

Every one of these declares stdio transport in the registry.

Where the wall at 94 comes from

87.8% of these packages have @modelcontextprotocol/sdk, the v1 SDK, in their production tree. On its own that package resolves to 93 install locations. express, hono, @hono/node-server, cors, jose, ajv, and express-rate-limit are all in its dependencies — not optional, not peer. They install regardless of which transport you use.

Of the 4,992 packages resolving SDK v1 at its current version, 45.9% land on exactly 94: themselves, plus the SDK's 93, and nothing else that npm has to unpack.

I want to be careful about what that does and does not mean, because in the first draft of this post I got it wrong. It does not mean those authors declared one dependency. I sampled 400 of them and read their published dependencies: 35.8% declared exactly one, most declared two, and some declared four. A server can list zod, cross-spawn and zod-to-json-schema and still land on 94, because all three are already inside the SDK's tree. "Adds nothing" is a fact about deduplication, not about authorship. My first draft quietly promoted one into the other, in the sentence carrying the whole moral argument.

What survives is narrower and still worth saying: of the 5,280 packages carrying an HTTP framework, 99.1% have the v1 SDK. Almost no one typed express.

The number nobody chose

Here is the part I missed entirely on the first pass, and it is the most important thing in this post.

94 is not what any of these authors pinned. It is what today's npm floats their ranges to. In 400 sampled packages, 400 declared a caret range on the SDK. Not one pinned a version. And the SDK's weight has grown a great deal:

SDK version published install locations express hono
1.0.0 2024-11-25 14 no no
1.10.0 2025-04-17 82 yes no
1.20.0 2025-10-09 88 yes no
1.25.0 2025-12-15 92 yes yes
1.30.0 2026-07-27 93 yes yes

A server published against ^1.0.0 in late 2024 installed about 14 packages the day it shipped. The same unchanged package.json installs 94 today. express arrived in the SDK's dependencies around April 2025 and hono in December 2025 — neither is ancient, and neither required anything of the 5,203 servers that now carry them.

So my first draft's line about "what that one line used to cost" had the arrow backwards. Nothing these authors wrote got heavier. A caret range did what caret ranges do, upstream added an HTTP stack, and 5,295 dependency trees grew without a single commit in any of them. The wall at 94 is a photograph of one afternoon's npm, not a property of anybody's code.

That also means this post has a shelf life, and that re-running it after the next SDK release will not reproduce these numbers. I could not demonstrate drift at the scale of a day — I re-resolved 413 random rows hours later and every one matched — but it follows mechanically from 400 caret ranges out of 400.

What changed three days ago

On 2026-07-27 the TypeScript SDK shipped v2, splitting the monolith: @modelcontextprotocol/server, @modelcontextprotocol/core beneath it, and HTTP framework adapters (/express, /fastify, /hono) as separate packages you install only if you serve HTTP.

I migrated. My server went from 95 packages to 5. npm install frisk-mcp now pulls the package itself plus @modelcontextprotocol/server, @modelcontextprotocol/core, zod, and my own screening library. No web framework. Both numbers count the same way every number above does: the package itself plus everything npm unpacks for it. The code change was two import lines; the rest was fixing what the codemod did to my dependencies — it put the MCP client package into runtime deps, where a server does not need it.

At the time of measurement 27 of 6,030 — 0.4% — had v2 in the tree, three days after release. And that 0.4% is softer than it sounds: 9 of the 27 also still carry v1 and the full express-plus-hono stack, resolving between 101 and 361 packages. A third of the v2 cohort has shed nothing yet.

What I am not claiming

A dependency is not a vulnerability. Ninety-four packages is not ninety-four problems, express is not unsafe, and nothing here says any of these servers is compromised or badly written. I measured package counts, not risk.

On stdio versus HTTP: the registry declares stdio for every entry I counted, and the framework is a hard dependency of the v1 SDK, so it lands in the tree either way. But some of these packages do serve HTTP as well, and rather than say "I did not audit that" — which is what the first draft said, and which was the more flattering choice — I went and counted: 176 of the 6,139 also ship a streamable-http package entry, and 759 active registry entries pair an npm stdio package with a remote HTTP endpoint. For those, the framework is not dead weight. That is under 3% of the population, and it does not rescue the other 97%.

On the comparison group: it is tempting to point at the 717 packages with no MCP SDK in the tree, note their median install is 1, and call that the counterfactual. It is not clean. All 443 single-package rows are in that group — 62% of it — and I say below that I would not vouch for a quarter of those. The median of 1 is produced by exactly the rows I am least willing to defend, so I am not resting the causal claim on it. The claim rests on the SDK's own dependency list and on 45.9% landing on 94 to the package.

On the single-package rows: median unpacked size 38 KB; 33% over 100 KB and plausibly bundling their dependencies at build time; 25% under 10 KB, small enough that I would not assume there is a working server inside.

On versions: I measured the version each registry entry declares. npx -y <package> installs whatever is latest on npm instead, which can resolve to a different tree than the one I counted.

Why I care about this one

I work on payment screening for agents. What I am paid to worry about is what an autonomous agent is about to hand money to. An MCP server sits closer to that decision than almost anything else on the machine: it runs with your agent's privileges, on your credentials, in the same process tree.

Which is why it is worth stating plainly that my own server — the one whose job is to be suspicious of things — was carrying a web framework it has no code path to reach, and that I only know because I looked. And then that my first attempt at looking was wrong twice: once about which ruler I was using, once about what the ruler could not see.

If you maintain an MCP server, the check takes about ten seconds. Do it as a real install in an empty directory, not with --dry-run — that is one of the two instruments I caught being wrong:

mkdir /tmp/dep-check && cd /tmp/dep-check && npm init -y
npm install <your-package> --omit=dev
Enter fullscreen mode Exit fullscreen mode

The last line npm prints is added N packages. If that number surprises you, the migration guide is upgrade-to-v2.md, and there is a codemod. Read what it does to your dependencies block before you commit it.


Frisk is an MIT-licensed screening library for agent payments. The MCP server described here is frisk-mcp.

Top comments (0)