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,394
hostnames — the unit of every figure here
12,537
services on them
13,008
endpoints scanned
generated 2026-08-22 19:37:07Z
recomputed at most every 5 minutes

The Model Context Protocol has an inventory problem

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

Most of them will tell a stranger everything.62.3% 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.7% (2,414 of 6,395 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.7%
Require authentication to list capabilities
2,414 of 6,395
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

22.6% of MCP hosts — 1,448 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_checkout38
create_booking30
create_project28
submit_feedback26
submit_form25
send_message19
delete_webhook16
create_task14
create_webhook13
update_project13
create_account11
create_order11
submit_lead11
subscribe11
create_api_key10
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 ) , tools as ( select distinct latest.slug, lower(jsonb_array_elements(e.payload->'tools')->>'name') as name from latest join evidence e on e.scan_id = latest.scan_id and e.type = 'mcp_capability_observation') 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,981 hosts that returned a tool list, 625 expose more than 20 tools and 172 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 tools140
1-5 tools1,608
6-20 tools1,608
21-50 tools453
more than 50 tools172
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,008 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.

22.6%
Reveal state-changing tools to unauthenticated callers
1,448 of 6,395
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,311 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,311 votes in every percentage we published.

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

StatisticPer servicePer hostname
Require authentication29.2%37.7%
Reveal state-changing tools33.2%22.6%
Publish OAuth metadata43.1%34.7%

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,31110.1%
api.m2mcent.com2762.1%
api.mcp.ai2381.8%
server.smithery.ai2161.7%
mcp.apify.com1281%

Authentication and its advertising

37.7% of MCP hostnames require authentication to list capabilities. 34.7% 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.7%
Publish OAuth protected-resource metadata
2,580 of 7,441
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.1% of MCP hostnames sit behind a CDN or PaaS(5,475 of 6,511 that returned a Server header). Among those where the provider is identifiable, Cloudflare fronts 64% 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,502
Vercel870
Railway516
Google Cloud303
Fly.io164
AWS ELB44
Netlify43
Heroku16
AWS CloudFront13
AWS S33
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.1%
Sit behind a CDN or PaaS
5,475 of 6,511
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.5% of hosts disclose a product version(411 of 7,465). 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,008 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.5%
Disclose a product version
411 of 7,465
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.7% of hosts serve a certificate valid for their hostname(7,436 of 7,461), and 95.6% 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.7%, which is the standard the authentication numbers above should eventually be held to.

99.7%
Serve a certificate valid for the hostname
7,436 of 7,461
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
95.6%
Endpoints reachable
7,465 of 7,811
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

11.1% of live MCP servers still speak the November 2024 revision (444 of 3,985 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-183,223
2024-11-05444
2025-03-26243
2025-11-2561
2026-07-2811
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

Capability drift

Whether a server's tools change under its clients is the measurement we most want and have least of. It requires two observations of the same endpoint, and this is a first scan.

7.2%
Changed their capability set between scans
22 of 304
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

17%
Disallow scanning via robots.txt
1,596 of 9,394
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.