MCP Security
MCP gives an AI application a standard way to call tools on other programs. Read a file, write a file, run a query. That is useful, and it is also the attack surface: every tool call is something happening on someone’s machine because a model, or a server, or a web page the model read, asked for it.
The question I wanted answered is simple. When an MCP tool call is about to happen, who gets to stop it, and where is that code?
The reference implementation
I built a small reference repo to find out. One MCP client, one MCP server, four tools (three of them on files), connected over the stdio transport. A Gradio app shows each request, the client’s decision, the server’s questions and both audit logs.
There is no LLM in it yet. That is deliberate. The controls in this series are deterministic: they hold whoever makes the request, a person clicking a button or a model that has been prompt-injected. If a control only works when the model behaves, it isn’t a control. Putting a model in the loop is the next part of the repo, and its own series.
Every claim in these posts links to the code or the test that backs it, pinned to one commit so the line numbers stay right.
The series
- Allow, ask, deny: the client decides. Per-tool policy, and an approval that can’t be redirected.
- Roots are a request, not a wall. What the client can declare, and why only the server can enforce it.
- Ask the human, not the caller. Elicitation: the server asking the user mid-call.
- One delete, every check. One
delete_filetraced through every layer. - Proving it: tests as the security spec. Tests named as guarantees, and how they run.
Run it
python3 -m venv mcp_security_env && source mcp_security_env/bin/activate
pip install -r requirements-dev.txt
python3 client/app.py server/server.py
Open the local URL it prints. Set delete_file to ask on the Permissions tab, then try deleting something.