I scanned 15 popular Python MCP servers — 10 still ship a vulnerable Starlette pin
· mcpsecuritystarletteauditcve-2026-48710fastmcp
I built a small static auditor for MCP server repos — mcp-audit. It ships two checks in v0.1: an old Starlette pin (the BadHost CVE-2026-48710 host-header bypass, patched in 1.0.1) and a FastMCP wrapper-layer pattern that swallows tool errors at runtime.
Then I pointed it at 15 of the most-starred public Python MCP server repos on GitHub.
10 of 15 (67%) had at least one finding. Every single finding across the whole corpus was the same CVE. The wrapper-layer check fired zero times across all 15 repos.
This post is the honest writeup. There are real caveats — most of the flagged servers default to stdio transport, which means the BadHost CVE isn’t exploit-reachable in the common case. I’ll get to that. But “stdio defends us in practice” is not the same thing as “the dependency is fine,” and the pattern across the corpus is more interesting than either headline allows on its own.
What I scanned and how
I picked 15 Python MCP server repos covering a mix of:
- Official infrastructure: modelcontextprotocol/servers, modelcontextprotocol/python-sdk, jlowin/fastmcp, awslabs/mcp.
- High-star community projects: ahujasid/blender-mcp (23.1k stars), oraios/serena (25.7k), LaurieWired/GhidraMCP (9.3k).
- Narrower domain MCPs: blazickjp/arxiv-mcp-server, MarkusPfundstein/mcp-obsidian, runekaagaard/mcp-alchemy, crystaldba/postgres-mcp, rohitg00/kubectl-mcp-server, abhiemj/manim-mcp-server, hannesrudolph/sqlite-explorer-fastmcp-mcp-server, strands-agents/mcp-server.
For each repo, a shallow clone, then mcp-audit /path/to/clone --json. The tool walks pyproject.toml, requirements*.txt, uv.lock, poetry.lock, and pdm.lock for the Starlette check, and AST-scans every .py file for the FastMCP one. All 15 cloned cleanly. Zero clone failures.
The whole run took less than a minute end to end on a laptop.
Headline numbers
| Metric | Value |
|---|---|
| Repos audited | 15 |
| Clone failures | 0 |
| Repos with at least one finding | 10 (67%) |
| Repos clean | 5 (or 6, depending on how you count — see below) |
| Total findings | 68 |
starlette_badhost findings |
68 |
fastmcp_wrapper_layer findings |
0 |
68 findings sounds dramatic. It mostly isn’t: 58 of those 68 are from one repo — awslabs/mcp, the official AWS-maintained MCP monorepo, which ships ~30 sub-servers each with its own uv.lock pinning Starlette at 0.50.0 or 1.0.0. That’s one upstream miss multiplied dramatically by monorepo structure. Treat it as one issue with broad blast radius rather than 58 independent bugs.
Strip out the monorepo amplification and the picture is: 10 distinct repos each carry the same CVE-affected pin once or twice each.
The Starlette BadHost pattern
CVE-2026-48710 is a host-header bypass in Starlette’s TrustedHostMiddleware. The fix is Starlette 1.0.1.
The exact pins I observed across the corpus:
| Pin / spec | Where | Severity |
|---|---|---|
starlette==0.41.3 |
mcp-obsidian/uv.lock | HIGH |
starlette==0.43.0 |
mcp-alchemy/uv.lock | HIGH |
starlette==0.46.0 |
blender-mcp/uv.lock | HIGH |
starlette==0.46.1 |
postgres-mcp/uv.lock | HIGH |
starlette==0.49.1 |
python-sdk/uv.lock | HIGH |
starlette==0.50.0 |
awslabs/mcp (many sub-locks) | HIGH |
starlette==0.52.1 |
fastmcp examples + arxiv-mcp-server | HIGH |
starlette==1.0.0 |
awslabs/mcp (many sub-locks) | HIGH |
starlette>=0.27.0 (range) |
kubectl-mcp-server/requirements.txt | MEDIUM |
starlette>=... (ranges) |
python-sdk + arxiv pyproject.toml | MEDIUM |
The 1.0.0 pin is interesting — one patch version below the fix. Whoever bumped to it bumped before 1.0.1 shipped, then never bumped again.
The medium-severity findings are the spec ranges. They permit a resolver to pick a vulnerable version, and the lockfiles next to them prove that’s exactly what happens.
The interesting silence: the wrapper-layer check found nothing
The second check in v0.1 flags sync @mcp.tool() functions that call asyncio.run() inside their body — the bug that ships green unit tests and then dies on the first real MCP protocol call with RuntimeError: asyncio.run() cannot be called from a running event loop. (We shipped this exact bug ourselves in April; full writeup here.)
Across 15 repos, including FastMCP’s own codebase: zero hits.
I don’t think that means the pattern is extinct. The honest reads are:
- The bug is genuinely rare in shipped code — most authors discover it the first time they try the server over the protocol and they fix it before publishing.
- The detector is too strict — only catching the exact
asyncio.run()inside@mcp.tool()shape, and not the broader family of “swallowed async errors” that the wrapper-layer post talks about.
One null result on a 15-repo sample is not evidence the pattern is solved. It’s evidence the check needs either tuning or retirement before v0.2. I’ll come back to this.
Per-repo: who got hit hardest
| Repo | Findings | Notes |
|---|---|---|
awslabs/mcp |
58 | Monorepo amplification — one outdated upstream pin in ~30 sub-server lockfiles. |
modelcontextprotocol/python-sdk |
3 | 1 HIGH lockfile pin + 2 MEDIUM pyproject.toml ranges. The reference SDK ships the vulnerable pin. |
blazickjp/arxiv-mcp-server |
2 | uv.lock at 0.52.1 + a permissive spec range. |
jlowin/fastmcp |
1 | Framework itself is clean. The finding is in examples/testing_demo/uv.lock. |
ahujasid/blender-mcp |
1 | uv.lock at 0.46.0. |
MarkusPfundstein/mcp-obsidian |
1 | uv.lock at 0.41.3 — the oldest pin in the corpus. |
runekaagaard/mcp-alchemy |
1 | uv.lock at 0.43.0. |
crystaldba/postgres-mcp |
1 | uv.lock at 0.46.1. |
rohitg00/kubectl-mcp-server |
1 | requirements.txt range, medium severity. |
modelcontextprotocol/servers |
0 | Clean. Canonical reference monorepo. |
oraios/serena |
0 | Clean. |
LaurieWired/GhidraMCP |
0 | Clean — but partly because the Python entrypoint doesn’t pull Starlette at all. Not the same thing as actively patched. |
abhiemj/manim-mcp-server |
0 | Clean — same caveat. |
hannesrudolph/sqlite-explorer-fastmcp |
0 | Clean. |
strands-agents/mcp-server |
0 | Clean. |
There’s a pattern visible in the per-repo column: frameworks upgrade faster than the servers built on them. FastMCP’s own repo is current. The downstream servers that wrap it (blender-mcp, mcp-obsidian, mcp-alchemy, postgres-mcp, arxiv-mcp-server) all carry the vulnerable pin.
What this actually means for users
The big honest caveat: BadHost requires an HTTP listener using TrustedHostMiddleware. Most of these MCP servers run over stdio transport by default — Claude Desktop spawns them as subprocesses; there’s no port, no Host header, no listener for the bug to reach. In that mode, the vulnerable pin sits in your dependency tree but doesn’t expose anything.
If you run any of these servers over HTTP / SSE / streamable-HTTP transport, especially exposed beyond localhost, the calculus changes. That’s the configuration where the CVE matters, and that’s the configuration where you should upgrade Starlette in your environment regardless of what the upstream lockfile says.
If you’re a maintainer of one of these repos: bump the lockfile. It’s a one-line uv lock --upgrade-package starlette (or pip equivalent) and it propagates to every downstream uv sync. Even if your own server is stdio-only, your users may use it differently.
Limitations
I want to be honest about how narrow this snapshot is:
- v0.1 ships two checks. A clean result from
mcp-auditmeans no hits on these two rules. It’s not a security clearance. - Stdio servers aren’t exploit-reachable for BadHost. The findings here are defense-in-depth concerns for stdio deployments, not active exploits.
- The awslabs/mcp count is monorepo-amplified. One upstream issue, ~30 sub-server lockfiles, looks like 58 findings.
- Two of the “clean” results are clean by absence of dependency, not active patching. Ghidra’s Python bridge and the Manim server don’t pull Starlette at all.
- Single-snapshot scan on 2026-06-24. Maintainers may already be patching as you read.
fastmcp_wrapper_layerreturning zero across 15 repos is suspicious. Either the pattern is rare or the detector is too strict. I’m treating that null result as “the check needs work” rather than “the bug class is solved.”
What’s next
The whole thing — checks, fixtures, tests, CLI — is open-source and pip-installable. The repo: github.com/Alienbushman/mcpdone-samples/tree/master/mcp-audit. Install it, point it at your MCP server, see what it says:
pip install -e ./mcp-audit
mcp-audit /path/to/your/mcp/server
For v0.2 I want to add: tool-input validation gaps, command-injection sinks in shell-wrapping MCPs (the kubectl- and blender- shaped ones), and path-traversal in filesystem-touching servers. The wrapper-layer detector also needs to either broaden into the wider “swallowed async errors” family or be retired.
If you find a check class that should exist and doesn’t, the issue tracker on the samples repo is the place. I’d rather ship a useful third check than a wider net of false-positive ones.