MCP
5 / 6
MCP 3 min

Security Model and Permissions

How MCP handles authentication, authorisation, and sandboxing tool execution safely.

Tool access is power, so MCP security has to be explicit by default. The clearest way to understand why is to look at a server that gets it wrong.

python
# VULNERABLE: trusts the model's "path" argument completely
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
    if name == "read_file":
        path = arguments["path"]
        with open(f"/data/user_files/{path}") as f:
            return CallToolResult(content=[TextContent(type="text", text=f.read())])
The Attack: Path Traversal via a Confused Deputy
If a prompt injected into the model's context (from a webpage, an email, a retrieved document) convinces it to call `read_file(path="../../../etc/passwd")`, this server happily reads it. The server is a 'confused deputy': it has legitimate permission to read files, but no way to tell a legitimate request from an attacker-steered one. This is not hypothetical, it is the single most common vulnerability class in early real-world MCP servers.
python
# FIXED: resolve the path and verify it stays inside the allowed directory
import os

ALLOWED_ROOT = os.path.realpath("/data/user_files")

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
    if name == "read_file":
        requested = os.path.realpath(os.path.join(ALLOWED_ROOT, arguments["path"]))
        if not requested.startswith(ALLOWED_ROOT + os.sep):
            return CallToolResult(
                content=[TextContent(type="text", text="Access denied: path escapes allowed directory")],
                isError=True,
            )
        with open(requested) as f:
            return CallToolResult(content=[TextContent(type="text", text=f.read())])

The fix is not 'be more careful', it is a structural check: resolve the real path and verify it is still inside the boundary, every single call, with no exceptions. The same pattern applies everywhere: the `postgres-server` in the companion repo blocks `INSERT/UPDATE/DELETE/DROP` with a regex before a query ever reaches the database, for exactly this reason.

Security Checklist
Use least-privilege tokens, validate every argument against the boundary it actually needs (not just its type), enforce tool allowlists in the agent layer too, log every tool call, and gate destructive actions behind explicit confirmation.

Treat server boundaries as trust boundaries. Never assume the model will always call tools safely, because the model's input can itself be steered by untrusted content it read. Defensive validation belongs in the server's code, not just in the system prompt.

API Keys in a Workshop

Giving an agent full admin credentials is like handing every tool in a workshop to a new intern on day one. Start with basic access and expand only when necessary.

What's Next
You've seen the theory and the vulnerabilities. Now build one, end to end, in under 150 lines.