# State of MCP server security: method Snapshot: **2026-11-05** (generated 2026-11-05T03:26:19+00:00 by `php artisan report:security`). ## Sources - **Official MCP Registry** (registry.modelcontextprotocol.io), mirrored daily by `mcp:registry-sync`. Only entries with `status=active` are counted. The registry is read as mirrored on the snapshot date, and the set of entries is fixed when the run starts, so a sync during the run can't change who is counted. - **OSV** (osv.dev, includes GitHub Security Advisories): advisories for each entry's npm / PyPI / NuGet / cargo package, from the daily `mcp:issues-scan`. - **NVD**: CVEs mentioning MCP, matched to entries by GitHub repo in the CVE references. Our feed covers a 120-day window (2026-07-08 to 2026-11-05). - **GitHub GraphQL API**: `pushedAt`, `isArchived` and `isSecurityPolicyEnabled` for each distinct repo, in batched queries. Advisories a reviewer dismissed as not applying are not counted. ## Populations - Full registry: 55934 active entries (101 deleted, 874 deprecated left out). - Repo checks: 29540 distinct GitHub repos. Monorepos back many entries, so repos are deduped by `github.com/owner/repo`. - Editorial sample: 82 hand-graded listings. **Not representative of the registry.** ## Checks ### 1. Vulnerable latest release (full registry) Share of npm / PyPI / NuGet / cargo entries where at least one OSV advisory (not withdrawn, not dismissed) affects the package version in the entry's latest registry record. That is the package's own version, which can differ from the server version and can trail the package's newest release. **Result:** 9 of 19765 entries (0.05%). Left out: No OSV-covered package (oci, mcpb or remote-only): 36164; Package version is a tag such as "latest" (OSV cannot check it): 5. *Caveat:* OSV covers the package itself, not its dependencies. oci, mcpb and remote-only entries have no OSV coverage and are left out of the base. Entries whose package has no version, or only a tag such as "latest" that OSV cannot match, are left out as not checkable (not counted as clean). Withdrawn advisories are dropped when the scan runs; one withdrawn after it was recorded stays until reviewed. ### 2. Any advisory ever filed (full registry) Share of entries with at least one advisory on record: OSV hits on the package (any version) plus NVD CVEs matched to the entry by its GitHub repo. Dismissed ones are not counted. Advisory counts merge OSV aliases (a GHSA and a PYSEC record for one flaw count once). **Result:** 84 of 37746 entries (0.22%). Left out: No OSV-covered package and no GitHub repo (nothing to match on): 18188. Also: entries with osv: 58; entries with nvd: 48; nvd cves published in window: 331; nvd cves unassigned in window: 273; nvd cves unassigned total: 384. *Caveat:* Base is entries we can match at all (an OSV-covered package or a GitHub repo). Our NVD feed only reaches back 120 days, so NVD is reported as "MCP CVEs published in the window" and CVEs we could not match to an entry are reported as unassigned. Shared infrastructure repos (the registry, the spec, the inspector, the official SDKs) are not matched on, since many unrelated entries link them; a CVE cited only through one counts as unassigned. ### 3. Stale repo (full registry) Share of GitHub repos (deduped, since monorepos back many entries) whose last push is more than 180 days before the snapshot, or that are archived. From GitHub GraphQL pushedAt and isArchived; a push to any branch counts. **Result:** 1957 of 25575 repos (7.7%). Left out: Unreachable on GitHub (deleted, private, renamed without redirect, blocked): 3965 repos; Repo URL not on GitHub: 89 entries; No repo URL: 19146 entries. Also: archived: 228; never pushed: 0; renamed resolved: 785; entries on stale repos: 2154. *Caveat:* Repos that GitHub cannot resolve (deleted, private, blocked) are left out and counted. Entries whose repo is not on GitHub are left out. A repo that was never pushed to counts as stale. ### 4. No security policy (full registry) Share of GitHub repos (deduped) where GraphQL isSecurityPolicyEnabled is false. Org-level policies count. **Result:** 17399 of 25575 repos (68%). Left out: Unreachable on GitHub (deleted, private, renamed without redirect, blocked): 3965 repos; Repo URL not on GitHub: 89 entries; No repo URL: 19146 entries. Also: entries on repos without policy: 22720. *Caveat:* Shows that a policy exists, not that anyone answers it. Unresolvable and non-GitHub repos are left out as for the stale check. ### 5. No public source (full registry) Share of entries with no repository URL in the registry. **Result:** 19146 of 55934 entries (34.2%). Also: of which list a remote: 18217. *Caveat:* Some of these are hosted, closed-source servers rather than neglected ones. ### 6. Plaintext remote (full registry) Share of entries with a remote endpoint where at least one remotes[].url uses http:// on a host that is not local (localhost, *.localhost, 127.0.0.0/8, ::1, 0.0.0.0). **Result:** 0 of 37722 entries (0%). Left out: No remote endpoint (package-only): 18212. Also: entries with localhost http remote: 0. *Caveat:* Base is entries that list a remote endpoint. We read the URL as published; we did not connect to it. ### 7. Grade distribution (editorial sample) TrustedMCP grade (A-D) of each hand-graded listing, per TRUST-RUBRIC.md. **Result** (of 82 listings): A 31 (37.8%), B 27 (32.9%), C 19 (23.2%), D 5 (6.1%). *Caveat:* Editorial sample, not representative of the registry: these servers were picked for grading, mostly popular and vendor-backed ones. ### 8. Default auth setting (editorial sample) Authentication as recorded for each hand-graded listing: on by default (yes), supported but off by default (optional), or none (no). **Result** (of 82 listings): yes 55 (67.1%), optional 12 (14.6%), no 15 (18.3%). *Caveat:* Editorial sample, not representative of the registry. ### 9. Transport-safety band (editorial sample) Transport-safety band of each hand-graded listing, from its rubric points (TRUST-RUBRIC.md, 20/14/6/0): "full" = 20 (transport fits its exposure, TLS on HTTP, no shell-out on untrusted input), "partial" = 14 (stdio only, no shell-out) or 6 (shells out to helpers, or HTTP without enforced TLS), "none" = 0 (shell-out on model-controlled input, or plaintext remote). Also broken down by points and by transport offered. **Result** (of 82 listings): full 58 (70.7%), partial 21 (25.6%), none 3 (3.7%). Also: inferred: 0. *Caveat:* Editorial sample, not representative of the registry. Bands come from the stored per-check points (servers.score_transport), the same ones the trust meter shows. A listing without stored points falls back to the meter's inferred two-state band (HTTP with some auth = full, otherwise partial), which errs low; those are counted as "inferred". ## Not measured - Unpinned versions: pinning is set in each client's config, and we don't have that data. - Broad permissions: we have no tool or scope data. - No auth across the full registry: stdio servers can't be observed, and probing remote ones would be active scanning. - Injection and path-traversal rates: these need code analysis; third-party figures are not presented as our findings. ## Hand check We checked the riskiest claims by hand. This was done on the run of 2026-11-04 and not repeated on the final run of 2026-11-05. The numbers marked "earlier run" below come from that run, so they differ from the rest of this file. We confirmed that the final run flags the same 9 entries. - **Advisories:** all 9 flagged entries were read against the advisory text. 8 hold as written. In the other one, the advisory covers the package version pinned in the registry record, not the server version the record shows. The report now reads the package version. No exploit was run, so this doesn't prove each flaw can be exploited. The other 19,756 entries weren't read one by one. - **Newest release:** for 3 of the 9, the listed version is also the package's newest release. In the others the registry lists an older or removed version. 1 of the 9 is a malware flag rather than a flaw: that version was removed from npm, the registry record still lists it, and We can't tell why it was flagged or who was behind it. - **Unreachable repos:** a random 50 of the 3,833 unreachable repos in the earlier run were retested without logging in, and 50 still returned 404 (95% interval 92.9-100%). A deleted repo, a private repo and a renamed repo whose redirect has gone all return the same 404, so there is no split between them. - **CVEs:** we rebuilt the unmatched list from public NVD data for the earlier run: 271 unmatched, where our scan then found 272 of 329 (the final run has 273 of 331). The two lists were not compared ID by ID. A random 60 of the 271 were read: 54 really about MCP, 1 borderline, 5 only mention it. By product type: 35 standalone servers or gateways, 6 SDKs or frameworks, 19 larger products. 10 of the 60 are one product. So the unmatched count is not a coverage number. ## How this public copy differs from our working data Our working data has one row per entry and per repo, with names. This copy has no entry, publisher or repo names, except the hand-graded listings in `editorial.csv`, which are public pages on our site. - Unreachable repos appear only as totals by size, in `unreachable-repos-by-size.csv`. A list of dead names could help someone take over a name. Publishing the names of the live repos would give the same list, by subtracting them from the public registry, so those are left out too. - `entries.csv` has no per-entry GitHub columns, and rows are in random order. Rows have no key, so the files don't join to each other, and a row has no name to check against. - `repos.csv` has the date of the last push, not the time. The stale rule uses dates: 2026-05-09 is the cut-off. - `editorial.csv` has the transport band for each listing but not its rubric points. The site shows bands, not points. - The code is in our private site repo: `php artisan report:security` is the command named above, and `TRUST-RUBRIC.md` is the rubric behind the grades. `summary.json` mentions both. The public summary of the rubric is at https://trustedmcp.io/guides/how-trustedmcp-scores-servers. ## GitHub answers The GitHub pass ran in parts and resumed from its own cache: 29,284 of 29,540 repo answers came from that cache and the rest from 6 queries in the last part. The cache has no timestamps, so we can't give an exact time for each answer. The run finished at 2026-11-05T03:26:19+00:00. Sort the repos by name and split them into ten parts, and the newest push recorded in each part is between 2026-11-05T01:53:40Z and 2026-11-05T03:25:40Z. That suggests the answers are recent throughout, but it doesn't prove it. ## Files - `summary.json`: every check with numerator, denominator, percentage, exclusions and caveat, unchanged from the report run. - `report-page.json`: the extra figures on the report page, unchanged. `entries.csv` re-counts most of them; `top_publishers_entries` and the hand check can't be re-counted from this package, because they need names or reading by hand. - `entries.csv`: one row per active registry entry (1 = yes, 0 = no, blank = not applicable). See the README for the columns. - `repos.csv`: one row per reachable GitHub repo. - `unreachable-repos-by-size.csv`: unreachable repos as totals by how many entries link to them. - `editorial.csv`: the hand-graded listings. These numbers are a signal derived from public data, not an audit.