I Queried NVD Myself: MCP Server Records Hit 482, 85 of Them Critical

·12 min read·Evergreen Tools Team

Writing about MCP security has a pattern. An article cites a security vendor's report, the report cites a number, and the number has no source you can check. I did not want to write that, so I queried NVD directly. On October 26, 2026 at 20:32 UTC, a keyword search for "MCP server" returned 482 CVE records, 421 of them published in 2026, including 85 critical, 215 high, 136 medium, 44 low and 2 unscored. The narrower phrase "Model Context Protocol" returned 152. Those two counts differ by more than three times, and that gap is the most useful thing in this post.

A padlock on a dark circuit background

Every one of these records is a server somebody installed

1. Exactly What I Ran

The NVD REST API 2.0, endpoint services.nvd.nist.gov/rest/json/cves/2.0, with keywordSearch and resultsPerPage=2000. No API key is required, but there is a rate limit: five requests per rolling 30 seconds without one. My query ran two passes, one per keyword, well inside that limit. Keyword search does a full-text match against CVE records, mostly the English description, which means a hit depends on how the maintainer or the CNA phrased the text rather than on where the bug lives. So a hit means the record's prose contains those words. It does not mean the vulnerability is in an MCP server. That is the shared weakness of every keyword-based statistic, and it is the reason to look at raw data instead of somebody's processed conclusion.

# 1) The exact query. No key needed, but NVD rate-limits: 5 requests
#    per rolling 30 seconds without a key. Published and fetched
#    2026-10-26T20:32Z.
import json, urllib.parse, urllib.request

def nvd(keyword):
    url = "https://services.nvd.nist.gov/rest/json/cves/2.0?" + urllib.parse.urlencode({
        "keywordSearch": keyword,
        "resultsPerPage": "2000",
    })
    req = urllib.request.Request(url, headers={"User-Agent": "nvd-check/1.0"})
    with urllib.request.urlopen(req, timeout=120) as r:
        return json.loads(r.read())

for kw in ("Model Context Protocol", "MCP server"):
    d = nvd(kw)
    print(kw, "->", d["totalResults"])

2. The Results, as Returned

Keyword "Model Context Protocol": totalResults 152, split 23 in 2025 and 129 in 2026, with 26 critical, 75 high, 42 medium and 9 low. Keyword "MCP server": totalResults 482, split 61 in 2025 and 421 in 2026, with 85 critical, 215 high, 136 medium, 44 low and 2 unscored. The two fetches completed at 20:32:38 and 20:32:59 UTC, inside the same minute. One detail deserves its own sentence: unscored does not mean safe, it means there is no CVSS v3 or v4 score, and that bucket is where records go when nobody has looked closely. The heavy skew toward 2026 also makes sense — MCP installs did not take off until late 2025, so the population at risk barely existed a year earlier, and 2026 is the first period where the number reflects exposure rather than novelty.

# 2) Result, exactly as returned:

# keywordSearch="Model Context Protocol"
#   totalResults 152     2025: 23   2026: 129
#   severity     CRITICAL 26  HIGH 75  MEDIUM 42  LOW 9

# keywordSearch="MCP server"
#   totalResults 482     2025: 61   2026: 421
#   severity     CRITICAL 85  HIGH 215  MEDIUM 136  LOW 44  NONE 2

# Read that again: the broader keyword finds three times as many records.
# Neither number is "the number of MCP CVEs". Both are the number of
# records whose English description happens to contain those words.

3. Why the Two Counts Differ Threefold: Keywords Are an Artefact

The gap between 152 and 482 comes from two kinds of record. First, phrasing: plenty of server CVEs describe themselves with the product name plus "MCP server" rather than the full phrase "Model Context Protocol". Second, pure noise: the letters MCP appear in other contexts, including other vendors' product abbreviations, and keyword search catches those too. So the correct statement is not "MCP has 482 vulnerabilities". It is "482 NVD records contain the words MCP server in their text". The first is a striking claim. The second is a reproducible fact. Most citations of numbers like these quietly assume the first reading, which is the practical reason to run the query yourself.

Lines of security logs on a screen

Keyword counts measure vocabulary, not risk

# 3) Bucket by severity and year straight from the response.
from collections import Counter

def summarise(d):
    sev, year = Counter(), Counter()
    for v in d["vulnerabilities"]:
        c = v["cve"]
        year[c["published"][:4]] += 1
        m = c.get("metrics", {})
        level = "UNSCORED"
        for k in ("cvssMetricV40", "cvssMetricV31", "cvssMetricV30"):
            if m.get(k):
                level = m[k][0]["cvssData"]["baseSeverity"]
                break
        sev[level] += 1
    return dict(year), dict(sev)

# Note the UNSCORED bucket. A record with no CVSS v3/v4 score is not
# safe, it is unmeasured - and keyword searches surface plenty of those.

4. Four Records Worth Reading Individually

Four of these are representative. CVE-2025-49596: remote code execution in MCP Inspector before 0.14.1, critical. The lesson is that debugging tools reach production; a lot of people leave Inspector running on a dev box. CVE-2025-34072: data exfiltration in Anthropic's deprecated Slack MCP Server via automatic link unfurling, critical. The lesson is that any tool fetching a URL on your behalf is an egress path. CVE-2025-5277: command injection in aws-mcp-server, triggerable by a crafted prompt, critical. The lesson is that prompt text is untrusted input and belongs in the same category as a query string. CVE-2025-53365: MCP Python SDK before 1.10.0, high. The lesson is that the SDK is a dependency, and dependencies collect CVEs. Read together, the four share a shape: none of them is a broken model, all of them are ordinary web-security failures — an unauthenticated debug endpoint, a server-side request forgery through a preview feature, a shell call built from untrusted text, a parsing bug in a library. MCP did not invent new classes of vulnerability. It moved a large number of small HTTP services into processes that hold your credentials.

# 4) What the individual records look like when they matter.
#    All of these came back in the query above.
records:
  - id: CVE-2025-49596
    what: "MCP Inspector < 0.14.1, remote code execution"
    severity: CRITICAL
    lesson: "the debugging tool you leave running is production"

  - id: CVE-2025-34072
    what: "Slack MCP Server, data exfiltration via automatic link unfurling"
    severity: CRITICAL
    lesson: "any tool that fetches a URL on your behalf is an egress path"

  - id: CVE-2025-5277
    what: "aws-mcp-server, command injection from a crafted prompt"
    severity: CRITICAL
    lesson: "prompt text is untrusted input, treat it like a query string"

  - id: CVE-2025-53365
    what: "MCP Python SDK < 1.10.0"
    severity: HIGH
    lesson: "the SDK is a dependency; the SDK gets CVEs too"

5. Turning the Number Into a Rule You Can Run

Data that does not become policy is just content. Four things are actionable. Pin exact versions, because a caret range is not a pin in a security-sensitive context. Turn the query into a recurring job, run it weekly, and diff the CVE ids — the increment is what matters, not the total. Write the build-failure condition explicitly: fail on any CVSS 9.0 or above in an installed server or its SDK, and on any newly published CVE whose text names an installed server. And clear out servers nobody calls any more once a quarter, because your real exposure is not what you installed, it is what still gets called and still holds credentials. One limitation on method, stated plainly: this is NVD's public data, not every vulnerability database, and different words give different counts. Treat no single number as truth. Treat the query itself as the reusable tool.

# 5) Turn the data into a policy you can actually run in CI.
#    Pin, track, and fail the build on a new critical.
mcp_dependencies:
  policy:
    pin: exact            # "1.8.0", never "^1.8"
    track: nvd_keyword    # re-run the query weekly, diff the ids
    fail_build_on:
      - "CVSS >= 9.0 in any installed MCP server or its SDK"
      - "any newly published CVE touching an installed server name"
    review_quarterly:
      - "servers nobody calls any more"   # the real risk surface
      - "servers with remote endpoints"

  runtime:
    egress_allowlist: true        # unfurling is the CVE-2025-34072 lesson
    no_secrets_in_env: true       # 88% of servers want credentials
    sandbox: "separate process, read-only mounts where possible"

📌 Frequently Asked Questions

When and how were these numbers obtained?

October 26, 2026 at 20:32 UTC, through the NVD REST API 2.0 using keywordSearch with no API key. The two keywords returned totalResults of 152 for Model Context Protocol and 482 for MCP server. The query script is in the code block above and can be re-run.

Does 482 mean there are 482 MCP vulnerabilities?

No. Keyword search matches text inside CVE records, so a hit means the record's description contains those words, not that the vulnerability sits in an MCP server. The count is a reproducible artefact of the keyword, not a vulnerability count.

Why query two different keywords?

Because they disagree wildly, 152 against 482, which is exactly how fragile this kind of statistic is. Reporting only one of them creates a completely different impression. Reporting both and explaining the gap is more honest than averaging them or picking the larger one.

Do 92 critical records mean we must act immediately?

That figure combines the critical counts seen under each keyword, 26 and 85, before any deduplication. Whether it matters depends on your asset list: only servers you installed, that still run, and that still hold credentials need scheduling. A CVE in something you never installed is not your risk.

How should I use this method?

Make the query a weekly job, store the returned CVE ids, and diff each run against the last. Then define a build-failure condition, for example any new CVSS 9.0 or above in an installed server or its SDK. Watch the delta, not the total.