atx/registry

State of the MCP ecosystem

An unauthenticated survey of every public remote MCP server we could reach. Every figure on this page is computed from immutable evidence by the query shown beneath it, states what it is a percentage of, and links to the hosts it counted. Figures are recomputed at most every five minutes and cached in between, so what you see was true within the last few minutes rather than this instant.

9,397
hostnames — the unit of every figure here
12,543
services on them
13,015
endpoints scanned
generated 2026-10-08 11:51:20Z
recomputed at most every 5 minutes

The Model Context Protocol has an inventory problem

We scanned every public MCP endpoint we could find — 13,015 endpoints, across 12,543 services, on 9,397 distinct hostnames — and asked each one a single question: what will you tell a stranger?

Most of them will tell a stranger everything.62.7% of MCP hosts hand their complete tool inventory to an unauthenticated caller. Not the data behind the tools — the tools themselves: names, descriptions, and full input schemas, returned to anyone who completes a protocol handshake. Only 37.3% (2,065 of 5,531 hostnames that spoke MCP) require credentials first.

This is not a bug in MCP. The specification does not mandate authentication fortools/list, and for a read-only public data server there is a reasonable argument that the inventory should be open. The problem is that the ecosystem has not sorted itself into servers where that argument holds and servers where it does not.

37.3%
Require authentication to list capabilities
2,065 of 5,531
hosts that spoke MCP
counted only when true of every endpoint on the hostname
see the hosts →

How many MCP endpoints refuse an anonymous handshake, rather than revealing their tool inventory to any caller?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_and(coalesce((e.payload->>'requires_auth')::bool, false)) as flag from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'mcp_protocol_observation' group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

What is in those open inventories

24% of MCP hosts — 1,330 of them — reveal at least one tool whose name indicates it changes state, moves money, or executes code.The names are not ambiguous, and they recur across unrelated operators.

State-changing tool names readable without credentials

Which tool names indicating a write, transfer, or execution appear most often across hosts? Detection reads the NAME only, so a harmless tool with an alarming name is counted and a destructive tool with a bland name is missed. These tools were listable anonymously; we never call one, so this is not evidence that they can be invoked without authentication.

Tool nameHostnames
create_checkout35
submit_feedback31
create_booking29
create_project28
submit_form23
send_message20
delete_webhook15
update_project14
create_task12
create_webhook12
send_feedback12
cancel_booking10
create_account10
create_order10
submit_lead10
how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) -- The tools array is stored only when the inventory changed, so the -- newest capability observation for an endpoint often carries just a hash -- and a count. Take the most recent one that actually has tools: it is by -- definition still the current inventory, or a later row would have -- recorded the change. , with_tools as ( select distinct on (ev.subject_id) ev.subject_id, ev.payload from evidence ev where ev.tenant_id = $1 and ev.type = 'mcp_capability_observation' and ev.payload ? 'tools' order by ev.subject_id, ev.observed_at desc) , tools as ( select distinct latest.slug, lower(jsonb_array_elements(wt.payload->'tools')->>'name') as name from latest join with_tools wt on wt.subject_id = latest.endpoint_id) select name as label, count(distinct slug)::int as hosts from tools where (name ~ '(^|[^a-z0-9])(delete|remove|drop|destroy|purge|revoke|write|update|create|insert|modify|upload|publish|send|transfer|pay|charge|refund|purchase|checkout|execute|exec|deploy|provision|grant|impersonate|sudo)([^a-z0-9]|$)' or (name !~ '^(get|list|read|search|fetch|find|query|describe|show|view|lookup|check|count|summarize|download|export|inspect|browse|preview|resolve|status)([^a-z0-9]|$)' and name ~ '(^|[^a-z0-9])(cancel|close|edit|set|put|patch|add|append|post|submit|approve|reject|invoice|buy|sell|order|trade|mint|sign|invite|subscribe|run|shell|command|admin|book|move|rename|reset|disable|enable|merge|push)([^a-z0-9]|$)')) group by name order by count(distinct slug) desc, name limit 15

Inventories are not always small, either. Of the 3,466 hosts that returned a tool list, 587 expose more than 20 tools and 162 expose more than 50. A fifty-tool inventory read by a stranger is a fifty-item map of what your service can be persuaded to do.

How large the exposed inventories are

How many tools does an anonymous caller learn about? Bucketed by the largest inventory observed on each host. Every count here was readable without credentials, because the scanner never authenticates.

Inventory sizeHostnames
no tools93
1-5 tools1,318
6-20 tools1,468
21-50 tools425
more than 50 tools162
how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, max((e.payload->>'tool_count')::int) as tools from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'mcp_capability_observation' group by latest.slug) select case when tools = 0 then 'no tools' when tools <= 5 then '1-5 tools' when tools <= 20 then '6-20 tools' when tools <= 50 then '21-50 tools' else 'more than 50 tools' end as label, count(*)::int as hosts, min(tools) as sort_key from per_host group by 1 order by min(tools)

The caveat that matters most

We never call a tool. Not once, in 13,015 scans. The scanner sends initialize andtools/list and nothing else, deliberately and permanently.

So this finding is precisely: the inventory is readable without authentication, and it contains state-changing tools. It is not a finding that those tools can be invoked without authentication. Many of these servers almost certainly do enforce authorisation at call time, and the correct reading is a question rather than a verdict: why is the map public if the door is locked?

Publishing an unauthenticated inventory of your write operations is a reconnaissance gift even when every one of those operations is properly guarded. It tells an attacker exactly which door to try.

24%
Reveal state-changing tools to unauthenticated callers
1,330 of 5,531
hosts that spoke MCP
counted when true of any endpoint on the hostname
see the hosts →

How many hosts let an unauthenticated caller ENUMERATE tools whose names suggest they change state, move money, or execute code? This measures what is listable, not what is callable — we never invoke a tool, so this is disclosure of capability, not proof of unauthenticated write access.

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_or((f.rule_id = 'mcp.unauthenticated_consequential_tools')) as flag from latest join evidence ev on ev.scan_id = latest.scan_id and ev.type = 'mcp_protocol_observation' left join findings f on f.scan_id = latest.scan_id group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

How much does the definition matter?

Detection reads the tool name only. That is a crude instrument, so we tested how crude: we re-ran the classification with a stricter rule that vetoes names beginning with a read verb, removing false positives like get_post andget_zodiac_sign that a naive keyword match had counted as state-changing. The figure moved by 0.3 percentage points. The finding is not an artifact of where we drew the line.

A correction we had to make to ourselves

The first version of these statistics was wrong, in a way worth publishing because we expect other people measuring this ecosystem to hit it.

We counted per service, where a service is a hostname plus a path. That is the right unit for a profile page: two paths on one gateway really are different services with different capabilities, and each deserves its own record. It is the wrong unit for an ecosystem statistic. One operator runs 1,312 of those paths on a single hostname — 10.5% of every service in the corpus. Every one of them shares an auth posture, a CDN, a protocol version and a tool template. Counted per service, that single deployment cast 1,312 votes in every percentage we published.

The corpus is lumpier than it looks: 12,543 services live on 9,397 hostnames. Recomputing per hostname moved our own headline numbers substantially:

StatisticPer servicePer hostname
Require authentication29.2%37.3%
Reveal state-changing tools33.2%24%
Publish OAuth metadata43.1%34%

The per-service column is a fixed historical measurement taken on 2026-08-20, kept as a record of the error. The per-hostname column is live.

Every figure on this page is per hostname. Profiles remain per service. Two units, because they answer two different questions — and if you are measuring this ecosystem yourself, the gateways will bite you too.

The most concentrated hostnames

HostnameEndpointsShare of corpus
gateway.pipeworx.io1,31210.1%
api.m2mcent.com2762.1%
api.mcp.ai2381.8%
server.smithery.ai2161.7%
mcp.apify.com1281%

Authentication and its advertising

37.3% of MCP hostnames require authentication to list capabilities. 34% of responding hostnames publish OAuth protected-resource metadata — the RFC 9728 document that tells a client where to authenticate.

These two figures have different denominators and should not be subtracted from one another. What can be said is that neither is a majority, and that this is the pair we most want to watch: a scan repeated in six months tells you whether the MCP ecosystem is hardening or simply growing.

34%
Publish OAuth protected-resource metadata
2,452 of 7,202
hosts that responded
counted only when true of every endpoint on the hostname
see the hosts →

How many endpoints tell a client how to authenticate, by serving /.well-known/oauth-protected-resource?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_and(coalesce((e.payload->>'metadata_present')::bool, false)) as flag from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'oauth_metadata_observation' group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

The ecosystem is standing on a very small number of floors

84.6% of MCP hostnames sit behind a CDN or PaaS(5,406 of 6,388 that returned a Server header). Among those where the provider is identifiable, Cloudflare fronts 63.9% of them.

Who is actually serving the MCP ecosystem

For hosts where a CDN or PaaS could be identified from response headers, which provider is in front? Hosts whose provider could not be identified are excluded, so this describes the identifiable population rather than the whole corpus.

ProviderHostnames
Cloudflare3,455
Vercel868
Railway514
Google Cloud291
Fly.io158
AWS ELB45
Netlify42
Heroku15
AWS CloudFront13
AWS S34
Erlang/Cowboy1
how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select distinct latest.slug, e.payload->>'provider' as provider from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'hosting_provider_observation' where e.payload->>'provider' is not null) select provider as label, count(distinct slug)::int as hosts from per_host group by provider order by count(distinct slug) desc limit 12

This has an immediate consequence for how anyone should read a trust score. A server presenting valid TLS, HSTS and clean headers may have earned none of it — it may have inherited all of it by deploying behind a platform. ATX therefore separatesprovider-earned signals from infrastructure-supplied ones and discounts the latter, because "this team makes good security decisions" and "this team clicked deploy on a platform that makes them" are different claims about the future.

It is also a concentration risk the ecosystem has not priced. One provider's outage or policy change moves most of MCP at once.

84.6%
Sit behind a CDN or PaaS
5,406 of 6,388
hosts that responded with a Server header
counted when true of any endpoint on the hostname
see the hosts →

How many endpoints are fronted by a vendor, so their TLS and headers are the vendor configuration rather than the provider one?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_or((e2.id is not null)) as flag from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'http_response_observation' and e.payload->>'server' is not null left join evidence e2 on e2.scan_id = latest.scan_id and e2.type = 'hosting_provider_observation' group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

You cannot patch-scan this ecosystem

Only 5.3% of hosts disclose a product version(381 of 7,222). Suppressed version banners are good hygiene, and the platform concentration above is largely why the number is so low — CDNs strip these headers by default. But the consequence is worth naming plainly: the standard external security assessment does not work here. Banner-grab-to-CVE-lookup produces almost nothing across 13,015 services, and where a match does exist it is an inference from a header that may be stale, proxied, or simply wrong.

ATX reports these matches at a severity ceiling of medium regardless of CVSS, labelled as inference rather than observation, because a 9.8 CVSS matched from aServer header is not a 9.8 CVSS finding about that service. External trust assessment for AI-callable software has to be built on behavioural evidence — what a server does when you talk to it — not on version arithmetic.

5.3%
Disclose a product version
381 of 7,222
hosts that responded
counted when true of any endpoint on the hostname
see the hosts →

How many endpoints report a precise software version, which is what vulnerability matching depends on?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_or((e.payload->>'server' ~ '[0-9]+\.[0-9]+')) as flag from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'http_response_observation' group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

Transport security is a solved problem

99.6% of hosts serve a certificate valid for their hostname(7,193 of 7,220), and 94% were reachable at all.This is the healthiest number in the report and it deserves to be said plainly: TLS is done. The residue is small, specific and fixable rather than an ecosystem-wide gap.

It is also a useful yardstick. This is what a solved problem looks like at 99.6%, which is the standard the authentication numbers above should eventually be held to.

99.6%
Serve a certificate valid for the hostname
7,193 of 7,220
hosts that responded over HTTPS
counted only when true of every endpoint on the hostname
see the hosts →

How many endpoints present TLS that validates for the host being called?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_and(((e.payload->>'hostname_match')::bool is not false)) as flag from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'tls_certificate_observation' group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host
94%
Endpoints reachable
7,210 of 7,673
hosts with a completed scan
counted when true of any endpoint on the hostname
see the hosts →

Of the remote MCP endpoints listed in the official registry, how many answer at all?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_or(coalesce((e.payload->>'reachable')::bool, true)) as flag from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'availability_observation' group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

The protocol is fragmenting slowly

8.1% of live MCP servers still speak the November 2024 revision (282 of 3,469 hostnames that negotiated a version). Version skew is normal in a young protocol; it matters because "still on an old spec" tends to correlate with "no longer actively maintained," and an unmaintained server with an open write-tool inventory is a different risk from a maintained one.

Which protocol revision servers negotiate

Which MCP protocol version did each host agree to speak? A host running several endpoints on different revisions is counted once per revision.

Protocol versionHostnames
2025-06-182,922
2024-11-05282
2025-03-26206
2025-11-2543
2026-07-2813
2025-11-053
how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select distinct latest.slug, e.payload->>'protocol_version' as version from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'mcp_protocol_observation' where e.payload->>'protocol_version' is not null) select version as label, count(distinct slug)::int as hosts from per_host group by version order by count(distinct slug) desc limit 12

Servers change under their clients

28.5% of hosts observed more than once altered their tool inventory between observations (105 of 368) — tools added, removed, or silently redefined, within days.

This is the only figure here that coverage alone cannot produce. Every other number is a snapshot anyone with a scanner could reproduce this afternoon; this one requires having watched.

Corrected 2026-08-26. This section previously said nothing in the protocol notifies you when capabilities change. That is wrong, and a reader was right to say so. The specification defines tools.listChanged: a server declares it and then emits notifications/tools/list_changed when its tool set changes. A client that holds a session open and honours that notification does not lose track.

The distinction matters, and it sharpens the finding rather than dissolving it. What we measured is out-of-session drift — we re-initialize days apart, which is precisely the case listChanged cannot serve, because there is no live session to receive a notification. So the gap is narrower and more specific than we first wrote it: a reviewer, a security scanner or a registry has no cheap way to askhas anything changed since I last looked without a full initialize + tools/list round trip. Anyone who approves a server's capabilities and then disconnects is relying on a review that has no staleness signal at all.

We do not claim these changes are malicious, or even significant. Most are almost certainly ordinary development. The point is the rate: at 28.5% per rescan interval, a one-time review of an MCP server's capabilities is a statement about the past.

The published figure is a lower bound, and here is by how much

Corrected 2026-08-26, again, from the same discussion. A second reader pointed out that what a capability digest covers decides whether it detects anything. Ours covered tool name and input schema — not descriptions. Tool poisoning lives in the description, so a rewritten description with an untouched name and schema produced no drift at all.

Recomputing the stored evidence under a digest that includes descriptions:

UnitName + schemaIncluding description
hosts8.8%10.9%
endpoints9.4%31.5%

The gap between those two rows is itself a finding. Description churn concentrates on multi-endpoint gateways: one operator editing a shared template rewrites a thousand descriptions at once, which barely moves a per-host figure and dominates a per-endpoint one. It is also concrete evidence for the objection anyone proposing a capability digest will meet — that a content hash fires on cosmetic edits. It does. That is the price of it detecting the edits that matter.

The scanner now hashes name, description, input schema and annotation values — the last because recording only which annotations exist would make readOnlyHint flipping from true to false invisible. The statistic below still reflects the old, narrower digest and will rise as the corpus is re-observed. It is left in place rather than quietly restated.

28.5%
Changed their capability set between scans
105 of 368
hosts with an endpoint seen at least twice
counted when true of any endpoint on the hostname
see the hosts →

Of hosts observed more than once, how many added, removed, or silently altered a tool?

how this is computed
with per_ep as ( select split_part(sv.slug, '/', 1) as host, ev.subject_id, count(*) filter (where ev.type = 'mcp_capability_observation') as obs_n, count(*) filter (where ev.type = 'mcp_capability_drift') as drift_n from evidence ev join endpoints e on e.id = ev.subject_id join services sv on sv.id = e.service_id where ev.tenant_id = $1 and ev.type in ('mcp_capability_observation', 'mcp_capability_drift') group by 1, 2) , per_host as ( select host, max(obs_n) as max_obs, sum(drift_n) as drift_n from per_ep group by host) select count(*) filter (where drift_n > 0)::int as numerator, count(*)::int as denominator from per_host where max_obs > 1

What this report does not tell you

18.3%
Disallow scanning via robots.txt
1,719 of 9,397
hosts with a terminal scan
counted when true of any endpoint on the hostname
see the hosts →

How many endpoints ask not to be scanned, and were therefore not scanned?

how this is computed
with latest as ( select distinct on (s.endpoint_id) s.id as scan_id, s.endpoint_id, s.status, split_part(sv.slug, '/', 1) as slug, sv.slug as service_slug from scans s join endpoints e on e.id = s.endpoint_id join services sv on sv.id = e.service_id where s.tenant_id = $1 and s.status in ('completed','blocked','error') order by s.endpoint_id, s.requested_at desc ) , per_host as ( select latest.slug, min(latest.service_slug) as rep, bool_or((latest.status = 'blocked')) as flag from latest group by latest.slug) select count(*) filter (where flag)::int as numerator, count(*)::int as denominator from per_host

Method

Every figure is derived from an append-only evidence ledger. Each observation is content-hashed, attributed to the scanner instance that produced it, and never mutated — corrections supersede rather than overwrite.

Scanning is deliberately conservative: at most three unauthenticated GET requests plus, where the endpoint speaks MCP, two read-only JSON-RPC calls (initialize and tools/list) over Streamable HTTP. No tool is ever invoked. One request at a time per host, a minimum delay between them, robots.txt honoured, Retry-Afterobeyed, and a user-agent carrying a contact URL and opt-out route on every request — see the scanner page.

Statistics use the most recent terminal scan per endpoint, so a frequently rescanned endpoint is not counted more heavily, then roll up so each hostname counts once. Where a hostname's endpoints disagree the rollup runs toward caution: a favourable property must hold on every endpoint, while a risk counts if it appears on any. Each statistic states which rule it used. Percentages are suppressed below a sample of 30 rather than published at a size that cannot support them.

Identity. A service is a hostname plus its path, with the trailing /mcp or /sse mount removed. A host that versions its path appears as a separate service per version, and a host that distinguishes services by query string collapses into one — query strings are user-composable, so treating them as identity would let anyone mint unlimited entries.

Every number here is a link. Click any figure to see the hosts behind it and the evidence behind them. If a figure is wrong, it is wrong in a way you can demonstrate — and we would like you to. We have already had to correct ourselves once, in public, above.

If this is your server

Every scanned service has a public profile. If you operate one:

ATX is an independent evidence layer for AI-callable software. No provider pays for a score, and no score can be bought.