Skip to content

The State of MCP Security, 2026

Report version 1.0 · Published 2026-08-17 · Scan date 2026-07-26 · Corpus: 2,303 distinct public MCP server configs · Tool: AgentAuditKit (offline, deterministic) · Licence: Apache-2.0 (same as the tool)

Stable identity. Cite this document as The State of MCP Security, 2026, version 1.0. The version changes only when a number in it changes; re-running the pipeline on the same committed corpus reproduces this exact file, so version 1.0 always refers to these numbers. See How to cite this report.

We statically scanned 2,303 distinct public Model Context Protocol server configurations — the largest snapshot we know of — and measured what fraction fail each security rule family. The headline: more than half of public MCP servers declare a remote endpoint with no authentication. Every number below carries its numerator, denominator, and — where a metric is not computable for the whole corpus — its coverage.

Methods in one paragraph. The corpus was collected 2026-07-26 from two provenance-tracked sources — 664 GitHub-crawled .mcp.json files (benchmarks/data/) and 1,639 latest-version servers from the official MCP Registry (registry.modelcontextprotocol.io) — deduplicated by SHA-256 of the normalised JSON. Each config was scanned in isolation with AgentAuditKit's engine and graded on the same penalty-based A–F scale as aak score. The scan makes zero network calls and is deterministic: the same corpus yields a byte-identical results.json across runs and Python hash seeds. Reproduce the published numbers with make report (offline); refreshing the corpus itself is a separate, explicitly network step (python research/state-of-mcp-2026/fetch_registry.py --target 5000). Full detail, including what a config-only corpus cannot measure, is in Method and Limitations.

Headline findings

Finding Rule Share of corpus
Remote MCP server with no authentication AAK-MCP-001 (critical) 52.1% — 1201 of 2,303
Carries at least one critical finding — 52.7% (1,213)
Fetches & executes remote packages via npx/uvx (supply-chain surface) AAK-MCP-005 (medium) 19.5% (450)
OAuth surface with no RFC 9728 Protected-Resource-Metadata discovery AAK-OAUTH-008 (low) 18.4% (424)
Command uses a relative path (PATH-hijack surface) AAK-MCP-006 (medium) 7.8% (179)
Secret in the env block (hardcoded credential) AAK-MCP-003 / AAK-SECRET-007 3.0% (70)
stdio launcher injection — shell interpreter with exec flag / interpolated args AAK-MCP-STDIO-LAUNCHER-INJECT-001 (high) 2.1% (49)
URL points to localhost / internal network (SSRF surface) AAK-MCP-009 (high) 1.9% (44)
Deprecated SSE transport (2026-07-28 stateless break) AAK-TRANSPORT-003 1.6% (36)

Auth posture, the 2026-07-28 profile:

  • No authentication: 1201 of 2,303 (52.1%).
  • RFC 9728 PRM discovery: 0 of 2,303 (0.0%) — not a single public server serves the discovery document the ratified auth spec expects.
  • Remote configs: 1,648 of 2,303 (71.6%). Of the 424 that embed an inline credential, 424 of 424 (100%) hardcode a static secret rather than reference an env var or use OAuth.

Method

Corpus construction

Two provenance-tracked sources, deduplicated by configuration content (SHA-256 of the normalised JSON):

Source Candidates In corpus
GitHub-crawled .mcp.json files (benchmarks/data/) 748 664
Official MCP Registry, latest-version servers (registry.modelcontextprotocol.io) 1,641 1,639
Combined 2,389 2,303

85 byte-duplicate configs and 1 unparseable file were removed. Registry servers were fetched on 2026-07-26 via cursor pagination (40-page crawl), filtered to isLatest = true / status = active, and each converted to a scannable .mcp.json-shaped config. The registry has grown fast — this snapshot is 1,641 distinct latest-version registry servers, up from 710 on 2026-07-19 (2.3×). Provenance for every registry server — name, version, transport, auth mode, source URL, fetch date — is recorded in corpus/registry-manifest.json, so each number here is reproducible from the committed manifest without re-fetching.

Scanning

Each config is scanned in isolation with AgentAuditKit's engine and scored with the same penalty-based A–F grade the aak score command and the MCP Security Index use. The scan is offline (zero network calls) and deterministic: the same corpus yields a byte-identical results.json on every run, across Python hash seeds — the property that makes a published number auditable.

Note on what is and isn't config-detectable: rule families that live in a client .mcp.json — no-auth, launcher injection, relative-path commands, env secrets, npx/uvx fetch-execute, transport — fire here. Families that require server source (tool-poisoning AAK-POISON-*, taint AAK-TAINT-*, the AAK-MCP-STDIO-CMD-INJ-* source detectors) do not fire on a config-only corpus and are reported as ~0% by construction, not by absence of risk. The MCP CVE version-pins (agent_audit_kit/scanners/mcp_cve_pins_2026_07.py) fire only when a config pins a specific vulnerable version; registry configs overwhelmingly use unpinned npx -y <pkg> (counted under AAK-MCP-005), so pinned-CVE hits are rare on this corpus.

Reproduce

make report          # refreshes results.json from the committed manifest, offline + deterministic
# or, explicitly:
python research/state-of-mcp-2026/run_report.py \
    --corpus benchmarks/data \
    --registry-manifest research/state-of-mcp-2026/corpus/registry-manifest.json \
    --out research/state-of-mcp-2026/results.json

Refreshing the corpus itself (the one network step) is separate: python research/state-of-mcp-2026/fetch_registry.py --target 5000.

At the published run this walk collected 1,641 distinct latest-version servers (corpus/registry-manifest.json → distinct_latest_servers, fetched_at 2026-07-26). --target 5000 is deliberately larger than that count so the walk runs to cursor-exhaustion rather than stopping early; the MCP Registry grows over time, so a later rerun that returns a larger N is registry growth, not a broken command. (--target is kept identical across the Makefile, the fetcher default, and this document by tests/test_corpus_target_consistency.py.)

Grade distribution (score calibration)

Grade Configs Share
A 637 27.7%
B 1,421 61.7%
C 111 4.8%
D 49 2.1%
F 85 3.7%

n = 2,303. The median config scores B. 1,213 (52.7%) carry at least one critical-severity finding — driven almost entirely by the no-auth remote server class. Most public configs are not catastrophic, but the single most common public posture is a remote server anyone can call.

Why this matters now — and the two wedges

The market backdrop is a supply-chain and trust crisis, not a scanner gap:

  • Shai-Hulud 2.0 — the self-propagating npm worm that republishes legitimate packages with a malicious preinstall script and uses GitHub infrastructure as C2 — compromised 25,000+ repositories across ~350 maintainers (Zapier, PostHog, Postman), with a Mini Shai-Hulud resurgence in May 2026 hitting 170+ npm packages (Microsoft Security). Our finding that 19.5% of public MCP servers npx/uvx-fetch-and-execute unpinned remote packages is that same blast radius, one .mcp.json away.
  • NSA MCP hardening guidance — the NSA AISC MCP Security Design Considerations CSI (U/OO/6030316-26, 2026-05-20) put MCP posture on government letterhead. It is prose, not a checklist — so the gap is evidence, not awareness.

Against that backdrop, two things a free hosted scanner structurally can't match:

  1. Offline + deterministic scanning. Zero network calls in the default scan path; the same input always yields the same finding (no model in the loop), so CI diffs, audit re-runs, and regression baselines stay byte-stable. This report is itself the proof: every number regenerates identically from the committed manifest.
  2. A compliance-evidence crosswalk. Every rule is mapped — rule by rule — to the NSA MCP Security CSI control it evidences and its OWASP Agentic Top-10 (2026) slot (also OWASP MCP Top-10 and EU AI Act), emitted as machine-readable evidence: agent-audit-kit --emit-coverage --format json (see docs/coverage.json and docs/crosswalk/nsa-csi-owasp-agentic.md). That turns a finding into audit evidence an assessor can cite.

We are not claiming to be first — public MCP-security surveys and vendor disclosures predate this. What is offline, deterministic, and standards-mapped is the combination.

2026-07-28 stateless-transport exposure

The ratified 2026-07-28 spec removes the initialize handshake and the Mcp-Session-Id header (stateless core). The measurable proxy in a client config is deprecated SSE transport: 36 configs (1.6%) still declare it and will break when the session model goes. streamable-HTTP dominates the fresh corpus (1,726 server entries) over stdio (1,347) and SSE (62).

Responsible-disclosure posture

Findings here are aggregate and de-identified; no server is named as vulnerable. The corpus is public registry metadata and public GitHub .mcp.json files. AAK's 90-day disclosure policy governs any per-server notification.

Limitations

  • Config-only for source-based families. Tool-poisoning and taint need server source; their ~0% here is a coverage boundary, not an all-clear (stated above).
  • Registry snapshot is a 40-page crawl (1,641 distinct latest); the registry is larger and still growing, so the true corpus is a floor.
  • Auth posture is inferred from declared config, not a live probe — a server could enforce auth the config doesn't declare (and vice-versa).
  • "Benign" is not claimed. This measures declared posture, not exploitability; see the benign-slice false-positive rate for how precise the high-severity findings are.

How to cite this report

Cite the report, not the repository, when you are citing a number from it — the repository moves and the report does not. Version 1.0 refers to the 2026-07-26 scan of 2,303 configs described above.

Plain text

Jain, S. (2026). The State of MCP Security, 2026 (Version 1.0). AgentAuditKit. https://github.com/sattyamjjain/agent-audit-kit/blob/main/research/state-of-mcp-2026/REPORT.md

BibTeX

@techreport{jain2026mcpsecurity,
  author      = {Jain, Sattyam},
  title       = {The State of MCP Security, 2026},
  institution = {AgentAuditKit},
  year        = {2026},
  month       = {8},
  type        = {Technical Report},
  number      = {Version 1.0},
  url         = {https://github.com/sattyamjjain/agent-audit-kit/blob/main/research/state-of-mcp-2026/REPORT.md},
  note        = {Corpus: 2,303 distinct public MCP server configurations, collected 2026-07-26. Offline, deterministic scan; reproducible with \texttt{make report}.}
}

Repository-level metadata is in CITATION.cff at the repo root, which GitHub renders as a "Cite this repository" widget. It points at this same identity, so the two cannot disagree about the report version.

Citing a specific number. Every figure above carries its numerator and denominator; quote both. For example: "0 of 2,303 public MCP configs served RFC 9728 Protected Resource Metadata discovery (Jain 2026, v1.0)." If you need the raw data, results.json beside this file is the machine-readable output, and corpus/registry-manifest.json records per-server provenance.