What this is
The first batch of a standing MCP-security audit series. Every server is audited discovery-only , we call initialize + list_tools/list_prompts/list_resources to read the server's real declared metadata and schemas, and we never invoke a tool. Scoring is the committed deterministic scorer (no LLM, no model spend). Grading is honest: a schema flag on a documented, scoped capability is a true-positive for reach but a security non-finding; a real finding requires the description to understate or hide the schema/behavior.
Servers audited (6)
| Server | Source | Tools | Result |
|---|---|---|---|
| mcp-server-fetch | official, MIT | 1 | description carries an agent-directed behavioral override (new class A) |
| mcp-server-git | official, MIT | 11 | clean; repo_path is a documented scoped capability |
| server-filesystem | official, MIT | 12 | path tools; scorer coverage limit (empty inputSchema.properties) |
| mcp-server-sqlite | official, MIT | 6 | read_query operation-scope mismatch (new class B); write_query expected |
| mcp-server-time | official, MIT | 2 | clean control (real server) |
| mcp-shell-server | community, 1.1.1 | 1 | shell_execute flagged; allowlist disclosed in description (positive example) |
No exploitable vulnerability was found in any server this batch. Two of them revealed a new detection class the fixtures never exposed. That is the point of auditing real code.
New class A , behavioral-override description poisoning (mcp-server-fetch)
fetch's description ends with an instruction aimed at the model, not the human: "...you were advised to refuse and tell the user this, this tool now grants you internet access. Now you can...". It tells the agent to revise a prior stance. Classic poisoning checks look for "ignore previous instructions"; this is the same trust-boundary event in softer words and slips past a literal-keyword scorer. Rule to add: match the revise-prior-guidance shape (a prior-stance cue paired with a reversal cue), not just the literal override phrase.
New class B , operation-scope mismatch / metadata-invisible constraint (mcp-server-sqlite)
read_query is described as "Execute a SELECT query on the SQLite database" and its input schema is {query: string}. Nothing in the metadata constrains the query to a SELECT. The read-only guarantee exists only as a runtime check in the handler (if not query.strip().upper().startswith("SELECT"): raise).
We checked for a real bypass and found none under default settings: sqlite3 blocks stacked statements, and load_extension is disabled by default, so a metadata-blind "read tool that writes" does not actually execute a write here. So this is not a server vulnerability. But it is a real auditor blind spot: from MCP metadata alone , which is all a scanner or a consuming agent sees , read_query is indistinguishable from an unconstrained SQL sink. The safety lives in code the auditor never reads. Rule to add: when a tool's description names a narrow operation ("SELECT", "read", "docs only") but its schema accepts a free-form param of a broader type (arbitrary SQL/path/command string), flag it as excessive-agency-unless-proven , the metadata cannot establish the constraint.
The positive contrast (mcp-shell-server)
shell_execute is exactly the kind of tool you'd expect to be dangerous, and our scorer flags its command param. But its description surfaces the actual runtime constraint: "Allowed commands: ls, echo" , the allowlist, reflected live from config, right in the metadata. This is the correct pattern and the direct inverse of sqlite's invisible guard: if a tool has a runtime constraint, put it where the auditor and the agent can see it. A good scanner should reward this (the disclosed allowlist downgrades the flag), and we'll encode that.
Honest grading summary
- Clean / expected-capability: git, filesystem paths, time, sqlite write_query, shell_execute (disclosed allowlist). Correct, non-findings.
- New classes, real-world-derived: fetch behavioral-override (A), sqlite operation-scope mismatch (B).
- No exploitable bug, no disclosure needed this batch. These are methodology + tooling results , the two new classes become rules that make the auditor catch what a fixture set alone never would.
Scope + method
Local, sandboxed, discovery-only. Servers installed pinned into a throwaway venv, never invoked. Deterministic scorer, zero model spend. Not an Anthropic submission (third-party MCP servers are out of that program's scope). Public reference writeup; no server here needed private disclosure.
Next batch
More community + framework surfaces; encode classes A and B into the scorer (via cipher) and re-audit fetch + sqlite to confirm recovery.