{
    "title": "State of MCP server security",
    "snapshot": "2026-11-05",
    "generated_at": "2026-11-05T03:26:19+00:00",
    "populations": {
        "registry_active_entries": 55934,
        "registry_other_entries": {
            "deleted": 101,
            "deprecated": 874
        },
        "github_repos": 29540,
        "editorial_listings": 82
    },
    "windows": {
        "stale_before": "2026-05-09",
        "nvd_from": "2026-07-08",
        "nvd_to": "2026-11-05"
    },
    "github": {
        "computed": true,
        "queries": 6,
        "points_used": 6,
        "repos_from_cache": 29284,
        "batch_size": 50
    },
    "checks": {
        "vulnerable_latest": {
            "n": 1,
            "title": "Vulnerable latest release",
            "scope": "full registry",
            "population": "Active entries in the official MCP Registry (status=active)",
            "unit": "entries",
            "computed": true,
            "numerator": 9,
            "denominator": 19765,
            "pct": 0.05,
            "excluded": [
                {
                    "reason": "No OSV-covered package (oci, mcpb or remote-only)",
                    "count": 36164
                },
                {
                    "reason": "Package has no version",
                    "count": 0
                },
                {
                    "reason": "Package version is a tag such as \"latest\" (OSV cannot check it)",
                    "count": 5
                }
            ],
            "definition": "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.",
            "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."
        },
        "advisory_any": {
            "n": 2,
            "title": "Any advisory ever filed",
            "scope": "full registry",
            "population": "Active entries in the official MCP Registry (status=active)",
            "unit": "entries",
            "computed": true,
            "numerator": 84,
            "denominator": 37746,
            "pct": 0.22,
            "excluded": [
                {
                    "reason": "No OSV-covered package and no GitHub repo (nothing to match on)",
                    "count": 18188
                }
            ],
            "definition": "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).",
            "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.",
            "extra": {
                "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
            }
        },
        "stale_repo": {
            "n": 3,
            "title": "Stale repo",
            "scope": "full registry",
            "population": "Distinct GitHub repos behind active registry entries (deduped by repo)",
            "unit": "repos",
            "computed": true,
            "numerator": 1957,
            "denominator": 25575,
            "pct": 7.7,
            "excluded": [
                {
                    "reason": "Unreachable on GitHub (deleted, private, renamed without redirect, blocked)",
                    "count": 3965,
                    "unit": "repos"
                },
                {
                    "reason": "Repo URL not on GitHub",
                    "count": 89,
                    "unit": "entries"
                },
                {
                    "reason": "No repo URL",
                    "count": 19146,
                    "unit": "entries"
                }
            ],
            "definition": "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.",
            "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.",
            "extra": {
                "archived": 228,
                "never_pushed": 0,
                "renamed_resolved": 785,
                "entries_on_stale_repos": 2154
            }
        },
        "no_security_policy": {
            "n": 4,
            "title": "No security policy",
            "scope": "full registry",
            "population": "Distinct GitHub repos behind active registry entries (deduped by repo)",
            "unit": "repos",
            "computed": true,
            "numerator": 17399,
            "denominator": 25575,
            "pct": 68,
            "excluded": [
                {
                    "reason": "Unreachable on GitHub (deleted, private, renamed without redirect, blocked)",
                    "count": 3965,
                    "unit": "repos"
                },
                {
                    "reason": "Repo URL not on GitHub",
                    "count": 89,
                    "unit": "entries"
                },
                {
                    "reason": "No repo URL",
                    "count": 19146,
                    "unit": "entries"
                }
            ],
            "definition": "Share of GitHub repos (deduped) where GraphQL isSecurityPolicyEnabled is false. Org-level policies count.",
            "caveat": "Shows that a policy exists, not that anyone answers it. Unresolvable and non-GitHub repos are left out as for the stale check.",
            "extra": {
                "entries_on_repos_without_policy": 22720
            }
        },
        "no_public_source": {
            "n": 5,
            "title": "No public source",
            "scope": "full registry",
            "population": "Active entries in the official MCP Registry (status=active)",
            "unit": "entries",
            "computed": true,
            "numerator": 19146,
            "denominator": 55934,
            "pct": 34.2,
            "excluded": [],
            "definition": "Share of entries with no repository URL in the registry.",
            "caveat": "Some of these are hosted, closed-source servers rather than neglected ones.",
            "extra": {
                "of_which_list_a_remote": 18217
            }
        },
        "plaintext_remote": {
            "n": 6,
            "title": "Plaintext remote",
            "scope": "full registry",
            "population": "Active entries in the official MCP Registry (status=active)",
            "unit": "entries",
            "computed": true,
            "numerator": 0,
            "denominator": 37722,
            "pct": 0,
            "excluded": [
                {
                    "reason": "No remote endpoint (package-only)",
                    "count": 18212
                }
            ],
            "definition": "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).",
            "caveat": "Base is entries that list a remote endpoint. We read the URL as published; we did not connect to it.",
            "extra": {
                "entries_with_localhost_http_remote": 0
            }
        },
        "grade_distribution": {
            "n": 7,
            "title": "Grade distribution",
            "scope": "editorial sample",
            "population": "Published hand-graded TrustedMCP listings (editorial sample, not representative of the registry)",
            "unit": "listings",
            "computed": true,
            "numerator": null,
            "denominator": 82,
            "pct": null,
            "excluded": [
                {
                    "reason": "No grade recorded",
                    "count": 0
                },
                {
                    "reason": "Draft (unpublished) listing",
                    "count": 0
                }
            ],
            "definition": "TrustedMCP grade (A-D) of each hand-graded listing, per TRUST-RUBRIC.md.",
            "caveat": "Editorial sample, not representative of the registry: these servers were picked for grading, mostly popular and vendor-backed ones.",
            "buckets": {
                "A": {
                    "count": 31,
                    "pct": 37.8
                },
                "B": {
                    "count": 27,
                    "pct": 32.9
                },
                "C": {
                    "count": 19,
                    "pct": 23.2
                },
                "D": {
                    "count": 5,
                    "pct": 6.1
                }
            }
        },
        "default_auth": {
            "n": 8,
            "title": "Default auth setting",
            "scope": "editorial sample",
            "population": "Published hand-graded TrustedMCP listings (editorial sample, not representative of the registry)",
            "unit": "listings",
            "computed": true,
            "numerator": null,
            "denominator": 82,
            "pct": null,
            "excluded": [
                {
                    "reason": "Draft (unpublished) listing",
                    "count": 0
                }
            ],
            "definition": "Authentication as recorded for each hand-graded listing: on by default (yes), supported but off by default (optional), or none (no).",
            "caveat": "Editorial sample, not representative of the registry.",
            "buckets": {
                "yes": {
                    "count": 55,
                    "pct": 67.1
                },
                "optional": {
                    "count": 12,
                    "pct": 14.6
                },
                "no": {
                    "count": 15,
                    "pct": 18.3
                }
            }
        },
        "transport_band": {
            "n": 9,
            "title": "Transport-safety band",
            "scope": "editorial sample",
            "population": "Published hand-graded TrustedMCP listings (editorial sample, not representative of the registry)",
            "unit": "listings",
            "computed": true,
            "numerator": null,
            "denominator": 82,
            "pct": null,
            "excluded": [
                {
                    "reason": "Draft (unpublished) listing",
                    "count": 0
                }
            ],
            "definition": "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.",
            "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\".",
            "extra": {
                "inferred": 0,
                "by_points": {
                    "20": 58,
                    "14": 11,
                    "6": 10,
                    "0": 3
                },
                "by_transport": {
                    "stdio only": 14,
                    "http only": 38,
                    "stdio + http": 30,
                    "other": 0
                }
            },
            "buckets": {
                "full": {
                    "count": 58,
                    "pct": 70.7
                },
                "partial": {
                    "count": 21,
                    "pct": 25.6
                },
                "none": {
                    "count": 3,
                    "pct": 3.7
                }
            }
        }
    },
    "dropped": [
        "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."
    ]
}
