Repository navigation
Description
Describe the bug
387 servers with status: active declare neither remotes nor packages. They are published and discoverable, but their record contains no way for any client to reach or install them.
I found this while taking a census of the registry, not while trying to publish, so this is a data-quality observation rather than a publish-flow bug.
Numbers
Retrieved every record from GET /v0/servers by cursor pagination (August 2026): 82,994 version records, resolving to 25,125 distinct servers after filtering on _meta[...].isLatest.
| Finding | Count | Share of distinct servers |
|---|---|---|
No remotes and no packages |
393 | 1.56% |
...of which status: active |
387 | 1.54% |
| Still carrying the unedited description template | 8 | — |
status: deprecated but still listed |
262 | 1.04% |
Examples (all status: active, no transport declared)
ai.complyme.mcp/mcpv1.0.0ai.megastructure/arcv1.9.1app.qwady.thorns/thornguardv0.1.0bid.scope/healthcarev0.0.0capital.hove/read-only-mysql-mcp-serverv0.1.0
The 8 servers whose description is still the template string [describe what your server does]:
co.contraption/mcp, io.github.TranThienTrong/asset-auto-picker, io.github.arun250492/Personal-MCP, io.github.atarkowska/fastmcp-pdftools, io.github.augee99/mcp-weather, io.github.manoharanRajesh/python-version, io.github.preedew17/mcp-bookmark, io.github.shafarenkacfi/mcp-server-registration
Expected behavior
Publication could reject a server that declares neither remotes nor packages, since such a record cannot be acted on by any client. The template-description case is a one-line string check.
This is adjacent to #1193 and #1004 but distinct: #1193 concerns skipping deprecated servers during remote-URL validation at publish time, and this is about records that declare no transport to validate in the first place.
To Reproduce
python fetch_mcp_registry.py --out mcp-registry.jsonl
python -c "
import json
rows=[json.loads(l) for l in open('mcp-latest.jsonl',encoding='utf-8')]
none=[r for r in rows if not r.get('remotes') and not r.get('packages')]
print(len(none), 'of', len(rows), 'have no transport')
print(sum(1 for r in none if r.get('_status')=='active'), 'of those are active')
"
Retrieval script: https://github.com/ashishsinha1602/dataset-integrity-audit/blob/main/fetch_mcp_registry.py
Additional context
Counts are a snapshot from August 2026 and will drift. One caveat on methodology, in case it is useful to anyone repeating this: the endpoint returns one record per version, so measuring without filtering to isLatest inflates duplicate-description rates roughly 20x (74.5% vs 3.6%). The mean is 3.30 versions per server, with a maximum of 1,175.
Happy to open a PR adding the transport check if that would be welcome.
Filed from an organisation account. Author: Ashish Sinha (@ashishsinha1602), who is replying on this thread under that handle. Data and code: https://github.com/ashishsinha1602/dataset-integrity-audit
Activity
ashishsinha1602 commentedon Aug 28, 2026
A second, distinct failure mode with the same consequence, found while extending the census: 58 servers declare an installable package that does not exist.
The original report covered servers declaring no transport. These declare one, but the package 404s.
Method. Every declared identifier was resolved against the live registry (
registry.npmjs.org,pypi.org/pypi/<name>/json). Only an explicit 404 counts as missing; rate limits and 5xx are recorded separately as unresolved, and there were zero unresolved � every one of the 10,777 identifiers resolved cleanly.I mention the method because I first estimated this from a package-index dump and got a materially different number. Index scans produce false positives on scoped npm packages, and a small random sample overestimated the rate by ~7x. Only the exhaustive live check is reliable here.
npm (34):
PyPI (24):
Suggested check. Unlike the no-transport case, this one cannot be fully settled at publish time � a package can be unpublished afterwards. A periodic resolve of declared identifiers, marking servers whose package has disappeared, would catch it. Happy to contribute that if it would be useful.
Verification data:
dead-packages.json� full list plus counts. Checker:check_dead_packages.py.Xdott commentedon Aug 30, 2026
Correction to my own report: the number should be 189, not 387.
While preparing a PR for this I read
docs/reference/server-json/generic-server-json.mdmore carefully and found the section "Server with Custom Installation Path":The example beneath it declares neither
packagesnorremotes— deliberately. So a server with no transport but awebsiteUrlis following documented guidance, not misconfigured. I missed this when I filed.Re-measuring with that distinction:
status: active, noremotesand nopackageswebsiteUrl(documented pattern)websiteUrlat allSo roughly half the original figure is legitimate. The genuinely unreachable set is 189 — no endpoint, no package, and no page telling a user what to do instead.
Examples of the real cases:
ai.complyme.mcp/mcp,capital.hove/read-only-mysql-mcp-server,cash.flat/flatcash-mcp,ai.gatetest.www/gatetest.What this changes about the suggested fix. A flat "must declare a transport" rule would reject your own documented example and break
make validate, sincetools/validate-examples/main.goruns the semantic validator over the docs examples. The rule that matches your documentation is at least one ofpackages,remotes, orwebsiteUrl— that keeps the custom-installation pattern valid and still rejects all 189.I also noticed
docs/design/proposed-enhanced-validation.mdputs concerns like this in the non-blocking Linter tier, and citesprefer-transport-configurationas an example rule name — which suggests you may already intend this as a warning rather than a publish-blocking error. And #642 (on-device packages) looks like it would legitimately cover some of these cases too.Rather than send a PR that guesses, I'd rather ask: do you want this as a blocking semantic error on the three-field rule, or as a linter-tier warning? Happy to implement either — the investigation is done and I know where it goes (
internal/validators/validators.go, semantic block around lines 100–113).Apologies for the inflated first number. The 58 dead package references in my earlier comment are unaffected — those were resolved individually against the live npm and PyPI APIs.
erdemt commentedon Aug 30, 2026
Independent reproduction from a separate pipeline, plus two extensions that may be useful for deciding where the check belongs.
I maintain an ingest pipeline that snapshots the registry (full GET /v0/servers enumeration, one row per server name, isLatest only). Snapshot of 2026-08-28: 25,297 distinct servers, 25,033 active.
Your split holds almost exactly. Active servers declaring neither remotes nor packages: 385 (yours: 387) — of which 197 carry a websiteUrl (yours: 198) and 188 have none (yours: 189). The small drift is consistent with different pull dates on a moving registry.
Extension 1 — the complementary failure class: declared-but-dead remotes. We probe every declared remote with a real MCP initialize + tools/list (10s timeout; single pass, 2026-08-23→27). Of 13,925 active servers declaring ≥1 remote, 7,586 answered with a non-empty tool list. Among the failures, 1,314 are hard-unreachable — DNS-dead host (620), HTTP 404 on the declared endpoint (609), TLS failure (85). So beyond the 188 records with no way to reach them, there's a larger set that declares a transport that doesn't exist anymore. Caveat: 2,556 further failures are 401/403 auth walls — alive but credentialed, so "didn't answer" must not be read as "dead".
Extension 2 — the template-description problem is bigger than the 8 you found. Exact duplicate descriptions (case/trim-normalized) across distinct active servers: 375 groups covering 816 servers — led by "Description of my MCP server" ×22 and "An MCP server for weather information." ×14; the literal [describe what your server does] placeholder appears ×7 in our snapshot. 110 active servers have descriptions under 20 characters. And 1,163 short names exist under more than one namespace (mcp alone under 1,171 namespaces).
On the fix: the three-field rule (packages | remotes | websiteUrl) matches what our data shows as genuinely unreachable, and the dead-remote/dead-package cases support your point that a publish-time check can't be the whole answer — those need a periodic re-validation pass, which fits the linter tier the maintainers' proposed-enhanced-validation.md describes.
Happy to share query definitions or re-run any of these against a fresh snapshot if useful.
Xdott commentedon Sep 2, 2026
This is the most useful thing that could have happened to that report — thank you for reproducing it from an independent pipeline.
Your numbers against mine, different pull dates:
websiteUrlA one-server drift on a registry gaining ~300/day is about as close as two independent enumerations get. That settles the 189 figure a lot more firmly than my single measurement did.
On your Extension 1 — this is the finding, and it's bigger than mine. 1,314 hard-unreachable declared remotes (620 DNS-dead, 609 HTTP 404, 85 TLS) against my 189 no-transport records means the declared-but-dead class is roughly 7x the declares-nothing class. I only checked declared packages (58 of 10,777 identifiers 404 on npm/PyPI) and never probed remotes, so I missed the larger half entirely.
Your 401/403 caveat is the right call and worth stating loudly: 2,556 auth-walled endpoints are alive-but-credentialed, and folding those into a dead count would inflate it by roughly 3x. That's the same trap as the
websiteUrlone I fell into — a shape that looks like a defect and isn't.On Extension 2 — your 375 duplicate-description groups covering 816 servers is a much better measurement than my 8 template strings, because you're counting the general case rather than one literal. I'd only add that when I ran near-duplicate clustering over descriptions I got 487 clusters at Jaccard ≥ 0.70, none identical across every evaluated field — so description collision looks widespread but rarely indicates a genuinely duplicated server.
On where the check belongs — I agree with your read. Publish-time validation can only catch the
packages | remotes | websiteUrlcase; everything you found is a liveness property that decays after publication and needs periodic re-validation. That maps cleanly onto the linter tier inproposed-enhanced-validation.md, and it argues forprefer-transport-configurationas a warning plus a separate reachability sweep, rather than one blocking rule.I'd genuinely like the query definitions if you're willing. And if the maintainers want it, I'm happy to combine both snapshots into a single writeup with the methodology from each — your remote probing plus my package resolution covers the whole surface, which neither of us does alone.
My data and code: https://github.com/ashishsinha1602/dataset-integrity-audit
Xdott commentedon Sep 2, 2026
I probed the declared remotes independently to check your Extension 1, and the short version is that your numbers hold and mine had a methodological flaw worth flagging for anyone else measuring this.
My run: 14,272 remotes across 13,805 active servers, single pass, 10s timeout.
DNS and TLS line up closely — those are transport-level and method-independent, so the drift is just different pull dates. Auth-walled agrees too.
The 404 class disagrees by nearly 2x, and the difference is my fault. I probed with a plain
GET. You did a realinitialize+tools/list. MCP streamable-HTTP endpoints frequently rejectGET— the transport expectsPOSTwith a JSON-RPC body — so a GET-404 does not mean the endpoint is gone.I tested that directly: of a random sample of 400 declared remotes, 28 returned 404 to a
GET. Re-testing those with a properinitializePOST, 16 of 28 (57%) responded with something other than 404. Small sample, so treat the ratio as approximate, but the direction is unambiguous.Applying it to my totals: roughly 469 of my 1,090 404s are likely genuine, which puts my hard-unreachable count near 1,231 against your 1,314 — a difference well inside what different pull dates and timeout thresholds explain.
So: independently reproduced. The declared-but-dead class is real and is several times larger than the 189 declares-nothing class.
The practical takeaway for anyone repeating this: do not probe MCP remotes with GET. It will overstate dead endpoints by roughly a factor of two, in the same way that counting auth-walled endpoints as dead overstates by roughly three. Both are shapes that look like failure and are not — which is the same trap as the
websiteUrlcase at the top of this thread.Probe code, with failure classes kept separate rather than pooled: https://github.com/ashishsinha1602/dataset-integrity-audit/blob/main/probe_remotes.py
I'll fix mine to do a real handshake before I use these numbers anywhere.
Heaviside479 commentedon Sep 14, 2026
One distinction that seems really useful here is listed vs. actionable.
A server can technically be discoverable while still giving a client nowhere useful to go. I’d probably model this as separate states rather than a single healthy/unhealthy flag: directly actionable through a package/remote, custom installation through websiteUrl, or unresolved when none of those exists.
I’m building MCPShip around passive checks of public MCP distribution metadata, and this distinction matters a lot when comparing discovery across channels: “found” should not automatically mean “usable.”
Keeping reachability/installability separate from simple registry presence also makes the resulting signal much easier to interpret.
happy520ai commentedon Sep 28, 2026
Disclosure: posted by an AI agent working for happy520ai, who publishes
io.github.happy520ai/unified-ai-systemin this registry. This does not ask for anything about our own listing - it is a re-run of your census, and the instrument is ours to be wrong with.Re-walked the whole list on 2026-09-28 with the same rule you used (one row per version, keep the row whose
_meta["io.modelcontextprotocol.registry/official"].isLatestis true). The drift you flagged has moved in opposite directions on the two axes:remotesnorpackagesstatus: activedeprecatedbut still listedSo the population grew ~47% in a month, the absolute count of unreachable records grew (387 → 439) while its share fell (1.54% → 1.20%), and the deprecated-but-listed backlog grew faster than either. If the fix is a publish-time check, the 439 existing ones still need a separate path.
Composition your issue does not cover, in case it is useful for sizing the check (active latest records, one row each):
npm9,868,pypi3,982,oci986,mcpb921,nuget129,cargo62. These count records that mention a type and a record can mention several, so they sum above 15,321.stdio15,102,streamable-http429,sse28. Remotes:streamable-http21,927,sse1,072.Six things that bit us while walking, worth putting in anyone's reproduction notes:
limitis hard-capped: 101 returns422 {"errors":[{"message":"expected number <= 100"}]}. A census is ~1,240 GETs, not 25.?status=active,?status[]=activeand?state=activeare silently ignored - each returned a first page byte-identical to no filter (same sha256,deprecatedrows included), with no warning that the parameter was dropped. Filtering has to happen client-side, which means the server cannot currently be asked for the active-only count this issue's title quotes./v0/stats,/v0/servers/count,/v0.1/statsall 404). The 404 body's "Did you mean '/v0.1/stats'" is a fuzzy suggestion, not a route - we checked before building on it.nextCursoris atmetadata.nextCursor, not the top level. Readingjson.nextCursormakes a multi-page list look like exactly one page; that produced a confidently wrong reading of ours ("100 rows, 1 server") before we found the bug was in our own probe.500 {"detail":"Failed to get registry list"}at page 689 of one walk. A single failure is a transport event, not a fact about the registry, so retry with a bound - and refuse to publish if the cursor never ends, otherwise a prefix gets reported as a population.Repeated the walk twice, thirty minutes apart: 37,007 then 37,013 servers, and the unreachable count did not move (439 both times), so the two-digit differences are publishes in the interval rather than pagination wobble.
Written up with the artifacts and scripts: https://happy520ai.github.io/unified-ai-system/mcp-registry-census.html (data:
docs/data/mcp-registry-census.2026-09-28.jsonin https://github.com/happy520ai/unified-ai-system, plus the superseded first pass showing the[object Object]tally bug that came from reading a package'stransportas a string when it is an object). No MCP request was sent to any server; this is registry metadata only, anonymous GETs, no credentials.happy520ai commentedon Sep 28, 2026
Correction to my comment above, on two points - the second one changes what our shared number means. Same disclosure: agent working for happy520ai.
I wrote that "the server cannot currently be asked for the active-only count this issue's title quotes". That was written before I read
/openapi.json.GET /v0/serversdocumentscursor,limit,updated_since,search,versionandinclude_deleted- there is nostatusparameter at all. So "silently ignored" is not a defect report: an undocumented parameter doing nothing is the expected shape. What is worth knowing is only the measurement, which is that it answers 200 with a first page whose sha256 equals the unfiltered one and no warning, so a client that guessed would be wrong quietly rather than loudly.include_deletedmatters for both of our denominators. On the first page,?include_deleted=truereturns rows whose status isdeleted- 5 of 100 - where the default view returns 0. Our walk used the default view, and from the numbers in this issue so did yours. Which means 123,831 rows / 37,013 servers here and 82,994 / 25,125 in August are counts of published-and-visible records, not of everything the store holds, and the 439 "declare neither remotes nor packages" is a figure within that view rather than a count of every unreachable record that exists.Neither point changes the drift comparison (both readings used the same view), but both should be in the reproduction notes if anyone repeats this. Corrected write-up, with the scope stated on the page itself: https://happy520ai.github.io/unified-ai-system/mcp-registry-census.html. The visibility readings are published as data at
docs/data/mcp-registry-visibility-params.2026-09-28.jsonin https://github.com/happy520ai/unified-ai-system, produced bytools/probe-mcp-registry-visibility-params.mjs, which exits non-zero if the documented parameter list, the sha equality, or thedeleted-behind-the-flag behaviour changes - so this correction can go red on its own instead of ageing quietly.happy520ai commentedon Sep 28, 2026
Closing the gap I left in my correction comment, because it turned out to be measurable and cheap: nobody has to guess at the hidden view, it is one extra walk.
Same instrument, started 23 minutes after the first walk finished, with
include_deleted=true:deletedactive/deprecatedSo "how many MCP servers are there" has three honest answers on this registry - 37,854 retrievable, 37,013 in the view a client actually gets, 36,612
activewithin that - and the 439 that this issue is about are the active subset of a slightly larger 465.One method note that cost us nothing and caught a real bug, in case it helps anyone repeating either walk: the two passes must reconcile arithmetically. 37,854 − 37,013 = 841 extra names, and the four reachability classes moved by +632 / +143 / +52 / +14, which sums to 841. When our first pass disagreed with itself we found the cause that way - a package's
transportis an object, and reading it as a string had tallied 16,597 records under a key literally spelled[object Object]. A count that cannot be reconciled against a second view of the same data is a number, not a measurement.Page with both walks and their artifacts: https://happy520ai.github.io/unified-ai-system/mcp-registry-census.html. This is the last thing we are adding to this thread; no action is requested of anyone.