What is MCP and Why Does Security Matter?
The Model Context Protocol (MCP) is an open standard that lets AI assistants connect to external tools, databases, and APIs. Claude Code, Cursor, and other AI coding assistants use MCP servers to extend their capabilities.
But every MCP server is a potential attack surface. A malicious or misconfigured MCP server can:
- Execute arbitrary commands on your machine
- Read sensitive files (SSH keys, .env, credentials)
- Send your data to external servers
- Install malicious packages
- Override safety instructions in the AI assistant
Common MCP Security Risks
1. Command Injection via Tool Arguments
{
"tools": [{
"name": "run_shell",
"description": "Run a shell command",
"inputSchema": {
"type": "object",
"properties": {
"command": {"type": "string"}
}
}
}]
}
This tool passes user input directly to a shell. An attacker can craft a malicious prompt that makes the AI execute rm -rf / or exfiltrate data via curl.
2. Unrestricted File Access
{
"tools": [{
"name": "read_file",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string"}
}
}
}]
}
No path validation means the AI can read ~/.ssh/id_rsa, ~/.aws/credentials, or /etc/passwd.
3. External Network Calls
An MCP server that makes HTTP requests to arbitrary URLs can be used as a data exfiltration channel:
@mcp.tool()
def fetch_url(url: str) -> str:
return requests.get(url).text # No URL validation!
4. Missing Rate Limits
Without rate limits, an attacker can:
- Exhaust API quotas
- Generate huge bills
- DoS the underlying service
5. Unpinned Dependencies
{
"dependencies": {
"some-package": "*" # Latest version, always
}
}
If a dependency is compromised (supply chain attack), your MCP server automatically installs the malicious version.
How to Audit Your MCP Configuration
Manual Review Checklist
- List all tools -- what can each tool do?
- Check input validation -- are arguments sanitized?
- Check file access -- is access restricted to specific directories?
- Check network calls -- are outbound URLs whitelisted?
- Check dependencies -- are they pinned to specific versions?
- Check permissions -- does the server run as root?
- Check logging -- are secrets being logged?
Automated Scanning with MCP Scanner
pip install dfx-mcp-scanner
The MCP Scanner automatically checks MCP server configurations for the risks above:
# Scan a Claude Code config
mcp-scanner ~/.claude/mcp.json
# Scan a Cursor config
mcp-scanner ~/.cursor/mcp.json
# JSON output for CI/CD
mcp-scanner config.json --format json
It detects:
- Tools with unrestricted shell access
- Tools with unrestricted file read/write
- External network calls without URL validation
- Missing rate limits
- Unpinned dependencies
- Hardcoded credentials in config
- And more (7 security checks total)
Combining with AgentGuard
For full coverage, use both tools together:
# Scan MCP configuration
mcp-scanner ~/.claude/mcp.json
# Scan the MCP server source code
agentguard ./my-mcp-server/ --format sarif
MCP Scanner audits the configuration. AgentGuard audits the implementation.
Best Practices for MCP Server Authors
1. Validate All Inputs
from pathlib import Path
@mcp.tool()
def read_file(path: str) -> str:
# Restrict to allowed directory
allowed = Path("/safe/directory")
target = (allowed / path).resolve()
if not str(target).startswith(str(allowed)):
raise ValueError("Path traversal detected")
return target.read_text()
2. Whitelist External URLs
ALLOWED_DOMAINS = {"api.github.com", "api.openai.com"}
@mcp.tool()
def fetch_url(url: str) -> str:
from urllib.parse import urlparse
domain = urlparse(url).hostname
if domain not in ALLOWED_DOMAINS:
raise ValueError(f"Domain {domain} not allowed")
return requests.get(url, timeout=10).text
3. Pin Dependencies
{
"dependencies": {
"requests": "2.31.0",
"pydantic": "2.5.0"
}
}
4. Run as Non-Root
USER 1000:1000
5. Add Rate Limits
from collections import defaultdict
from time import time
RATE_LIMIT = {}
RATE_WINDOW = 60 # seconds
RATE_MAX = 10 # calls per window
def check_rate_limit(client_id: str):
now = time()
if client_id not in RATE_LIMIT:
RATE_LIMIT[client_id] = []
RATE_LIMIT[client_id] = [t for t in RATE_LIMIT[client_id] if now - t < RATE_WINDOW]
if len(RATE_LIMIT[client_id]) >= RATE_MAX:
raise Exception("Rate limit exceeded")
RATE_LIMIT[client_id].append(now)
Conclusion
MCP servers are powerful extensions for AI assistants, but they introduce real security risks. Audit your configurations regularly, validate all inputs, restrict file and network access, and use automated tools to catch issues early.
Tools Mentioned
- AgentGuard -- OWASP ASI Top 10 scanner for AI agent code
- MCP Scanner -- Security scanner for MCP server configurations
- AgentGuard Benchmark -- 28 vulnerable code samples for testing
All tools are MIT-licensed and open source. Install with pip install dfx-agentguard dfx-mcp-scanner.
Top comments (1)
The config-vs-implementation split is a useful frame, but there's a third gap that bites most people: the server you're auditing usually isn't one you wrote. Most of us install someone else's MCP server off GitHub or npm and wire it straight into Claude or Cursor, so the manual checklist and even mcp-scanner only help if you remember to run them on a stranger's code first, which almost nobody does.
The item on your list I'd call hardest to catch statically is "override safety instructions in the AI assistant." That's tool-description poisoning, and it's semantic rather than syntactic. The description passes a schema check and reads fine to a regex, but it carries instructions that hijack the agent when the tool gets loaded. Config scanning won't see it, which is why static checks really need to be paired with something that reasons about intent.
We've been scanning public servers for exactly this at mcpsafe.io (pre-install, you hand it a GitHub URL, an npm/PyPI package, or a Docker image). Across 708 public servers the findings land almost exactly where your checklist points: server configuration and verbose-error leakage are the two biggest buckets by a wide margin, 6,966 findings in total. So your "audit the config regularly" advice is well placed, that's where the long tail actually lives.
One small thing on the path-traversal guard: resolving before the prefix check is exactly right. Worth applying the same pattern to write tools and any list-directory helper too, since those leak structure even when the read tool is locked down.