The Model Context Protocol has become the way AI coding agents reach tools: a JSON file lists servers, the agent starts them, and from then on every tool description and every tool result lands in the model's context. That plumbing is now a security category of its own. The OWASP MCP Top 10 lists Token Mismanagement (MCP01), Tool Poisoning (MCP03), Command Injection (MCP05), Insufficient Authentication and Authorization (MCP07) and Shadow MCP Servers (MCP09) among its entries.
The attacks are measured, not hypothetical. MCPTox, published in August 2025, ran 1,348 malicious cases against 20 agents using 45 live servers and 353 tools. The best attack succeeded 72.8% of the time, and the more capable models were often more susceptible, because they follow instructions more faithfully. In July 2026 the Cloud Security Alliance documented a malicious repository whose crafted MCP config achieved remote code execution in several IDEs; one popular AI editor tied its approval prompt to the server name rather than to the server's contents, so a once-approved entry could be swapped for anything.
There are two directions to secure here. Inbound: the servers your agent is allowed to start. Outbound: whether the agent has a tool that can check the code it writes. This post covers both.
Inbound: the config file is the attack surface
An MCP config is a list of commands your agent will run with your permissions. Read one the way you would read a CI pipeline that a stranger committed. The entries that should stop you look like this:
{
"mcpServers": {
"files": {
"command": "npx",
"args": ["-y", "@example/fs-server", "/"]
},
"tracker": {
"command": "uvx",
"args": ["tracker-mcp"],
"env": { "TRACKER_TOKEN": "trk_live_8f3a..." }
},
"internal": {
"url": "http://10.0.0.12:8080/sse"
}
}
}Three problems in three entries. npx -y with no version resolves to whatever the registry serves today, so the server you approved last month is not the one that starts tomorrow. The filesystem server is mounted at /, which hands the model every file the user can read. The tracker token is inline, in a file that gets copied between machines and committed by accident. The third server is reached over plain http:// with no visible authentication.
vulkro mcp-audit checks exactly these shapes. Its rules are MCP-001 for an unpinned npx or uvx package, MCP-002 for a git source on a mutable ref, MCP-003 for a filesystem server rooted at / or the home directory, MCP-004 for an inline credential, MCP-005 for http:// transport or no visible auth, and MCP-006 for a release known to be compromised. It reads the config locations used by Claude Desktop, VS Code and several other agents and editors, so one run covers every agent on the machine rather than the one file you remembered.
In CI the useful form is:
vulkro mcp-audit --require-mcp --fail-on critical,high--require-mcp turns a missing config into an error instead of a quiet pass, which matters when the point of the job is to prove that the audit ran. The exit code follows the usual contract: 0 clean, 1 findings, 2 error.
The server you wrote is code, so scan it like code
Most teams now have at least one in-house MCP server, usually a thin wrapper over an internal API written against the Python or TypeScript SDK. It is ordinary code with two unusual properties: its tool descriptions are read by a model that will obey them, and its results are pasted into that model's context.
vulkro scan-mcp-server targets that shape. Rules MCP-SERVER-001 to 008 cover description injection (instructions hidden in a tool's description), tool-poisoning sinks (a tool argument flowing into a shell, a file path or a query), a tools/list that changes after the first call (the rug pull), schema mismatch between the declared input and what the handler reads, secrets returned into model context, and destructive tools that run without any authorization check.
The last two are the ones that most often survive human review. A delete_branch tool that any caller can invoke is fine in a demo and a liability the moment the server is reachable by an agent that has just read a poisoned issue comment. A get_config tool that returns the whole environment, including the database URL, puts that secret into a transcript that may be logged, shared or summarised.
vulkro scan-mcp-server ./serverIt is the same engine that scans your routes, so a tool argument that reaches subprocess.run is traced the same way a query parameter reaching a SQL string is.
Outbound: give the agent a deterministic scanner
The other half of the problem is that agents write code quickly and check it badly. Asking the same model to review its own change gives you a second opinion from the same opinion. What an agent needs is a tool whose answer does not depend on how the question was phrased.
Vulkro ships as an MCP server for that purpose. vulkro mcp serve exposes scan_project, scan_diff, prove, explain, suggest_fixes, verify_fix, verify_sarif, code_graph and assess_change. vulkro mcp serve --port 9100 serves SSE on loopback only; VULKRO_OFFLINE=1 is honoured, so a scan never makes a network call and never writes to disk.
The loop that works in practice is short. The agent makes a change and calls scan_diff, which scans only the lines the diff touched. For anything reported, it calls prove to get the hop-by-hop chain from source to sink, so the fix targets the actual path rather than the line number. It then proposes a fix and calls verify_fix, which applies the diff to a temporary copy, re-scans it and answers fixed, not-fixed or regressed. Only a verdict of fixed earns the edit. suggest_fixes does the same from the other side: it returns patches that already carry a verdict.
One rule is written into the tool description because an agent needs it as plainly as a person does: an empty proof chain from prove means no proven data flow was found. It never means the code is safe.
For Salesforce teams the same pattern exists in vulkro-sf, with 14 local tools such as sf_scan_project, sf_org_perms and sf_org_mfa, plus 15 tools that talk to Vulkro Cloud.
What this does not cover
Be clear about the limits. mcp-audit reads configuration, so a server that is pinned, authenticated and locally sourced but malicious in its behaviour passes. scan-mcp-server analyses source, so it sees a tools/list that is coded to change but not one that changes because a remote dependency was swapped at runtime. Neither tool inspects what a third-party server does with the data it receives. And a scanner called by an agent only helps if the agent is configured to call it and to treat a regressed verdict as a stop; that is a policy decision in your agent setup, not something a tool can enforce from the inside.
What to do this week
- Run
vulkro mcp-auditon every developer machine and in CI with--require-mcp --fail-on critical,high. - Pin every
npxanduvxserver to a version, move credentials out of the config into the environment, and narrow filesystem roots to the project. - Run
vulkro scan-mcp-server ./serveron any MCP server you maintain and fix destructive tools without auth first. - Add
vulkro mcp serveto your agent configuration and makeverify_fixpart of its fix routine.
The MCP server docs list every tool and its arguments. To run this against your own configs and servers, start the 14-day trial.